Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/review-shell.spec.ts

typescript329 行查看原文
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import sharp from "sharp";
import { runGit } from "../../packages/desktop/src/main/workspace-files.ts";
import { chunk, 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("review tabs support overview, closure restoration and complete-view conversation without losing drafts or diff state", async ({
  desktopIsolation,
}, testInfo) => {
  void desktopIsolation;
  const dir = await mkdtemp(join(tmpdir(), "zpi-review-shell-")),
    project = join(dir, "project");
  await mkdir(project);
  await runGit(project, ["init", "-b", "main"]);
  await runGit(project, ["config", "user.name", "Review Test"]);
  await runGit(project, ["config", "user.email", "review@example.invalid"]);
  const file = join(project, "main.ts");
  const original = Array.from({ length: 120 }, (_, index) => `export const value${index} = ${index};`).join(
    "\n",
  );
  await writeFile(file, original);
  await runGit(project, ["add", "."]);
  await runGit(project, ["commit", "-m", "Initial"]);
  await writeFile(file, original.replace("value19 = 19", "value19 = 190"));
  const server = await fakeServer((_, response) => {
    send(response, chunk({ content: "Completed review shell reply." }));
    done(response);
  });
  let app: Awaited<ReturnType<typeof launchDesktop>> | undefined;
  try {
    app = await launchDesktop({ dir, project, url: server.url });
    const page = await app.firstWindow();
    await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1512, 893));
    const errors: string[] = [];
    page.on("pageerror", (error) => errors.push(error.message));
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    const composer = page.getByLabel("消息", { exact: true });
    await composer.fill("review shell");
    await composer.press("Enter");
    await expect(page.getByTestId("run")).toHaveAttribute("data-status", "completed");
    await page.getByRole("button", { name: "展开右侧栏", exact: true }).click();
    await page.locator('[data-side-pane-open-tab-item="review"]:visible').click();
    const pane = page.locator(".review-pane:visible");
    await pane.getByRole("button", { name: /main.ts/ }).click();
    await pane.locator("[data-separator-first] [data-expand-button]").first().click();
    await expect(pane.locator("diffs-container")).toContainText("value0 = 0");
    await pane.locator("[data-separator-last] [data-expand-button]").first().click();
    await expect(pane.locator("diffs-container")).toContainText("value119 = 119");
    await composer.fill("retain this draft");
    const initial = await page.locator(".right-pane").boundingBox();
    const sidebar = await page.locator(".sidebar").boundingBox();
    await pane.locator(".review-change-list").evaluate((element) => (element.scrollTop = 150));
    expect(await pane.locator(".review-change-list").evaluate((element) => element.scrollTop)).toBe(150);
    await page.getByRole("button", { name: "进入完整视图", exact: true }).click();
    await expect(page.locator(".shell")).toHaveClass(/pane-full-floating/);
    await expect(page.locator(".sidebar")).toBeVisible();
    expect(await page.locator(".sidebar").boundingBox()).toEqual(sidebar);
    expect((await page.locator(".right-pane").boundingBox())?.x).toBe((sidebar?.width ?? 0) + 4);
    expect((await page.locator(".right-pane").boundingBox())?.width).toBeGreaterThan(initial?.width ?? 0);
    const input = page.locator(".main .composer");
    await expect(input).toHaveCSS("height", "46px");
    await expect(input.getByRole("button")).toHaveCount(2);
    await expect(input.getByRole("button", { name: "发送", exact: true })).toBeVisible();
    const plus = input.getByRole("button", { name: "添加上下文", exact: true });
    const plusBox = await plus.boundingBox();
    const editorBox = await composer.boundingBox();
    const sendBox = await input.getByRole("button", { name: "发送", exact: true }).boundingBox();
    expect(plusBox?.y).toBe(editorBox?.y);
    expect(sendBox?.y).toBe(editorBox?.y);
    expect(editorBox?.x).toBe((plusBox?.x ?? 0) + 36);
    expect(sendBox?.x).toBe((editorBox?.x ?? 0) + (editorBox?.width ?? 0) + 8);
    await plus.hover();
    await expect(page.getByRole("tooltip")).toHaveText("添加上下文");
    await expect(page.locator(".main")).toHaveCSS("height", "46px");
    await expect(page.getByRole("button", { name: "展开消息", exact: true })).toBeHidden();
    await input.getByRole("button", { name: "发送", exact: true }).hover();
    await expect(page.getByRole("tooltip")).toBeHidden();
    await expect(page.locator(".main")).toHaveCSS("height", "46px");
    await plus.click();
    const addPanel = page.getByRole("listbox", { name: "添加上下文", exact: true });
    await expect(addPanel).toBeVisible();
    await addPanel.press("Escape");
    await expect(composer).toHaveText("retain this draft");
    await expect(pane.locator(".review-file-row")).toHaveAttribute("aria-expanded", "true");
    await expect(pane.locator("diffs-container")).toContainText("value0 = 0");
    await expect
      .poll(() => pane.locator(".review-change-list").evaluate((element) => element.scrollTop))
      .toBe(150);
    await composer.focus();
    await expect(page.locator(".main")).toHaveCSS("height", "86px");
    await page.mouse.move(700, 100);
    await plus.hover();
    await expect(page.getByRole("tooltip")).toHaveText("添加上下文");
    const tooltip = await page.locator(".control-hint-tooltip:visible").boundingBox();
    if (!tooltip) throw new Error("Context hint is missing");
    await page.mouse.move(tooltip.x + tooltip.width / 2, tooltip.y + tooltip.height / 2);
    await expect(page.locator(".main")).toHaveCSS("height", "86px");
    await expect(page.getByRole("button", { name: "最小化编辑器", exact: true })).toBeVisible();
    await input.getByRole("button", { name: "发送", exact: true }).hover();
    await expect(page.getByRole("tooltip")).toBeHidden();
    await expect(page.locator(".main")).toHaveCSS("height", "86px");
    await page.mouse.move(700, 100);
    await page.screenshot({ path: testInfo.outputPath("full-view-focused-light.png") });
    await page.emulateMedia({ colorScheme: "dark" });
    await expect(input).toHaveCSS("background-color", "rgb(43, 43, 43)");
    await page.screenshot({ path: testInfo.outputPath("full-view-focused-dark.png") });
    await page.emulateMedia({ colorScheme: "light" });
    await expect(input).toHaveCSS("background-color", "rgb(255, 255, 255)");
    await composer.fill("first line\nsecond line\nthird line");
    await expect(input).toHaveCSS("height", "86px");
    await composer.fill("retain this draft");
    await pane.getByRole("combobox", { name: "审查来源", exact: true }).focus();
    await page.mouse.move(700, 100);
    await expect(page.locator(".main")).toHaveCSS("height", "46px");
    await page.screenshot({ path: testInfo.outputPath("full-view-idle.png") });
    await composer.hover();
    await expect(page.locator(".main")).toHaveCSS("height", "46px");
    await composer.focus();
    await page.getByRole("button", { name: "展开消息", exact: true }).click();
    await expect(page.locator(".main")).toHaveCSS("height", "640px");
    await page.getByRole("button", { name: "最小化编辑器", exact: true }).click();
    await expect(page.getByRole("button", { name: "展开编辑器", exact: true })).toBeVisible();
    await expect(composer).toBeHidden();
    await page.getByRole("button", { name: "展开编辑器", exact: true }).click();
    await expect(composer).toBeVisible();
    await expect(composer).toHaveText("retain this draft");
    const grip = page.getByRole("button", { name: "拖动以移动 · 点击查看选项", exact: true });
    await grip.click();
    await page.getByRole("menuitem", { name: "最小化编辑器", exact: true }).click();
    await page.getByRole("button", { name: "展开编辑器", exact: true }).click();
    await grip.hover();
    await expect(page.locator(".pane-full-view-controls")).toHaveCSS("height", "40px");
    const handle = await grip.boundingBox();
    if (!handle) throw new Error("Floating editor handle is missing");
    await page.mouse.move(handle.x + handle.width / 2, handle.y + handle.height / 2);
    await page.mouse.down();
    await page.mouse.move(100, 100, { steps: 5 });
    await page.mouse.up();
    await expect(page.locator(".pane-full-view-controls")).toHaveAttribute("data-anchor", "top-left");
    await expect(composer).toHaveText("retain this draft");
    const floatingLeft = () =>
      page.locator(".main").evaluate((element) => element.getBoundingClientRect().left);
    await expect.poll(floatingLeft).toBeCloseTo((sidebar?.width ?? 0) + 21, 1);
    await page.getByTestId("left-sidebar-toggle").click();
    await expect(page.locator(".sidebar")).toBeHidden();
    await expect.poll(floatingLeft).toBeCloseTo(21, 1);
    await page.getByTestId("left-sidebar-toggle").click();
    await expect(page.locator(".sidebar")).toBeVisible();
    await expect.poll(floatingLeft).toBeCloseTo((sidebar?.width ?? 0) + 21, 1);
    await page.getByRole("separator", { name: "侧边栏宽度", exact: true }).press("ArrowRight");
    await expect.poll(floatingLeft).toBeCloseTo((sidebar?.width ?? 0) + 37, 1);
    await page.getByRole("separator", { name: "侧边栏宽度", exact: true }).press("ArrowLeft");
    await expect.poll(floatingLeft).toBeCloseTo((sidebar?.width ?? 0) + 21, 1);
    await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(800, 700));
    await expect
      .poll(async () => {
        const frame = await page.locator(".main").boundingBox();
        const slot = await page.locator(".right-pane-content").boundingBox();
        return frame && slot && frame.x >= slot.x && frame.x + frame.width <= slot.x + slot.width - 16;
      })
      .toBe(true);
    await expect(input).toHaveCSS("height", "46px");
    await page.screenshot({ path: testInfo.outputPath("full-view-narrow.png") });
    await app.evaluate(({ BrowserWindow }) => BrowserWindow.getAllWindows()[0].setSize(1512, 893));
    await page.getByRole("tab", { name: "review shell", exact: true }).click();
    await expect(page.locator(".shell")).toHaveClass(/pane-full-conversation/);
    await expect(page.locator(".sidebar")).toBeVisible();
    await expect.poll(floatingLeft).toBeCloseTo((sidebar?.width ?? 0) + 5, 1);
    await page.getByTestId("left-sidebar-toggle").click();
    await expect.poll(floatingLeft).toBeCloseTo(5, 1);
    await expect(page.getByRole("tab", { name: "review shell", exact: true })).toBeVisible();
    await page.getByTestId("left-sidebar-toggle").click();
    await expect.poll(floatingLeft).toBeCloseTo((sidebar?.width ?? 0) + 5, 1);
    await expect(page.getByTestId("run")).toBeVisible();
    await expect(page.getByRole("tab", { name: "审查", exact: true })).toHaveAttribute(
      "aria-selected",
      "false",
    );
    await page.getByRole("tab", { name: "审查", exact: true }).click();
    await expect(page.locator(".shell")).toHaveClass(/pane-full-floating/);
    await expect(composer).toHaveText("retain this draft");
    await page.getByRole("button", { name: "退出完整视图", exact: true }).click();
    await expect(page.locator(".shell")).not.toHaveClass(/pane-full-view/);
    expect((await page.locator(".right-pane").boundingBox())?.width).toBe(initial?.width);
    await page.getByRole("button", { name: "新增侧栏标签", exact: true }).click();
    await page.locator('[data-side-pane-open-tab-item="files"]:visible').click();
    await page.getByRole("button", { name: "搜索标签页", exact: true }).click();
    await page.getByPlaceholder("搜索标签页...", { exact: true }).fill("审查");
    await page.getByRole("option", { name: /审查/ }).click();
    await page.getByRole("tab", { name: "审查", exact: true }).click({ button: "right" });
    await page.getByRole("menuitem", { name: "关闭其他标签", exact: true }).click();
    await expect(page.getByRole("tablist", { name: "侧栏标签" }).getByRole("tab")).toHaveCount(1);
    await pane.getByRole("combobox", { name: "审查来源", exact: true }).click();
    await page.getByRole("option", { name: "已暂存", exact: true }).click();
    await page.getByRole("tab", { name: "审查", exact: true }).click({ button: "right" });
    await page.getByRole("menuitem", { name: "关闭标签", exact: true }).click();
    await page.getByRole("button", { name: "展开右侧栏", exact: true }).click();
    await page.getByRole("button", { name: "搜索标签页", exact: true }).click();
    await expect(page.getByText("最近关闭的标签页", { exact: true })).toBeVisible();
    await page.getByRole("option", { name: /审查/ }).click();
    await expect(pane.getByRole("combobox", { name: "审查来源", exact: true })).toHaveText("已暂存");
    await expect(page.locator(".pane-tab-overview")).toBeHidden();
    await composer.focus();
    await composer.press(`${process.platform === "darwin" ? "Meta" : "Control"}+Shift+f`);
    await expect(page.locator(".shell")).toHaveClass(/pane-full-view/);
    await composer.press(`${process.platform === "darwin" ? "Meta" : "Control"}+Shift+f`);
    await expect(page.locator(".shell")).not.toHaveClass(/pane-full-view/);
    await page.getByRole("button", { name: "进入完整视图", exact: true }).click();
    await expect(page.locator(".sidebar")).toBeVisible();
    await page.getByTestId("left-sidebar-toggle").click();
    await expect(page.locator(".sidebar")).toBeHidden();
    expect((await page.locator(".right-pane").boundingBox())?.x).toBe(4);
    await expect(page.getByTestId("model-performance")).toBeHidden();
    await composer.fill("another answer in full view");
    await composer.press("Enter");
    await expect(page.locator(".pane-floating-answer")).toContainText("Completed review shell reply.");
    await expect(page.locator(".pane-floating-answer")).toBeVisible();
    await page.screenshot({ path: testInfo.outputPath("review-full-view.png") });
    await page.getByRole("button", { name: "关闭回答预览", exact: true }).click();
    await expect(page.locator(".pane-floating-answer")).toBeHidden();
    await page.getByRole("button", { name: "退出完整视图", exact: true }).click();
    await expect(page.locator(".sidebar")).toBeHidden();
    await page.getByRole("button", { name: "进入完整视图", exact: true }).click();
    await expect(page.locator(".sidebar")).toBeHidden();
    expect(errors).toEqual([]);
  } finally {
    await runCleanup([
      () => app?.close(),
      () => server.close(),
      () => rm(dir, { recursive: true, force: true }),
    ]);
  }
});

