Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running turns active while users steer or queue follow-ups and edit pending delivery. Refresh the model catalog before generation and preserve approval mode selection. Integrate scoped investigation messaging/presentation, geographic place selection, current Agent composer styles and supporting workbench contracts. Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and production build. Preserve direct Server/Agent routing and project authentication.
154 lines
8.9 KiB
TypeScript
154 lines
8.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
|
|
|
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 one composer and directly 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(".agent-panel-start-composer").boundingBox())!;
|
|
const panelBox = (await panel.boundingBox())!;
|
|
expect(Math.abs(composer.y + composer.height / 2 - (panelBox.y + 56 + (panelBox.height - 56) / 2))).toBeLessThan(1);
|
|
await expect(panel.locator("form")).toHaveCount(1);
|
|
await panel.screenshot({ path: "test-results/agent-history-ui/agent-start-generated.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(panel).toHaveCSS("width", `${width}px`);
|
|
}
|
|
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", "96px");
|
|
expect((await panel.locator(".agent-panel-band").last().boundingBox())!.height).toBeLessThanOrEqual(160);
|
|
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" });
|
|
|
|
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).toBe(96);
|
|
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, 192));
|
|
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", "96px");
|
|
await panel.screenshot({ path: "test-results/agent-history-ui/agent-conversation-short-viewport.png" });
|
|
});
|
|
|
|
test("failed recommendation generation leaves the centered 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("first send smoothly moves the existing composer to the bottom", async ({ page }) => {
|
|
await page.emulateMedia({ reducedMotion: "no-preference" });
|
|
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);
|
|
await panel.evaluate(async (element) => {
|
|
await Promise.all(element.getAnimations({ subtree: true })
|
|
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity)
|
|
.map((animation) => animation.finished.catch(() => undefined)));
|
|
});
|
|
const form = panel.locator("form");
|
|
await form.locator("textarea").fill("核对近期供水压力变化");
|
|
const beforeTop = (await form.boundingBox())!.y;
|
|
await page.evaluate(() => {
|
|
const panel = document.querySelector('aside[aria-label="Agent 命令面板"]')!;
|
|
const form = panel.querySelector("form")!;
|
|
const state = { form, positions: [] as Array<{ active: boolean; y: number }>, done: false };
|
|
Object.assign(window, { __agentComposerTransition: state });
|
|
const start = performance.now();
|
|
const sample = () => {
|
|
state.positions.push({ active: panel.getAttribute("data-agent-starting") === "false", y: form.getBoundingClientRect().y });
|
|
if (performance.now() - start < 1000) requestAnimationFrame(sample);
|
|
else state.done = true;
|
|
};
|
|
requestAnimationFrame(sample);
|
|
});
|
|
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
|
|
await expect(panel).toHaveAttribute("data-agent-starting", "false");
|
|
await expect.poll(() => page.evaluate(() => (window as typeof window & {
|
|
__agentComposerTransition: { done: boolean }
|
|
}).__agentComposerTransition.done)).toBe(true);
|
|
const afterTop = (await form.boundingBox())!.y;
|
|
const result = await page.evaluate(() => {
|
|
const state = (window as typeof window & { __agentComposerTransition: {
|
|
form: HTMLFormElement; positions: Array<{ active: boolean; y: number }>
|
|
} }).__agentComposerTransition;
|
|
return { sameForm: state.form === document.querySelector('aside[aria-label="Agent 命令面板"] form'), positions: state.positions };
|
|
});
|
|
expect(result.sameForm).toBe(true);
|
|
expect(afterTop).toBeGreaterThan(beforeTop);
|
|
expect(result.positions.some((position) => position.active && position.y > beforeTop + 4 && position.y < afterTop - 4)).toBe(true);
|
|
await expect(panel.locator("form")).toHaveCount(1);
|
|
});
|