import { expect, test } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; import { responsesFixture } from "./support/responses-fixture"; for (const width of [1440, 320]) { test(`native user message copy preserves source text at ${width}px`, async ({ page, context }) => { await page.setViewportSize({ width, height: 900 }); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await mockWorkbenchContext(page); await mockAgentApi(page); let responseIndex = 0; await page.route("**/api/v1/responses", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" }, body: responsesFixture([ { type: "start", messageId: `copy-answer-${++responseIndex}` }, { type: "data-final_answer", data: { content: "已核对管道。" } }, { type: "finish", finishReason: "stop" } ]) })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); const prompt = "能直接高亮三根管道吗"; await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill(prompt); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); await expect(panel.getByText("已核对管道。", { exact: true })).toBeVisible(); const answerParagraph = panel.locator(".agent-streaming-response p").first(); await answerParagraph.click({ clickCount: 3 }); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("已核对管道。"); const text = panel.locator(".agent-panel-user-message p"); await text.click({ clickCount: 3 }); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(prompt); // Exact substrings retain native copy behavior. await text.evaluate((paragraph) => { const range = document.createRange(); range.setStart(paragraph.firstChild!, 2); range.setEnd(paragraph.firstChild!, 6); window.getSelection()!.removeAllRanges(); window.getSelection()!.addRange(range); }); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(prompt.slice(2, 6)); // A selection containing another message must retain its native separators. const expectedCrossMessage = await panel.evaluate((element) => { const user = element.querySelector(".agent-panel-user-message p")!; const answer = element.querySelector(".agent-streaming-response p")!; const range = document.createRange(); range.setStart(user.firstChild!, 0); range.setEnd(answer.firstChild!, answer.textContent!.length); window.getSelection()!.removeAllRanges(); window.getSelection()!.addRange(range); return window.getSelection()!.toString(); }); expect(expectedCrossMessage).toContain("已核对管道。"); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(expectedCrossMessage); // Multiline source text keeps blank lines, indentation and interior spaces. const multiline = "第一行\n\n 第二行 内容"; await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill(multiline); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); const lastText = panel.locator(".agent-panel-user-message p").last(); await expect(lastText).toHaveText(multiline, { useInnerText: false }); await expect(panel.locator(".agent-streaming-response p")).toHaveCount(2); await expect(panel.locator(".agent-streaming-response p").last()).toHaveText("已核对管道。"); await panel.evaluate((element) => { const user = element.querySelectorAll(".agent-panel-user-message p"); const answer = element.querySelectorAll(".agent-streaming-response p"); const range = document.createRange(); range.setStart(user[user.length - 1].firstChild!, 0); range.setEnd(answer[answer.length - 1].firstChild!, 0); window.getSelection()!.removeAllRanges(); window.getSelection()!.addRange(range); }); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(multiline); }); } for (const width of [1440, 320]) { test(`assistant rich text keeps meaningful native copy at ${width}px`, async ({ page, context }) => { await page.setViewportSize({ width, height: 900 }); await context.grantPermissions(["clipboard-read", "clipboard-write"]); await mockWorkbenchContext(page); await mockAgentApi(page); const markdown = "首段包含**加粗**文字。\n\n第二段保留段落。\n\n```text\n first\n second\n```\n\n| 名称 | 数值 |\n| --- | --- |\n| 流量 | 12 |"; await page.route("**/api/v1/responses", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream" }, body: responsesFixture([ { type: "start", messageId: "rich-copy-answer" }, { type: "data-final_answer", data: { content: markdown } }, { type: "finish", finishReason: "stop" } ]) })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" }).fill("核对格式"); await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click(); const response = panel.locator(".agent-streaming-response"); await expect(response.locator("table")).toBeVisible(); await response.locator("p").first().click({ clickCount: 3 }); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe("首段包含加粗文字。"); for (const selector of ["p", "pre", "table"]) { const nativeText = await response.evaluate((element, selector) => { const nodes = element.querySelectorAll(selector); const range = document.createRange(); if (selector === "p") { range.setStart(nodes[0].firstChild!, 0); range.setEnd(nodes[1].firstChild!, nodes[1].firstChild!.textContent!.length); } else { range.selectNodeContents(nodes[0]); } window.getSelection()!.removeAllRanges(); window.getSelection()!.addRange(range); return window.getSelection()!.toString(); }, selector); if (selector === "p") expect(nativeText).toContain("\n\n"); if (selector === "pre") expect(nativeText).toContain(" first\n second"); if (selector === "table") expect(nativeText).toContain("\t"); await page.keyboard.press("Control+c"); expect(await page.evaluate(() => navigator.clipboard.readText())).toBe(nativeText); } await expect(panel.getByRole("button", { name: /复制/ }).first()).toBeAttached(); }); }