test("full view keeps an unsent draft docked through expansion and attachments above its inline input", async ({
  desktopIsolation,
}, testInfo) => {
  void desktopIsolation;
  const dir = await mkdtemp(join(tmpdir(), "zpi-full-view-draft-"));
  const project = join(dir, "project");
  await mkdir(project);
  const image = join(dir, "sample.png");
  await sharp({ create: { width: 16, height: 16, channels: 3, background: "blue" } })
    .png()
    .toFile(image);
  const server = await fakeServer((_, response) => done(response));
  let app: Awaited<ReturnType<typeof launchDesktop>> | undefined;
  try {
    app = await launchDesktop({ dir, project, images: [image], url: server.url });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).click();
    const editor = page.getByLabel("消息", { exact: true });
    const input = page.locator(".main .composer");
    await editor.fill("unsent draft");
    const originalEditor = await editor.elementHandle();
    await page.getByTestId("right-sidebar-toggle").click();
    await page.getByRole("button", { name: "进入完整视图", exact: true }).click();
    await expect(page.locator(".draft-composer-header")).toBeHidden();
    await expect(input).toHaveCSS("height", "46px");
    const expectBottomDock = async () => {
      await expect
        .poll(() =>
          input.evaluate((element) => {
            const body = element.closest(".task-body");
            if (!body) throw Error("Missing floating conversation body");
            return Math.abs(body.getBoundingClientRect().bottom - element.getBoundingClientRect().bottom);
          }),
        )
        .toBeLessThanOrEqual(1);
    };
    for (const size of [
      [1200, 800],
      [900, 640],
    ]) {
      await app.evaluate(
        ({ BrowserWindow }, size) => BrowserWindow.getAllWindows()[0].setSize(size[0], size[1]),
        size,
      );
      await editor.focus();
      await page.getByRole("button", { name: "展开消息", exact: true }).click();
      await expectBottomDock();
      await expect(page.locator(".main .draft-greeting")).toBeInViewport();
      await expect(editor).toHaveText("unsent draft");
      await page.screenshot({ path: testInfo.outputPath(`full-view-empty-expanded-${size[0]}.png`) });
      await page.getByRole("button", { name: "收起消息", exact: true }).click();
      await expect(input).toHaveCSS("height", "46px");
      await expectBottomDock();
    }
    await input.getByRole("button", { name: "添加上下文", exact: true }).click();
    await page.getByRole("option", { name: "附件", exact: true }).click();
    const attachment = input.locator(".image-chip");
    await expect(attachment).toBeVisible();
    await expect(input).toHaveCSS("height", "102px");
    // Read both rectangles in one frame: the floating host reanchors as attachments change its height.
    const attachmentGap = await input.evaluate((element) => {
      const attachment = element.querySelector(".image-chip");
      const editor = element.querySelector(".mention-editor");
      if (!attachment || !editor) throw Error("Missing attachment or editor");
      return editor.getBoundingClientRect().top - attachment.getBoundingClientRect().bottom;
    });
    expect(attachmentGap).toBeGreaterThan(0);
    await editor.focus();
    await page.getByRole("button", { name: "展开消息", exact: true }).click();
    await expectBottomDock();
    await page.getByRole("button", { name: "收起消息", exact: true }).click();
    await page.screenshot({ path: testInfo.outputPath("full-view-draft-attachment.png") });
    await page.getByRole("button", { name: "最小化编辑器", exact: true }).click();
    await page.getByRole("button", { name: "展开编辑器", exact: true }).click();
    await expect(attachment).toBeVisible();
    await page.getByRole("button", { name: "移除 sample.png", exact: true }).click();
    await expect(input).toHaveCSS("height", "46px");
    await page.getByRole("button", { name: "退出完整视图", exact: true }).click();
    await expect(page.locator(".draft-composer-header")).toBeVisible();
    await expect(editor).toHaveText("unsent draft");
    expect(await editor.evaluate((element, original) => element === original, originalEditor)).toBe(true);
    expect(server.requests).toHaveLength(0);
  } finally {
    await runCleanup([
      () => app?.close(),
      () => server.close(),
      () => rm(dir, { recursive: true, force: true }),
    ]);
  }
});