Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/review-regressions.spec.ts

typescript193 行查看原文
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { ElectronApplication } from "@playwright/test";
import sharp from "sharp";
import { done, fakeServer } from "../fake-server.ts";
import { runCleanup } from "../helpers/cleanup.ts";
import { select } from "../helpers/composer.ts";
import { launchDesktop } from "../helpers/desktop.ts";
import { readStoredDraft } from "../helpers/storage.ts";
import { expect, test } from "../helpers/test.ts";
import { seedHistory } from "../history-fixture.ts";

test("draft save failure cancels quitting and allows a successful retry", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-quit-draft-failure-"));
  let app: Awaited<ReturnType<typeof launchDesktop>>;
  try {
    app = await launchDesktop({ dir, url: "" });
    const page = await app.firstWindow();
    await app.evaluate(({ ipcMain }) => {
      const internal = ipcMain as unknown as {
        _invokeHandlers: Map<string, (event: unknown, method: string, args: unknown[]) => Promise<unknown>>;
      };
      const original = internal._invokeHandlers.get("zpi:call");
      if (!original) throw Error("handler");
      ipcMain.removeHandler("zpi:call");
      ipcMain.handle("zpi:call", async (event, method, args) =>
        method === "saveDraft"
          ? { ok: false, error: { code: "storage", message: "disk full" } }
          : original(event, method, args),
      );
      (globalThis as unknown as { restoreDraftHandler: () => void }).restoreDraftHandler = () => {
        ipcMain.removeHandler("zpi:call");
        ipcMain.handle("zpi:call", original);
      };
    });
    await page.getByLabel("消息", { exact: true }).fill("保存失败后仍然保留的草稿");
    const id = await page.evaluate(() => localStorage.getItem("zpi.selectedSession"));
    await app.evaluate(({ app }) => app.quit());
    await expect
      .poll(() => readFile(join(dir, "agent", "logs", "error.log"), "utf8"))
      .toContain("drafts.quit");
    expect(app.process().exitCode).toBeNull();
    expect(await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0]?.isEnabled())).toBe(
      true,
    );
    await expect(page.getByLabel("消息", { exact: true })).toHaveText("保存失败后仍然保留的草稿");
    await app.evaluate(() =>
      (globalThis as unknown as { restoreDraftHandler: () => void }).restoreDraftHandler(),
    );
    await app.close();
    const draft = readStoredDraft(dir, id);
    expect(draft?.text).toBe("保存失败后仍然保留的草稿");
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => app?.close(),
      () =>
        app
          ?.evaluate(() =>
            (globalThis as unknown as { restoreDraftHandler?: () => void }).restoreDraftHandler?.(),
          )
          .catch(() => {}),
    ]);
  }
});

test("leaving the models tab during ChatGPT login restores settings controls", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-login-unmount-"));
  let app: Awaited<ReturnType<typeof launchDesktop>>;
  try {
    app = await launchDesktop({ dir, url: "" });
    await app.evaluate(({ shell }) => {
      shell.openExternal = async () => {};
    });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "设置", exact: true }).click();
    await page.getByRole("button", { name: "模型", exact: true }).click();
    await page
      .locator(".provider-template-grid")
      .getByRole("button", { name: "OpenAI(ChatGPT)", exact: true })
      .click();
    await page.getByRole("button", { name: "Continue with ChatGPT", exact: true }).click();
    await expect(page.getByRole("button", { name: "取消登录", exact: true })).toBeVisible();
    await page.getByRole("button", { name: "常规", exact: true }).click();
    await page.getByRole("button", { name: "模型", exact: true }).click();
    await expect(page.getByRole("button", { name: "新增提供商", exact: true })).toBeEnabled();
    await page.getByRole("button", { name: "新增提供商", exact: true }).click();
    await page.getByRole("button", { name: "自定义提供商", exact: true }).click();
    await page.getByRole("button", { name: "放弃更改并继续", exact: true }).click();
    await expect(page.getByLabel("提供商名称", { exact: true })).toBeEnabled();
    await page.getByLabel("提供商名称", { exact: true }).fill("After cancellation");
    await expect(page.getByLabel("提供商名称", { exact: true })).toHaveValue("After cancellation");
  } finally {
    await runCleanup([() => rm(dir, { recursive: true, force: true }), () => app?.close()]);
  }
});

