Z ZPI 文档 v0.3.8
介绍页

源码

tests/desktop/file-preview-comments.spec.ts

typescript321 行查看原文
import { mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import type { Locator, Page } from "@playwright/test";
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";

// Hold the real annotation slot beyond the initial focus attempt. This
// exercises renderer timing without depending on machine speed or a timeout.
async function holdCommentSlot(preview: Locator, line: number) {
  const host = preview.locator("diffs-container");
  await host.evaluate((node, line) => {
    const shadow = node.shadowRoot;
    if (!shadow) throw new Error("Missing source shadow root");
    const observer = new MutationObserver(() => {
      const slot = shadow.querySelector<HTMLSlotElement>(`slot[name="annotation-${line}"]`);
      if (!slot) return;
      observer.disconnect();
      const marker = document.createComment("delayed annotation");
      slot.replaceWith(marker);
      node.setAttribute("data-test-annotation-pending", "");
      node.addEventListener("test-release-annotation", () => marker.replaceWith(slot), { once: true });
    });
    observer.observe(shadow, { childList: true, subtree: true });
  }, line);
  return async () => {
    await expect(host).toHaveAttribute("data-test-annotation-pending", "");
    await host.evaluate(async (node) => {
      await new Promise<void>((resolve) =>
        requestAnimationFrame(() => requestAnimationFrame(() => resolve())),
      );
      node.removeAttribute("data-test-annotation-pending");
      node.dispatchEvent(new Event("test-release-annotation"));
    });
  };
}

for (const delayed of [false, true]) {
  for (const submit of ["shortcut", "button"] as const) {
    test(`Markdown source comments focus the editor from the gutter plus and restore the main composer on ${submit} (${delayed ? "delayed" : "ready"} slot)`, async () => {
      const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-file-comment-focus-")));
      const project = join(dir, "project");
      await mkdir(project);
      const path = join(project, "hello.md");
      await writeFile(path, "# Hello\n\nSource comment target\n");
      const server = await fakeServer((_, response) => {
        send(response, chunk({ content: `[hello.md](<${path}>)` }));
        done(response);
      });
      let app: Awaited<ReturnType<typeof launchDesktop>> | undefined;
      try {
        app = await launchDesktop({ dir, url: server.url, project });
        const page = await app.firstWindow();
        // Emulate renderer focus while keeping the native window hidden and non-focusable.
        const cdp = await page.context().newCDPSession(page);
        await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true });
        const main = page.locator("main");
        const composer = main.getByLabel("消息", { exact: true });
        await page.getByLabel("添加项目", { exact: true }).first().click();
        await composer.fill("显示文件");
        await composer.press("Enter");
        await expect(main.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
        await composer.fill("保留主草稿");
        await main.locator(".answer").getByRole("button", { name: "hello.md", exact: true }).click();
        const manager = page.locator(".file-manager:visible");
        await manager.getByRole("button", { name: "文件预览菜单", exact: true }).click();
        await page.getByRole("menuitemradio", { name: "源码", exact: true }).click();
        const preview = manager.getByRole("region", { name: "文件预览", exact: true });
        const releaseSlot = delayed ? await holdCommentSlot(preview, 3) : undefined;
        await preview.locator('[data-column-number="3"]').first().hover();
        await preview.locator("[data-utility-button]").click({ delay: 150 });
        const comment = preview.getByLabel("代码备注", { exact: true });
        await releaseSlot?.();
        await page.keyboard.type("instant input");
        await expect(comment).toHaveValue("instant input");
        await expect(comment).toBeFocused();
        await expect(composer).toHaveText("保留主草稿");
        await composer.click();
        await preview.locator('[data-column-number="3"]').first().hover();
        await preview.locator("[data-utility-button]").click();
        await expect(comment).toBeFocused();
        await page.keyboard.type("应取消的备注");
        await page.keyboard.press("Escape");
        await expect(comment).toHaveCount(0);
        const releaseAbandonedSlot = delayed ? await holdCommentSlot(preview, 3) : undefined;
        await preview.locator('[data-column-number="3"]').first().hover();
        await preview.locator("[data-utility-button]").click();
        if (releaseAbandonedSlot) {
          await composer.click();
          await releaseAbandonedSlot();
          await expect(comment).toBeVisible();
          await expect(composer).toBeFocused();
          await preview.locator('[data-column-number="3"]').first().hover();
          await preview.locator("[data-utility-button]").click();
        }
        await expect(comment).toBeFocused();
        await expect(comment).toHaveValue("");
        await page.keyboard.type("从源码补充备注");
        await expect(comment).toHaveValue("从源码补充备注");
        if (submit === "shortcut")
          await page.keyboard.press(`${process.platform === "darwin" ? "Meta" : "Control"}+Enter`);
        else await preview.getByRole("button", { name: /添加到对话/ }).click();
        await expect(preview.locator("[data-file-comment-draft]")).toHaveCount(0);
        await expect(main.locator("[data-file-comment-count]")).toHaveAttribute(
          "data-file-comment-count",
          "1",
        );
        await expect(preview.locator("[data-file-comment-id]")).toContainText("从源码补充备注");
        await expect(composer).toBeFocused();
        await expect(composer).toHaveText("保留主草稿");
        await page.keyboard.type("继续补充");
        await expect(composer).toContainText("继续补充");
        await expect(composer).toContainText("保留主草稿");
        expect(server.requests).toHaveLength(1);
      } finally {
        await runCleanup([
          () => app?.close(),
          () => server.close(),
          () => rm(dir, { recursive: true, force: true }),
        ]);
      }
    });
  }
}

