Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/changed-files.spec.ts

typescript299 行查看原文
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import type { ElectronApplication } from "@playwright/test";
import { chunk, deferred, done, fakeServer, send } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { expect, test } from "../helpers/test.ts";

test("changed files start collapsed, group repeated edits, scroll within the card and open diffs", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-changed-files-"));
  const project = join(dir, "project");
  await mkdir(join(project, "src"), { recursive: true });
  await mkdir(join(project, "tests"));
  const paths = Array.from({ length: 20 }, (_, index) =>
    index === 1
      ? "tests/1.txt"
      : `src/${index === 19 ? "a-long-file-name-that-should-truncate-in-a-narrow-conversation" : index + 1}.txt`,
  );
  await Promise.all(paths.map((path, index) => writeFile(join(project, path), `original ${index}\n`)));
  const release = deferred();
  const server = await fakeServer((body, response) => {
    const messages = body.messages as unknown as { role: string; content: string }[];
    const results = messages.filter((message) => message.role === "tool");
    const user = messages.filter((message) => message.role === "user").at(-1)?.content;
    if (user === "修改二十个文件" && results.length < 21) {
      const edits =
        results.length === 0
          ? paths.map((path, index) => ({ path, oldText: `original ${index}`, newText: `updated ${index}` }))
          : [{ path: paths[0], oldText: "updated 0", newText: "updated again" }];
      send(
        response,
        chunk({
          tool_calls: edits.map(({ path, oldText, newText }, index) => ({
            index,
            id: `edit-${results.length + index}`,
            type: "function",
            function: { name: "edit", arguments: JSON.stringify({ path, edits: [{ oldText, newText }] }) },
          })),
        }),
      );
      done(response, "tool_calls");
    } else if (user === "修改二十个文件") {
      send(response, chunk({ content: "已修改 20 个文件,其中一个文件修改了两次。" }));
      return release.promise.then(() => done(response));
    } else {
      send(response, chunk({ content: "这一轮没有修改文件。" }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    let page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("修改二十个文件");
    await page.getByLabel("消息", { exact: true }).press("Enter");
    const card = page.locator(".changed-files");
    const summary = card.locator("summary");
    const list = card.locator(".changed-files-list");
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "running");
    await expect(page.getByTestId("run")).toContainText("已修改 20 个文件,其中一个文件修改了两次。");
    await expect(card).toHaveCount(0);
    release.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await expect(summary).toContainText("20 个文件");
    await expect(summary.locator(".changed-files-totals")).toHaveText("+21-21");
    await expect(card).not.toHaveAttribute("open");
    await expect(list).toBeHidden();
    await summary.click();
    await expect(list).toBeVisible();
    await expect(card.locator(".changed-file-card")).toHaveCount(20);
    await expect(card.locator(".changed-file-counts").first()).toHaveText("+2-2");
    await expect(card.locator(".changed-file-directory").first()).toHaveText("src/");
    await expect(card.locator(".changed-file-directory").nth(1)).toHaveText("tests/");
    const first = card.locator(".changed-file-row").first();
    const infoBox = await first.locator(".changed-file-info").boundingBox();
    const countsBox = await first.locator(".changed-file-counts").boundingBox();
    expect((countsBox?.x ?? 0) - ((infoBox?.x ?? 0) + (infoBox?.width ?? 0))).toBeLessThanOrEqual(8);
    await expect(card).toHaveAttribute("open", "");
    await summary.focus();
    await summary.press("Space");
    await expect(list).toBeHidden();
    await page.locator(".answer").click();
    await page.screenshot({ path: "test-results/changed-files-collapsed.png" });
    await summary.focus();
    await summary.press("Enter");
    await expect(list).toBeVisible();
    await expect(card.locator(".changed-files-chevron")).toHaveCSS("transform", "matrix(0, 1, -1, 0, 0, 0)");
    await page.locator(".answer").click();
    const bounds = await list.evaluate((element) => ({
      height: element.clientHeight,
      content: element.scrollHeight,
      width: element.clientWidth,
      contentWidth: element.scrollWidth,
    }));
    expect(bounds.height).toBeLessThanOrEqual(320);
    expect(bounds.content).toBeGreaterThan(bounds.height);
    expect(bounds.contentWidth).toBe(bounds.width);
    await page.screenshot({ path: "test-results/changed-files-expanded.png" });
    await page.emulateMedia({ colorScheme: "dark" });
    await page.screenshot({ path: "test-results/changed-files-dark.png" });
    await page.emulateMedia({ colorScheme: "light" });
    const rowBox = await first.boundingBox();
    expect(rowBox).not.toBeNull();
    for (const position of [
      { x: 2, y: 2 },
      { x: (rowBox?.width ?? 0) - 2, y: (rowBox?.height ?? 0) - 2 },
    ]) {
      await first.click({ position });
      await expect(
        page.getByRole("tabpanel", { name: "变更", exact: true }).locator("diffs-container"),
      ).toContainText("original 0");
      await expect(
        page.getByRole("tabpanel", { name: "变更", exact: true }).locator("diffs-container"),
      ).toContainText("updated again");
      await page.getByLabel("收起右侧栏", { exact: true }).click();
    }
    await first.getByRole("button", { name: "查看修改 1.txt", exact: true }).focus();
    await first.getByRole("button", { name: "查看修改 1.txt", exact: true }).press("Enter");
    await expect(
      page.getByRole("tabpanel", { name: "变更", exact: true }).locator("diffs-container"),
    ).toContainText("updated again");
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    await app.evaluate(({ shell }) => {
      const calls: { action: string; path: string }[] = [];
      (globalThis as typeof globalThis & { fileActionCalls: typeof calls }).fileActionCalls = calls;
      shell.openPath = async (path) => {
        calls.push({ action: "open", path });
        return "";
      };
      shell.showItemInFolder = (path) => {
        calls.push({ action: "reveal", path });
      };
    });
    const recordedActions = () =>
      app?.evaluate(() => (globalThis as typeof globalThis & { fileActionCalls: unknown[] }).fileActionCalls);
    const absolutePath = await realpath(join(project, paths[0]));
    await first.getByRole("button", { name: "打开 1.txt", exact: true }).click();
    await expect(page.locator(".file-text-preview")).toContainText("updated again");
    expect(await recordedActions()).toEqual([]);
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    const menu = first.getByRole("button", { name: "打开菜单 1.txt", exact: true });
    await menu.click();
    await expect(page.getByRole("menuitem")).toHaveText([
      "Finder",
      "使用默认程序打开",
      "复制绝对路径",
      "复制相对路径",
    ]);
    await expect
      .poll(() =>
        page
          .getByRole("menuitem", { name: "Finder", exact: true })
          .locator("img")
          .evaluate((img) => (img as HTMLImageElement).naturalWidth),
      )
      .toBeGreaterThan(0);
    await page.screenshot({ path: "test-results/changed-files-open-menu.png" });
    await page.getByRole("menuitem", { name: "Finder", exact: true }).click();
    await expect.poll(recordedActions).toEqual([{ action: "reveal", path: absolutePath }]);
    await menu.click();
    await expect(
      page.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).locator("svg"),
    ).toBeVisible();
    await page.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect.poll(recordedActions).toEqual([
      { action: "reveal", path: absolutePath },
      { action: "open", path: absolutePath },
    ]);
    await menu.click();
    await page.getByRole("menuitem", { name: "复制绝对路径", exact: true }).click();
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toBe(absolutePath);
    await menu.click();
    await page.getByRole("menuitem", { name: "复制相对路径", exact: true }).click();
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toBe(paths[0]);
    await expect(page.locator(".right-pane")).toBeHidden();
    await app.evaluate(({ shell }) => {
      shell.openPath = async () => "没有可用的默认应用程序";
    });
    await menu.click();
    await page.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect(card.getByRole("alert")).toContainText("没有可用的默认应用程序");
    await menu.click();
    await page.getByRole("menuitem", { name: "复制相对路径", exact: true }).click();
    await expect(card.getByRole("alert")).toHaveCount(0);
    await card
      .locator(".changed-file-row")
      .nth(1)
      .getByRole("button", { name: "审查 1.txt", exact: true })
      .click();
    await expect(
      page.getByRole("tabpanel", { name: "变更", exact: true }).locator("diffs-container"),
    ).toContainText("original 1");
    await expect(
      page.getByRole("tabpanel", { name: "变更", exact: true }).locator("diffs-container"),
    ).toContainText("updated 1");
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.setSize(840, 720));
    await expect.poll(() => card.evaluate((element) => element.scrollWidth - element.clientWidth)).toBe(0);
    await card.locator(".changed-file-card").last().scrollIntoViewIfNeeded();
    await expect(card.locator(".changed-file-card").last()).toBeInViewport();
    await page.screenshot({ path: "test-results/changed-files-narrow.png" });
    await page.getByLabel("消息", { exact: true }).fill("不修改文件");
    await page.getByLabel("消息", { exact: true }).press("Enter");
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    await expect(page.getByTestId("run").last().locator(".changed-files")).toHaveCount(0);
    await app.close();
    app = await launchDesktop({ dir, project, url: server.url });
    page = await app.firstWindow();
    await expect(page.getByTestId("run")).toHaveCount(2);
    await expect(page.locator(".changed-files-list")).toBeHidden();
    await expect(page.locator(".changed-files-totals")).toHaveText("+21-21");
  } finally {
    release.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

test("changed HTML opens in the same built-in browser as conversation links, with explicit default-app opening", async () => {
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-changed-html-")));
  const project = join(dir, "project");
  await mkdir(project);
  const name = "页面 #1.html";
  const path = join(project, name);
  const server = await fakeServer((body, response) => {
    const messages = body.messages as unknown as { role: string }[];
    if (!messages.some((message) => message.role === "tool")) {
      send(
        response,
        chunk({
          tool_calls: [
            {
              index: 0,
              id: "write-html",
              type: "function",
              function: {
                name: "write",
                arguments: JSON.stringify({ path: name, content: "<h1>Built-in preview</h1>" }),
              },
            },
          ],
        }),
      );
      done(response, "tool_calls");
    } else {
      send(response, chunk({ content: `[页面](<${pathToFileURL(path).href}>)` }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await app.evaluate(({ shell }) => {
      Object.assign(globalThis, { htmlOpenCalls: [] as string[] });
      shell.openPath = async (path) => {
        (globalThis as typeof globalThis & { htmlOpenCalls: string[] }).htmlOpenCalls.push(path);
        return "";
      };
    });
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("create page");
    await page.getByLabel("发送", { exact: true }).click();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await page.locator(".changed-files summary").click();
    await page.getByRole("button", { name: `打开 ${name}`, exact: true }).click();
    await expect(page.getByRole("textbox", { name: "浏览器地址", exact: true })).toHaveValue(
      pathToFileURL(path).href,
    );
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    await page.locator(".answer").getByRole("button", { name: "页面", exact: true }).click();
    await expect(page.getByRole("textbox", { name: "浏览器地址", exact: true })).toHaveValue(
      pathToFileURL(path).href,
    );
    expect(
      await app.evaluate(() => (globalThis as typeof globalThis & { htmlOpenCalls: string[] }).htmlOpenCalls),
    ).toEqual([]);
    await page.getByRole("button", { name: `打开菜单 ${name}`, exact: true }).click();
    await page.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect
      .poll(() =>
        app?.evaluate(() => (globalThis as typeof globalThis & { htmlOpenCalls: string[] }).htmlOpenCalls),
      )
      .toEqual([path]);
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});