test("reading position survives immediate session switching and closing before the bookmark timer fires", async () => {
  const dir = await mkdtemp(join(tmpdir(), "zpi-reading-flush-"));
  const cwd = join(dir, "workspace");
  await mkdir(cwd);
  const first = seedHistory(dir, cwd, 30);
  const second = seedHistory(dir, cwd, 1);
  const server = await fakeServer((_, response) => done(response));
  let app: ElectronApplication | undefined;
  try {
    app = await launchDesktop({ dir, url: server.url });
    let page = await app.firstWindow();
    await select(page, first.id);
    await expect(page.getByTestId("run")).toHaveCount(10);
    const scroll = await page.locator(".conversation").evaluate((element) => {
      element.scrollTop = element.scrollHeight / 2;
      element.dispatchEvent(new Event("scroll", { bubbles: true }));
      return element.scrollTop;
    });
    await select(page, second.id);
    await select(page, first.id);
    await expect
      .poll(() => page.locator(".conversation").evaluate((element) => element.scrollTop))
      .toBe(scroll);
    const last = await page.locator(".conversation").evaluate((element) => {
      element.scrollTop = element.scrollHeight / 2 + 50;
      element.dispatchEvent(new Event("scroll", { bubbles: true }));
      return element.scrollTop;
    });
    await app.close();
    app = await launchDesktop({ dir, url: server.url });
    page = await app.firstWindow();
    await expect
      .poll(() => page.locator(".conversation").evaluate((element) => element.scrollTop))
      .toBe(last);
  } finally {
    await runCleanup([
      () => rm(dir, { recursive: true, force: true }),
      () => server.close(),
      () => app?.close(),
    ]);
  }
});

for (const selection of [
  [1, 1],
  [0, 2],
] as const) {
  test(`mixed image and text paste keeps subsequent typing after the inserted text (${selection})`, async () => {
    const dir = await mkdtemp(join(tmpdir(), "zpi-mixed-paste-"));
    const server = await fakeServer((_, response) => done(response));
    let app: Awaited<ReturnType<typeof launchDesktop>>;
    try {
      app = await launchDesktop({ dir, url: server.url });
      const bytes = await sharp({ create: { width: 1, height: 1, channels: 3, background: "white" } })
        .png()
        .toBuffer();
      const page = await app.firstWindow();
      const editor = page.getByLabel("消息", { exact: true });
      await editor.fill("前尾");
      await editor.evaluate((element, positions) => {
        const range = document.createRange();
        range.setStart(element.firstChild as Node, positions[0]);
        range.setEnd(element.firstChild as Node, positions[1]);
        window.getSelection()?.removeAllRanges();
        window.getSelection()?.addRange(range);
      }, selection);
      await editor.evaluate((element, bytes) => {
        const data = new DataTransfer();
        data.setData("text/plain", "粘贴");
        data.items.add(new File([new Uint8Array(bytes)], "paste.png", { type: "image/png" }));
        element.dispatchEvent(
          new ClipboardEvent("paste", { clipboardData: data, bubbles: true, cancelable: true }),
        );
      }, Array.from(bytes));
      await editor.pressSequentially("X");
      const expected = selection[0] === 1 ? "前粘贴X尾" : "粘贴X";
      await expect(editor).toHaveText(expected);
      await expect(page.getByRole("button", { name: "预览 paste.png", exact: true })).toBeVisible();
      const id = await page.evaluate(() => localStorage.getItem("zpi.selectedSession"));
      await expect
        .poll(async () => {
          const draft = await page.evaluate((id) => window.zpi.getDraft(id as string), id);
          return draft.ok ? draft.value.text : draft.error.message;
        })
        .toBe(expected);
    } finally {
      await runCleanup([
        () => rm(dir, { recursive: true, force: true }),
        () => server.close(),
        () => app?.close(),
      ]);
    }
  });
}