import { expect, test, type Locator } from "@playwright/test"; import { mockAgentApi } from "./support/mock-agent-api"; import { mockWorkbenchContext } from "./support/mock-workbench-context"; async function expectCenteredStart(panel: Locator) { await expect(panel).toHaveAttribute("data-agent-centered", "true"); await expect.poll(async () => { const body = (await panel.locator(".agent-panel-body").boundingBox())!; const group = (await panel.locator(".agent-panel-band").boundingBox())!; return Math.abs(group.y + group.height / 2 - body.y - body.height / 2); }).toBeLessThan(2); const body = (await panel.locator(".agent-panel-body").boundingBox())!; const form = (await panel.locator("form").boundingBox())!; expect(Math.abs(form.x + form.width / 2 - body.x - body.width / 2)).toBeLessThan(1); expect(form.width).toBeLessThanOrEqual(552); await expect(panel.locator("form")).toHaveCount(1); } async function preparePreview(page: import("@playwright/test").Page) { await page.evaluate(() => document.fonts.ready); // Close this snapshot together; positional locators shift as earlier notices exit. await page.getByRole("button", { name: "关闭通知", exact: true }).evaluateAll((buttons) => { for (const button of buttons) (button as HTMLButtonElement).click(); }); } test.beforeEach(async ({ page }) => { await page.emulateMedia({ reducedMotion: "reduce" }); await mockWorkbenchContext(page); await mockAgentApi(page, { sessions: [{ session_id: "recent-pressure", title: "城南供水压力复核", created_at: Date.now() - 3_600_000, updated_at: Date.now() - 60_000, }] }); }); test("empty conversation centers the first question and sends an Agent-generated recent activity question", async ({ page }) => { let requests = 0; await page.route("**/api/v1/suggestions", async (route) => { if (route.request().method() === "OPTIONS") return route.fallback(); requests += 1; const body = route.request().postDataJSON() as { model: string; activities: Array<{ id: string; title: string; kind: string }> }; expect(body.model).toBe("test/model"); const recent = body.activities.find((item) => item.title === "城南供水压力复核"); expect(recent).toBeDefined(); await route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { suggestions: [{ question: "重新核对城南供水压力复核涉及的监测证据", activity_id: recent!.id, }] } }); }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(panel).toHaveAttribute("data-agent-starting", "true"); await expect(panel.getByRole("heading", { name: "有什么可以帮你?" })).toBeVisible(); const question = panel.getByRole("button", { name: /重新核对城南供水压力复核涉及的监测证据/ }); await expect(question).toBeVisible(); await expect(question).toContainText("近期会话"); const composer = (await panel.locator("form").boundingBox())!; await expectCenteredStart(panel); const suggestions = (await panel.getByRole("group", { name: "推荐问题" }).boundingBox())!; expect(suggestions.y).toBeGreaterThan(composer.y + composer.height); expect(composer.height).toBeLessThan(150); await expect(panel.locator("form")).toHaveCount(1); await panel.screenshot({ path: "/tmp/nexttjwater-agent-start-preview/centered-desktop.png" }); const handle = page.getByRole("separator", { name: "调整 Agent 面板宽度" }); for (const width of [400, 640]) { const box = (await handle.boundingBox())!; await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); await page.mouse.down(); await page.mouse.move(width, box.y + box.height / 2); await page.mouse.up(); await expect.poll(async () => Math.round((await panel.boundingBox())!.width)).toBe(width); await expectCenteredStart(panel); } expect(requests).toBe(1); await question.click(); await expect(panel).toHaveAttribute("data-agent-starting", "false"); await expect(panel.locator(".agent-panel-composer-motion")).toHaveCSS("transform", "none"); await expect(panel.locator("textarea")).toHaveCSS("min-height", "64px"); expect((await panel.locator(".agent-panel-band").last().boundingBox())!.height).toBeLessThanOrEqual(180); await expect(panel.locator(".agent-panel-user-message")).toHaveText("重新核对城南供水压力复核涉及的监测证据"); await expect(panel.locator("form")).toHaveCount(1); await expect.poll(async () => { const form = (await panel.locator("form").boundingBox())!; const box = (await panel.boundingBox())!; return Math.abs(form.y + form.height - (box.y + box.height - 8)); }).toBeLessThan(1); await panel.screenshot({ path: "test-results/agent-history-ui/agent-conversation-compact.png" }); }); test("multiline follow-up stays reachable in a short desktop viewport", async ({ page }) => { await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await panel.locator("textarea").fill("核对供水压力"); await panel.getByRole("button", { name: "发送 Agent 指令" }).click(); await expect(panel).toHaveAttribute("data-agent-starting", "false"); const prompt = panel.locator("textarea"); await prompt.focus(); await page.keyboard.type("核对监测点"); await page.keyboard.press("Shift+Enter"); await page.keyboard.type("补充现场记录"); await expect(prompt).toHaveValue("核对监测点\n补充现场记录"); const initialHeight = (await prompt.boundingBox())!.height; expect(initialHeight).toBeGreaterThanOrEqual(64); expect(initialHeight).toBeLessThanOrEqual(70); await prompt.fill("监测点、异常时间及现场核验记录。\n".repeat(30)); for (const height of [900, 640]) { await page.setViewportSize({ width: 1280, height }); const promptBox = (await prompt.boundingBox())!; expect(promptBox.height).toBeGreaterThan(initialHeight); expect(promptBox.height).toBeLessThanOrEqual(Math.min(height * 0.24, 160)); expect(promptBox.x).toBeGreaterThanOrEqual(0); await expect.poll(() => prompt.evaluate((element) => { const input = element.getBoundingClientRect(); const container = element.closest("aside")!.getBoundingClientRect(); return input.left >= container.left && input.right <= container.right; })).toBe(true); expect(await prompt.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true); const send = panel.getByRole("button", { name: "发送 Agent 指令" }); await expect(send).toBeVisible(); const sendBox = (await send.boundingBox())!; expect(sendBox.y + sendBox.height).toBeLessThanOrEqual(height); expect((await panel.getByRole("log").boundingBox())!.height).toBeGreaterThan(100); } await prompt.fill(""); await expect(prompt).toHaveCSS("height", "64px"); await panel.screenshot({ path: "test-results/agent-history-ui/agent-conversation-short-viewport.png" }); }); test("failed recommendation generation leaves the input ready for a normal conversation", async ({ page }) => { await page.route("**/api/v1/suggestions", (route) => route.request().method() === "OPTIONS" ? route.fallback() : route.fulfill({ status: 503, headers: { "Access-Control-Allow-Origin": "*" }, json: { message: "unavailable" }, })); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(panel.getByText("推荐问题暂不可用,可直接输入任务。")).toBeVisible(); const prompt = panel.getByPlaceholder("询问供水问题,或描述需要处理的任务"); await prompt.fill("核对供水运行情况"); await panel.getByRole("button", { name: "发送 Agent 指令" }).click(); await expect(panel.locator(".agent-panel-user-message")).toHaveText("核对供水运行情况"); await expect(panel).toHaveAttribute("data-agent-starting", "false"); }); test("sending moves the same centered composer to the bottom and new conversation restores the start", async ({ page }) => { await page.emulateMedia({ reducedMotion: "no-preference" }); await mockAgentApi(page, { sessions: [] }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(panel.getByRole("group", { name: "推荐问题" }).getByRole("button")).toHaveCount(3); const form = panel.locator("form"); await preparePreview(page); await panel.screenshot({ path: "/tmp/nexttjwater-agent-start-preview/centered-desktop.png" }); await form.locator("textarea").fill("核对近期供水压力变化"); await panel.evaluate(async (element) => { await Promise.allSettled(element.getAnimations({ subtree: true }) .filter((animation) => animation.effect?.getComputedTiming().iterations !== Infinity) .map((animation) => animation.finished)); }); await page.evaluate(() => { Object.assign(window, { __agentComposerBefore: document.querySelector('aside[aria-label="Agent 命令面板"] form') }); }); const before = (await form.boundingBox())!; await form.evaluate((element) => { const positions: number[] = []; Object.assign(window, { composerMotionPositions: positions }); const start = performance.now(); const sample = () => { positions.push(element.getBoundingClientRect().y); if (performance.now() - start < 700) requestAnimationFrame(sample); }; requestAnimationFrame(sample); }); await panel.getByRole("button", { name: "发送 Agent 指令" }).click(); await expect(panel).toHaveAttribute("data-agent-starting", "false"); await expect(panel.locator(".agent-panel-user-message")).toBeVisible(); await expect(panel.locator(".agent-panel-composer-motion")).toHaveCSS("transform", "none"); const after = (await form.boundingBox())!; expect(after.y).toBeGreaterThan(before.y + 100); await expect(panel).toHaveAttribute("data-agent-centered", "false"); const panelBox = (await panel.boundingBox())!; expect(Math.abs(after.y + after.height - (panelBox.y + panelBox.height - 8))).toBeLessThan(1); expect(Math.abs(after.height - before.height)).toBeLessThan(1); await expect.poll(() => page.evaluate(({ start, end }) => (window as unknown as { composerMotionPositions: number[] }).composerMotionPositions .some((y) => y > start + 2 && y < end - 2), { start: before.y, end: after.y })).toBe(true); expect(await page.evaluate(() => (window as typeof window & { __agentComposerBefore: Element }).__agentComposerBefore === document.querySelector('aside[aria-label="Agent 命令面板"] form'))).toBe(true); await expect(panel.locator("form")).toHaveCount(1); await panel.getByRole("button", { name: "新建 Agent 对话" }).click(); await expect(panel.locator(".agent-panel-composer-motion")).toHaveCSS("transform", "none"); await expectCenteredStart(panel); await expect(form.locator("textarea")).toHaveValue(""); }); for (const width of [320, 375]) { test(`centered first question and restored persona fit at ${width}px`, async ({ page }) => { await page.setViewportSize({ width, height: 844 }); await page.goto("/", { waitUntil: "domcontentloaded" }); await page.getByRole("button", { name: "打开 Agent 面板" }).click(); const panel = page.getByRole("complementary", { name: "Agent 命令面板" }); await expect(panel.getByRole("heading", { name: "有什么可以帮你?" })).toBeVisible(); await expect(panel.getByRole("group", { name: "推荐问题" }).getByRole("button")).toHaveCount(3); const form = (await panel.locator("form").boundingBox())!; expect(form.x).toBeGreaterThanOrEqual(0); expect(form.x + form.width).toBeLessThanOrEqual(width); expect(form.height).toBeLessThan(150); await expectCenteredStart(panel); await expect(panel.locator("header").getByLabel("Agent 状态")).toBeVisible(); await preparePreview(page); await panel.screenshot({ path: `/tmp/nexttjwater-agent-start-preview/centered-${width}.png` }); await panel.locator("textarea").fill("短屏下也能完成首次提问"); await page.setViewportSize({ width, height: 480 }); const send = panel.getByRole("button", { name: "发送 Agent 指令" }); await send.scrollIntoViewIfNeeded(); expect(await panel.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true); const sendBox = (await send.boundingBox())!; expect(sendBox.y + sendBox.height).toBeLessThanOrEqual(480); await send.click(); await expect(panel.locator(".agent-panel-user-message")).toHaveText("短屏下也能完成首次提问"); await expect(panel).toHaveAttribute("data-agent-centered", "false"); }); }