async function selectLines(page: Page, start: number, end = start) {
  const preview = page.getByRole("region", { name: "文件预览", exact: true });
  const first = preview.locator(`[data-column-number="${start}"]`).first();
  if (start === end) {
    await first.click();
  } else {
    const last = preview.locator(`[data-column-number="${end}"]`).first();
    const from = await first.boundingBox();
    const to = await last.boundingBox();
    if (!from || !to) throw new Error("Missing file gutter coordinates");
    await page.mouse.move(from.x + from.width / 2, from.y + from.height / 2);
    await page.mouse.down();
    await page.mouse.move(to.x + to.width / 2, to.y + to.height / 2, { steps: 5 });
    await page.mouse.up();
  }
  await expect(preview.locator("[data-file-comment-draft]")).toBeVisible();
  return preview;
}

for (const target of ["main", "side"] as const) {
  test(`file comments preserve ${target} draft ownership, cancellation, deletion and composer removal`, async () => {
    const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-file-comments-")));
    const project = join(dir, "project");
    await mkdir(project);
    const path = join(project, "example.ts");
    await writeFile(path, "const first = 1;\nconst second = 2;\nconst third = 3;\n");
    const requests: string[] = [];
    const server = await fakeServer((body, response) => {
      requests.push(JSON.stringify(body));
      send(response, chunk({ content: `[示例文件](<${path}>)` }));
      done(response);
    });
    let app: Awaited<ReturnType<typeof launchDesktop>> | undefined;
    try {
      app = await launchDesktop({ dir, url: server.url, project });
      const page = await app.firstWindow();
      const main = page.locator("main");
      await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
      await main.getByLabel("消息", { exact: true }).fill("显示文件");
      await main.getByLabel("发送", { exact: true }).click();
      await expect(main.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
      await main.getByLabel("消息", { exact: true }).fill("保留主草稿");
      if (target === "side") {
        await page.getByLabel("展开右侧栏", { exact: true }).click();
        await page
          .locator(".pane-empty-launcher")
          .getByRole("button", { name: "辅助对话", exact: true })
          .click();
        const side = page.getByRole("region", { name: "辅助对话", exact: true });
        await side.getByLabel("消息", { exact: true }).fill("显示辅助文件");
        await side.getByLabel("发送", { exact: true }).click();
        await expect(side.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
        await side.getByLabel("消息", { exact: true }).fill("保留辅助草稿");
      }
      const owner = target === "main" ? main : page.getByRole("region", { name: "辅助对话", exact: true });
      await owner.locator(".answer").last().getByRole("button", { name: "示例文件", exact: true }).click();
      const preview = await selectLines(page, 1);
      await preview.getByLabel("代码备注", { exact: true }).fill("应取消的备注");
      await preview.getByLabel("代码备注", { exact: true }).press("Escape");
      await expect(preview.locator("[data-file-comment-draft]")).toHaveCount(0);
      await expect(preview.locator("[data-file-comment-id]")).toHaveCount(0);
      await expect(preview.locator("[data-selected-line]")).toHaveCount(0);
      await selectLines(page, 2, 3);
      await preview.getByLabel("代码备注", { exact: true }).fill("检查多行上下文");
      await preview.getByRole("button", { name: /添加到对话/ }).click();
      await expect(preview.locator("[data-file-comment-id]")).toContainText("检查多行上下文");
      await expect(preview.locator("[data-file-comment-id]")).toContainText("评论第 2 行到第 3 行");
      await expect(preview.locator("[data-selected-line]")).toHaveCount(0);
      if (target === "side")
        await expect(main.locator(".composer-container .selection-reference-chip")).toHaveCount(0);
      await preview.getByRole("button", { name: "删除评论", exact: true }).click();
      await expect(preview.locator("[data-file-comment-id]")).toHaveCount(0);
      await expect(preview.locator("[data-selected-line]")).toHaveCount(0);
      await selectLines(page, 1);
      await preview.getByRole("button", { name: /添加到对话/ }).click();
      await expect(preview.locator("[data-file-comment-id]")).toHaveCount(1);
      if (target === "side")
        await page.getByRole("tab", { name: "辅助对话 1", exact: true }).locator(".pane-tab-trigger").click();
      await owner
        .locator(".composer-container")
        .getByRole("button", { name: "移除全部评论", exact: true })
        .click();
      await expect(owner.locator(".composer-container .selection-reference-chip")).toHaveCount(0);
      await page.getByRole("tab", { name: "example.ts", exact: true }).locator(".pane-tab-trigger").click();
      await expect(preview.locator("[data-file-comment-id]")).toHaveCount(0);
      await expect(preview.locator("[data-selected-line]")).toHaveCount(0);
      await selectLines(page, 2, 3);
      await preview.getByLabel("代码备注", { exact: true }).fill("最终提交备注");
      await preview.getByRole("button", { name: /添加到对话/ }).click();
      if (target === "side")
        await page.getByRole("tab", { name: "辅助对话 1", exact: true }).locator(".pane-tab-trigger").click();
      const commentPill = owner
        .locator(".composer-container")
        .getByRole("button", { name: "1 条评论", exact: true });
      await owner.getByLabel("消息", { exact: true }).focus();
      await commentPill.hover();
      await expect(owner.getByLabel("消息", { exact: true })).toBeFocused();
      const commentDetails = page.getByRole("dialog", { name: "评论内容", exact: true });
      await expect(commentDetails).toContainText("example.ts");
      await expect(commentDetails).toContainText("L2-L3");
      await expect(commentDetails).toContainText("最终提交备注");
      await expect(commentDetails).not.toContainText("const second = 2;");
      await expect(commentDetails.locator("small")).toHaveCSS("text-overflow", "ellipsis");
      await commentPill.focus();
      await commentPill.press("Enter");
      await expect(
        commentDetails.getByRole("button", { name: "打开 example.ts L2-L3", exact: true }),
      ).toBeFocused();
      await page.keyboard.press("Tab");
      await expect(commentDetails.getByRole("button", { name: "移除评论", exact: true })).toBeFocused();
      await page.keyboard.press("Escape");
      await expect(commentPill).toBeFocused();
      await expect(commentDetails).toHaveCount(0);
      // Escape closes the card while the pointer remains over the trigger.
      // Re-enter it to exercise the next hover, rather than hovering the same coordinates.
      await page.mouse.move(0, 0);
      await commentPill.hover();
      await commentDetails.getByRole("button", { name: "打开 example.ts L2-L3", exact: true }).click();
      await expect(preview).toBeVisible();
      if (target === "side")
        await page.getByRole("tab", { name: "辅助对话 1", exact: true }).locator(".pane-tab-trigger").click();
      await expect(owner.getByLabel("消息", { exact: true })).toHaveText(
        target === "main" ? "保留主草稿" : "保留辅助草稿",
      );
      await owner.getByLabel("发送", { exact: true }).click();
      await expect.poll(() => requests.at(-1)).toContain("最终提交备注");
      expect(requests.at(-1)).toContain("const second = 2;");
      expect(requests.at(-1)).toContain("const third = 3;");
      const sentComment = owner
        .getByTestId("run")
        .last()
        .getByRole("button", { name: "1 条评论", exact: true });
      await expect(sentComment).toBeVisible();
      await sentComment.hover();
      await expect(commentDetails).toContainText("最终提交备注");
      await expect(commentDetails.getByRole("button", { name: "移除评论", exact: true })).toHaveCount(0);
      if (target === "side") await expect(main.getByLabel("消息", { exact: true })).toHaveText("保留主草稿");
    } finally {
      await runCleanup([
        () => app?.close(),
        () => server.close(),
        () => rm(dir, { recursive: true, force: true }),
      ]);
    }
  });
}

test("text preview menu reflects wrap state, copies exact paths and dismisses on Escape", async () => {
  const dir = await realpath(await mkdtemp(join(tmpdir(), "zpi-file-preview-menu-")));
  const project = join(dir, "project");
  await mkdir(project);
  const path = join(project, "long.ts");
  await writeFile(path, `const message = "${"long line ".repeat(80)}";\n`);
  const server = await fakeServer((_, response) => {
    send(response, chunk({ content: `[长文件](<${path}>)` }));
    done(response);
  });
  let app: Awaited<ReturnType<typeof launchDesktop>> | undefined;
  try {
    app = await launchDesktop({ dir, url: server.url, project });
    const page = await app.firstWindow();
    await page.getByRole("button", { name: "添加项目", exact: true }).first().click();
    await page.getByLabel("消息", { exact: true }).fill("显示文件");
    await page.getByLabel("发送", { exact: true }).click();
    await expect(page.getByTestId("run").last()).toHaveAttribute("data-status", "completed");
    await page.locator(".answer").getByRole("button", { name: "长文件", exact: true }).click();
    const preview = page.locator(".file-manager:visible");
    const trigger = preview.getByRole("button", { name: "文件预览菜单", exact: true });
    await trigger.click();
    const wrap = page.getByRole("menuitemcheckbox", { name: "自动换行", exact: true });
    await expect(wrap).toHaveAttribute("aria-checked", "false");
    await wrap.click();
    await trigger.click();
    await expect(wrap).toHaveAttribute("aria-checked", "true");
    await page.getByRole("menuitem", { name: "复制相对路径", exact: true }).click();
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toBe("long.ts");
    await trigger.click();
    await page.getByRole("menuitem", { name: "复制绝对路径", exact: true }).click();
    await expect.poll(() => app?.evaluate(({ clipboard }) => clipboard.readText())).toBe(path);
    await trigger.click();
    await wrap.press("Escape");
    await expect(page.getByRole("menu")).toHaveCount(0);
    await expect(trigger).toBeFocused();
    await expect(preview.getByRole("button", { name: "在编辑器中打开", exact: true })).toBeDisabled();
    await page.screenshot({ path: "test-results/file-preview-wrap-menu.png" });
  } finally {
    await runCleanup([
      () => app?.close(),
      () => server.close(),
      () => rm(dir, { recursive: true, force: true }),
    ]);
  }
});