Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/preview-sidebar.spec.ts

typescript374 行查看原文
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import { StateStore } from "../../packages/desktop/src/main/state-store.ts";
import { chunk, deferred, done, fakeServer, send } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { testStorageDirectory } from "../helpers/storage.ts";
import { expect, test } from "../helpers/test.ts";
import { seedHistory } from "../history-fixture.ts";

test("terminal cards use real turn changes, IPC file validation and native/browser/file opens", async () => {
  test.setTimeout(60000);
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-preview-cards-")));
  const project = join(dir, "中文 项目");
  await mkdir(project);
  await writeFile(join(project, "existing.md"), "# Existing\n");
  await writeFile(join(project, "slides.pptx"), "presentation fixture");
  const release = deferred();
  const server = await fakeServer((body, response) => {
    const messages = body.messages as unknown as { role: string; content: string }[];
    const user = messages.filter((m) => m.role === "user").at(-1)?.content;
    if (user === "生成预览" && !messages.some((m) => m.role === "tool")) {
      send(
        response,
        chunk({
          tool_calls: [
            {
              index: 0,
              id: "html",
              type: "function",
              function: {
                name: "write",
                arguments: JSON.stringify({ path: "页面 demo.html", content: "<h1>真实 HTML 预览</h1>" }),
              },
            },
            {
              index: 1,
              id: "md",
              type: "function",
              function: {
                name: "write",
                arguments: JSON.stringify({ path: "报告.md", content: "# 真实 Markdown 预览\n" }),
              },
            },
          ],
        }),
      );
      done(response, "tool_calls");
    } else if (user === "生成预览") {
      send(
        response,
        chunk({
          content:
            "已生成 [页面](<./页面 demo.html>) 和 [报告](报告.md)。另有 `slides.pptx`、`missing.pdf`。",
        }),
      );
      return release.promise.then(() => done(response));
    } else if (user === "网址") {
      send(
        response,
        chunk({
          content:
            "预览 [动态页面](http://localhost:43210/route?q=1)。普通网站 https://example.com 不生成卡片。",
        }),
      );
      done(response);
    } else {
      send(response, chunk({ content: "仅讨论 `existing.md` 和 `页面 demo.html`,没有修改。" }));
      done(response);
    }
  });
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({
      dir,
      project,
      url: server.url,
      packaged: Boolean(process.env.ZPI_TEST_PACKAGED_APP),
    });
    const page = await app.firstWindow();
    await page.emulateMedia({ reducedMotion: "no-preference" });
    await app.evaluate(({ BrowserWindow, shell }) => {
      BrowserWindow.getAllWindows()[0].setSize(1200, 800);
      const calls: string[] = [];
      (globalThis as typeof globalThis & { previewCalls: string[] }).previewCalls = calls;
      shell.openPath = async (path) => {
        calls.push(path);
        return "";
      };
      shell.showItemInFolder = (path) => {
        calls.push(`reveal:${path}`);
      };
      shell.openExternal = async (url) => {
        calls.push(url);
      };
    });
    const calls = () =>
      app?.evaluate(() => (globalThis as typeof globalThis & { previewCalls: string[] }).previewCalls);
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    const editor = page.getByLabel("消息", { exact: true });
    await editor.fill("生成预览");
    await editor.press("Enter");
    await expect(page.locator(".answer")).toContainText("已生成");
    await expect(page.locator(".assistant-preview-card")).toHaveCount(0);
    release.resolve();
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    const cards = page.locator(".assistant-preview-card");
    await expect(cards).toHaveCount(3);
    await expect(cards.locator(".assistant-preview-title")).toHaveText([
      "slides.pptx",
      "报告.md",
      "页面 demo.html",
    ]);
    await expect(cards.locator(".assistant-preview-subtitle")).toHaveText([
      "演示文稿 · PPTX",
      "文档 · MD",
      "网站 · HTML",
    ]);
    expect(
      await cards.evaluateAll((nodes) =>
        nodes.map((node) => {
          const style = getComputedStyle(node);
          return [style.animationDuration, style.animationDelay];
        }),
      ),
    ).toEqual([
      ["0.9s", "0s"],
      ["0.9s", "0.036s"],
      ["0.9s", "0.072s"],
    ]);
    await expect.poll(calls).toEqual([]);
    await expect
      .poll(() =>
        cards
          .locator("img")
          .first()
          .evaluate((image: HTMLImageElement) => image.naturalWidth),
      )
      .toBeGreaterThan(0);
    await page.emulateMedia({ colorScheme: "light", reducedMotion: "reduce" });
    await page.screenshot({ path: "test-results/previews-light.png" });
    await page.emulateMedia({ colorScheme: "dark" });
    await page.screenshot({ path: "test-results/previews-dark.png" });
    const md = cards.filter({ hasText: "文档 · MD" });
    await md.getByRole("button", { name: "打开", exact: true }).click();
    await expect(page.locator(".file-markdown-preview")).toContainText("真实 Markdown 预览");
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    const menu = page.getByRole("menu");
    const fileMenuItems = ["Finder", "使用默认程序打开", "复制绝对路径", "复制相对路径"];
    await page.locator(".changed-files-summary").click();
    await page.getByRole("button", { name: "打开菜单 报告.md", exact: true }).click();
    await expect(menu.getByRole("menuitem")).toHaveText(fileMenuItems);
    const changedMenuClass = await menu.getAttribute("class");
    await page.keyboard.press("Escape");
    await md.getByLabel("选择打开方式").click();
    await expect(menu.getByRole("menuitem")).toHaveText(fileMenuItems);
    await expect(menu).toHaveAttribute("class", changedMenuClass ?? "");
    await expect(menu.getByRole("separator")).toHaveCount(1);
    await expect
      .poll(() =>
        menu
          .getByRole("menuitem", { name: "Finder", exact: true })
          .locator("img")
          .evaluate((image: HTMLImageElement) => image.naturalWidth),
      )
      .toBeGreaterThan(0);
    await page.screenshot({ path: "test-results/preview-open-menu.png" });
    await page.getByRole("menuitem", { name: "复制相对路径" }).click();
    await expect(page.getByRole("menu")).toHaveCount(0);
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toBe("报告.md");
    await md.getByLabel("选择打开方式").click();
    await page.getByRole("menuitem", { name: "Finder", exact: true }).click();
    await expect(page.getByRole("menu")).toHaveCount(0);
    await expect.poll(calls).toContain(`reveal:${join(project, "报告.md")}`);
    await md.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "复制绝对路径", exact: true }).click();
    await expect
      .poll(() => app?.evaluate(({ clipboard }) => clipboard.readText()))
      .toBe(join(project, "报告.md"));
    await md.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect.poll(calls).toContain(join(project, "报告.md"));
    await expect(page.locator(".right-pane")).toBeHidden();
    await app.evaluate(({ shell }) => {
      shell.openPath = async (path) => {
        (globalThis as typeof globalThis & { previewCalls: string[] }).previewCalls.push(path);
        return path.endsWith("报告.md") ? "没有可用的默认应用程序" : "";
      };
    });
    await md.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect(md.getByRole("alert")).toContainText("没有可用的默认应用程序");
    await md.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "复制相对路径", exact: true }).click();
    await expect(md.getByRole("alert")).toHaveCount(0);
    const html = cards.filter({ hasText: "网站 · HTML" });
    await html.getByRole("button", { name: "打开", exact: true }).click();
    await expect(page.locator(".file-text-preview")).toContainText("真实 HTML 预览");
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    await html.getByLabel("选择打开方式").click();
    await expect(menu.getByRole("menuitem")).toHaveText(fileMenuItems);
    await expect(menu).toHaveAttribute("class", changedMenuClass ?? "");
    await expect(menu.getByRole("separator")).toHaveCount(1);
    await page.screenshot({ path: "test-results/html-preview-open-menu.png" });
    await menu.getByRole("menuitem", { name: "Finder", exact: true }).click();
    await expect(page.getByRole("menu")).toHaveCount(0);
    await expect.poll(calls).toContain(`reveal:${join(project, "页面 demo.html")}`);
    await html.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "复制绝对路径", exact: true }).click();
    await expect
      .poll(() => app?.evaluate(({ clipboard }) => clipboard.readText()))
      .toBe(join(project, "页面 demo.html"));
    await html.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "复制相对路径", exact: true }).click();
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toBe("页面 demo.html");
    await html.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect(page.getByRole("menu")).toHaveCount(0);
    await expect.poll(calls).toContain(join(project, "页面 demo.html"));
    await app.evaluate(({ shell }) => {
      shell.openPath = async () => "没有可用的默认应用程序";
    });
    await html.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "使用默认程序打开", exact: true }).click();
    await expect(html.getByRole("alert")).toContainText("没有可用的默认应用程序");
    await html.getByLabel("选择打开方式").click();
    await menu.getByRole("menuitem", { name: "复制相对路径", exact: true }).click();
    await expect(html.getByRole("alert")).toHaveCount(0);
    await editor.fill("讨论");
    await editor.press("Enter");
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    await expect(page.getByTestId("run").last().locator(".assistant-preview-card")).toHaveCount(0);
    await editor.fill("网址");
    await editor.press("Enter");
    const website = page.getByTestId("run").last().locator(".assistant-preview-card");
    await expect(website).toHaveCount(1);
    await website.getByRole("button", { name: "打开", exact: true }).click();
    await expect
      .poll(() =>
        app?.evaluate(({ webContents }) =>
          webContents.getAllWebContents().some((wc) => wc.getURL() === "http://localhost:43210/route?q=1"),
        ),
      )
      .toBe(true);
    await page.getByLabel("收起右侧栏", { exact: true }).click();
    await website.getByLabel("选择打开方式").click();
    await page.getByRole("menuitem", { name: "在浏览器中打开" }).click();
    await expect(page.getByRole("menu")).toHaveCount(0);
    await expect.poll(calls).toContain("http://localhost:43210/route?q=1");
    await page.reload();
    await expect(page.locator(".assistant-preview-card")).toHaveCount(4);
    expect((await calls())?.filter((path) => path.endsWith("slides.pptx"))).toHaveLength(0);
  } finally {
    release.resolve();
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

test("independent project/task/timeline pages, menu keyboard navigation, preferences and real task selection", async () => {
  test.setTimeout(60000);
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-sidebar-pages-")));
  const now = new Date("2026-10-08T12:00:00+08:00").getTime();
  const projects = ["alpha", "beta"].map((id) => ({ id, name: id, path: join(dir, id), updatedAt: now }));
  await Promise.all(projects.map((p) => mkdir(p.path)));
  const storageDirectory = testStorageDirectory(dir);
  const projectState = new StateStore(join(storageDirectory, "state.sqlite"));
  projectState.setProjects(projects);
  projectState.close();
  for (const [scope, count] of [
    ["alpha", 11],
    ["beta", 6],
    [null, 41],
  ] as const) {
    for (let i = 0; i < count; i++) {
      const seeded = seedHistory(dir, projects.find((p) => p.id === scope)?.path ?? dir, 1, {
        projectId: scope,
        createdAt: now - (count - i) * 86400000,
        updatedAt: now - i * 86400000,
      });
      const taskState = new StateStore(join(storageDirectory, "state.sqlite"));
      try {
        const task = taskState.getTask(seeded.id);
        if (!task) throw new Error("Missing seeded task");
        taskState.upsertTask({
          ...task,
          title: `${scope ?? "任务"} ${String(i).padStart(2, "0")} 完整标题用于窄侧栏渐隐显示`,
        });
      } finally {
        taskState.close();
      }
    }
  }
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, url: "", packaged: Boolean(process.env.ZPI_TEST_PACKAGED_APP) });
    let page = await app.firstWindow();
    await page.clock.setFixedTime(now);
    await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1200, 800));
    const alpha = page.locator('[data-project-id="alpha"]');
    const beta = page.locator('[data-project-id="beta"]');
    await expect(alpha.getByTestId("session-row")).toHaveCount(5);
    await expect(beta.getByTestId("session-row")).toHaveCount(5);
    await alpha.getByText("显示更多", { exact: true }).click();
    await expect(alpha.getByTestId("session-row")).toHaveCount(10);
    await expect(beta.getByTestId("session-row")).toHaveCount(5);
    await alpha.getByText("显示更多", { exact: true }).click();
    await expect(alpha.getByTestId("session-row")).toHaveCount(11);
    await expect(alpha.getByText("显示更多", { exact: true })).toHaveCount(0);
    await beta.getByText("显示更多", { exact: true }).click();
    await expect(beta.getByTestId("session-row")).toHaveCount(6);
    await page.getByLabel("收起项目 alpha", { exact: true }).click();
    await page.getByLabel("展开项目 alpha", { exact: true }).click();
    await expect(alpha.getByTestId("session-row")).toHaveCount(5);
    const tasks = page.locator(".recent-sessions");
    await expect(tasks.getByTestId("session-row")).toHaveCount(5);
    await expect(tasks.locator(".timeline-group-label")).toHaveCount(0);
    for (const count of [10, 15, 20, 25, 30, 35, 40, 41]) {
      await tasks.getByText("显示更多", { exact: true }).click();
      await expect(tasks.getByTestId("session-row")).toHaveCount(count);
    }
    await expect(tasks.getByText("显示更多", { exact: true })).toHaveCount(0);
    const filter = page.getByLabel("筛选视图", { exact: true });
    await filter.click();
    await expect(page.getByRole("menuitemradio", { name: "按项目" })).toBeChecked();
    await expect(page.getByRole("menuitemradio")).toHaveCount(3);
    await page.getByRole("menuitemradio", { name: "最近更新" }).click();
    const timeline = page.locator(".timeline-tasks");
    await expect(timeline.getByTestId("session-row")).toHaveCount(20);
    await expect(timeline.locator(".timeline-group-label").first()).toHaveText("今天");
    await timeline.getByText("显示更多", { exact: true }).click();
    await expect(timeline.getByTestId("session-row")).toHaveCount(40);
    await timeline.getByText("显示更多", { exact: true }).click();
    await expect(timeline.getByTestId("session-row")).toHaveCount(58);
    await expect(timeline.getByText("显示更多", { exact: true })).toHaveCount(0);
    await page.reload();
    await expect(timeline.getByTestId("session-row")).toHaveCount(20);
    await filter.click();
    await expect(page.getByRole("menuitemradio", { name: "最近更新" })).toHaveAttribute(
      "aria-checked",
      "true",
    );
    await expect(page.getByRole("menuitemradio")).toHaveCount(3);
    await page.screenshot({ path: "test-results/sidebar-sort-menu.png" });
    await page.keyboard.press("Escape");
    await expect(filter).toBeFocused();
    await timeline.locator(".session-name").first().click();
    await expect(page.getByTestId("run")).toHaveCount(1);
    await page.getByLabel("消息", { exact: true }).focus();
    await page.mouse.move(1100, 700);
    await expect(timeline.locator(".task-row-time").first()).toBeVisible();
    await page.emulateMedia({ colorScheme: "light" });
    await page.screenshot({ path: "test-results/sidebar-timeline-light.png" });
    await page.emulateMedia({ colorScheme: "dark" });
    await page.screenshot({ path: "test-results/sidebar-timeline-dark.png" });
    await app.close();
    app = await launchDesktop({ dir, url: "", packaged: Boolean(process.env.ZPI_TEST_PACKAGED_APP) });
    page = await app.firstWindow();
    await page.getByLabel("筛选视图", { exact: true }).click();
    await expect(page.getByRole("menuitemradio", { name: "最近更新" })).toHaveAttribute(
      "aria-checked",
      "true",
    );
    await expect(page.getByRole("menuitemradio")).toHaveCount(3);
  } finally {
    await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
  }
});