源码
tests/desktop/file-preview-comments.spec.ts
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 }),
]);
}
});