220 lines
15 KiB
TypeScript
220 lines
15 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
|
import { responseFixtureWriter } from "./support/responses-fixture";
|
|
|
|
async function setupTopics(page: Page) {
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await mockWorkbenchContext(page);
|
|
await mockAgentApi(page);
|
|
const topics = [{ id: "10000000-0000-4000-8000-000000000001", name: "压力异常事件", createdAt: "2026-10-22T01:00:00Z", updatedAt: "2026-10-22T01:00:00Z" },
|
|
{ id: "10000000-0000-4000-8000-000000000002", name: "阀门检修问题", createdAt: "2026-10-22T01:00:00Z", updatedAt: "2026-10-22T01:00:00Z" }];
|
|
const sessions = [{ id: "topic-chat-a", title: "夜间压力复核", topic_id: topics[0].id, updated_at: Date.now(), created_at: Date.now(), purpose: "ordinary_chat" },
|
|
{ id: "topic-chat-b", title: "检修记录核对", topic_id: topics[1].id, updated_at: Date.now() - 1000, created_at: Date.now() - 1000, purpose: "ordinary_chat" }];
|
|
const metrics = { topicReads: 0, sentTopic: "" };
|
|
await page.route(/\/api\/v1\/topics(?:\/[^?]+)?$/, (route) => {
|
|
const request = route.request(); if (request.method() === "OPTIONS") return route.fallback();
|
|
const id = new URL(request.url()).pathname.split("/").at(-1);
|
|
const headers = { "Access-Control-Allow-Origin": "*" };
|
|
if (request.method() === "GET") { metrics.topicReads++; return route.fulfill({ headers, json: { topics } }); }
|
|
if (request.method() === "POST") {
|
|
const topic = { id: "10000000-0000-4000-8000-000000000003", name: request.postDataJSON().name, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
|
|
topics.push(topic); return route.fulfill({ headers, status: 201, json: topic });
|
|
}
|
|
if (request.method() === "PATCH") { const topic = topics.find((entry) => entry.id === id)!; topic.name = request.postDataJSON().name; return route.fulfill({ headers, json: topic }); }
|
|
const index = topics.findIndex((entry) => entry.id === id); topics.splice(index, 1);
|
|
return route.fulfill({ headers, status: 204 });
|
|
});
|
|
await page.route("**/api/v1/sessions?**", (route) => {
|
|
if (route.request().method() === "OPTIONS") return route.fallback();
|
|
const query = new URL(route.request().url()).searchParams;
|
|
const q = query.get("q") ?? "";
|
|
const topicId = query.get("topic_id");
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
|
sessions: sessions.map((session) => ({ ...session, topic_id: topics.some((topic) => topic.id === session.topic_id) ? session.topic_id : null,
|
|
topic_name: topics.find((topic) => topic.id === session.topic_id)?.name,
|
|
search_snippet: q && "南桥阀门夜间巡检记录".includes(q) ? "正文命中:南桥阀门夜间巡检记录" : undefined,
|
|
})).filter((session) => (!q || session.title.includes(q) || session.search_snippet)
|
|
&& (!topicId || (topicId === "unclassified" ? !session.topic_id : session.topic_id === topicId))), has_more: false, next_cursor: null,
|
|
} });
|
|
});
|
|
await page.route(/\/api\/v1\/session\/topic-chat-[ab]\/topic$/, (route) => {
|
|
if (route.request().method() === "OPTIONS") return route.fallback();
|
|
const id = new URL(route.request().url()).pathname.split("/").at(-2);
|
|
const topicId = route.request().postDataJSON().topic_id;
|
|
sessions.find((session) => session.id === id)!.topic_id = topicId;
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { session_id: id, topic_id: topicId } });
|
|
});
|
|
await page.route("**/api/v1/responses", (route) => {
|
|
if (route.request().method() === "OPTIONS") return route.fallback();
|
|
metrics.sentTopic = route.request().postDataJSON().tjwater.topic_id;
|
|
const write = responseFixtureWriter("new-topic-conversation");
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, contentType: "text/event-stream", body: [
|
|
{ type: "start", messageId: "topic-answer" }, { type: "text-start", id: "topic-text" },
|
|
{ type: "text-delta", id: "topic-text", delta: "已按当前主题整理问题。" }, { type: "text-end", id: "topic-text" }, { type: "finish" },
|
|
].map(write).join("") });
|
|
});
|
|
return metrics;
|
|
}
|
|
|
|
test("topics organize chats while full-text history searches across all topics", async ({ page }) => {
|
|
test.setTimeout(60_000);
|
|
const metrics = await setupTopics(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const topicTrigger = page.getByRole("button", { name: "选择主题" });
|
|
await topicTrigger.click();
|
|
await page.getByRole("menuitem", { name: "新建主题", exact: true }).click();
|
|
const create = page.getByRole("dialog", { name: "新建主题", exact: true });
|
|
await expect(create.getByRole("textbox", { name: "主题名称" })).toBeFocused();
|
|
await expect(create).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await expect(page.locator(".agent-history-overlay")).toHaveCSS("backdrop-filter", "none");
|
|
await create.screenshot({ path: "test-results/agent-topics/create.png" });
|
|
await create.getByRole("textbox", { name: "主题名称" }).fill("城南供水复盘");
|
|
await create.getByRole("button", { name: "创建主题", exact: true }).click();
|
|
await expect(create).toBeHidden();
|
|
await expect(topicTrigger).toHaveText("城南供水复盘");
|
|
await topicTrigger.click();
|
|
await page.getByRole("menuitemradio", { name: "压力异常事件", exact: true }).click();
|
|
await expect(topicTrigger).toHaveText("压力异常事件");
|
|
await topicTrigger.click();
|
|
await page.getByRole("menuitemradio", { name: "城南供水复盘", exact: true }).click();
|
|
await expect(topicTrigger).toHaveText("城南供水复盘");
|
|
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
await panel.locator("textarea").fill("整理相关事件与待查问题");
|
|
await panel.getByRole("button", { name: "发送 Agent 指令", exact: true }).click();
|
|
await expect(panel.getByText("已按当前主题整理问题。", { exact: true })).toBeVisible();
|
|
expect(metrics.sentTopic).toBe("10000000-0000-4000-8000-000000000003");
|
|
await panel.locator("textarea").fill("这段草稿稍后继续整理");
|
|
await panel.getByRole("button", { name: "新建 Agent 对话" }).click();
|
|
await expect(panel.locator("textarea")).toHaveValue("");
|
|
await expect(topicTrigger).toHaveText("城南供水复盘");
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话", exact: true }).locator(".agent-history-session-trigger").filter({ hasText: "新对话" }).click();
|
|
await expect(page.getByRole("dialog", { name: "历史对话", exact: true })).toBeHidden();
|
|
await expect(panel.locator("textarea")).toHaveValue("这段草稿稍后继续整理");
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
const history = page.getByRole("dialog", { name: "历史对话", exact: true });
|
|
await history.getByRole("searchbox").fill("南桥阀门");
|
|
await expect(history.locator(".agent-history-session-trigger")).toHaveCount(2);
|
|
await expect(history.getByText("压力异常事件", { exact: true })).toBeVisible();
|
|
await expect(history.getByText("阀门检修问题", { exact: true })).toBeVisible();
|
|
await expect(history.getByText("正文命中:南桥阀门夜间巡检记录", { exact: true })).toHaveCount(2);
|
|
await history.getByRole("button", { name: "会话操作:夜间压力复核" }).click();
|
|
await page.getByRole("menuitem", { name: "移入主题", exact: true }).click();
|
|
await history.getByRole("button", { name: "城南供水复盘", exact: true }).click();
|
|
await expect(history.getByText("城南供水复盘", { exact: true })).toBeVisible();
|
|
await history.getByRole("button", { name: "筛选历史主题" }).click();
|
|
await expect(page.getByRole("menu")).toHaveCSS("background-color", "rgb(255, 255, 255)");
|
|
await page.getByRole("menuitemradio", { name: "阀门检修问题", exact: true }).click();
|
|
await expect(history.locator(".agent-history-session-trigger")).toHaveCount(1);
|
|
await history.getByRole("button", { name: "筛选历史主题" }).click();
|
|
await page.getByRole("menuitemradio", { name: "全部主题", exact: true }).click();
|
|
await expect(history.locator(".agent-history-session-trigger")).toHaveCount(2);
|
|
await history.screenshot({ path: "test-results/agent-topics/cross-topic-search.png" });
|
|
await page.keyboard.press("Escape");
|
|
await topicTrigger.click(); await page.getByRole("menuitem", { name: "管理主题", exact: true }).click();
|
|
const manage = page.getByRole("dialog", { name: "管理主题", exact: true });
|
|
await expect(manage.getByRole("button", { name: "新建主题", exact: true })).toBeFocused();
|
|
await manage.screenshot({ path: "test-results/agent-topics/manage.png" });
|
|
await manage.getByRole("button", { name: "重命名主题:城南供水复盘" }).click();
|
|
await expect(page.getByRole("dialog", { name: "重命名主题", exact: true }).getByRole("textbox", { name: "主题名称" })).toBeFocused();
|
|
await page.getByRole("dialog", { name: "重命名主题", exact: true }).getByRole("textbox", { name: "主题名称" }).fill("城南复盘与跟进");
|
|
await page.getByRole("dialog", { name: "重命名主题", exact: true }).getByRole("button", { name: "保存主题" }).click();
|
|
await expect(manage.getByText("城南复盘与跟进", { exact: true })).toBeVisible();
|
|
await manage.getByRole("button", { name: "关闭主题窗口" }).click();
|
|
await expect(topicTrigger).toHaveText("城南复盘与跟进");
|
|
await topicTrigger.click(); await page.getByRole("menuitem", { name: "管理主题", exact: true }).click();
|
|
await manage.getByRole("button", { name: "删除主题:城南复盘与跟进" }).click();
|
|
await page.getByRole("dialog", { name: "删除主题", exact: true }).screenshot({ path: "test-results/agent-topics/delete.png" });
|
|
await page.getByRole("dialog", { name: "删除主题", exact: true }).getByRole("button", { name: "确认删除主题" }).click();
|
|
await expect(manage.getByText("城南复盘与跟进", { exact: true })).toHaveCount(0);
|
|
await expect(manage.getByRole("button", { name: "关闭主题窗口" })).toBeEnabled();
|
|
await page.keyboard.press("Escape");
|
|
await expect(manage).toBeHidden();
|
|
await expect(topicTrigger).toHaveText("未分类");
|
|
const reads = metrics.topicReads;
|
|
for (let i = 0; i < 3; i++) { await topicTrigger.click(); await expect(page.getByRole("menuitem", { name: "新建主题", exact: true })).toBeVisible(); await page.keyboard.press("Escape"); }
|
|
expect(metrics.topicReads).toBe(reads);
|
|
await page.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await expect(history.getByText("夜间压力复核", { exact: true })).toBeVisible();
|
|
await expect(history.getByText("未分类", { exact: true })).toBeVisible();
|
|
});
|
|
|
|
test("a topic can be created on a 320px mobile sheet", async ({ page }) => {
|
|
await page.setViewportSize({ width: 320, height: 812 });
|
|
await setupTopics(page);
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: "打开 Agent 面板" }).click();
|
|
await page.getByRole("button", { name: "选择主题" }).click();
|
|
await page.getByRole("menuitem", { name: "新建主题", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", { name: "新建主题", exact: true });
|
|
await expect(dialog.getByRole("textbox", { name: "主题名称" })).toBeFocused();
|
|
await dialog.getByRole("textbox", { name: "主题名称" }).fill("供水异常事件与问题跟踪");
|
|
expect(await dialog.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
|
await dialog.screenshot({ path: "test-results/agent-topics/mobile-create.png" });
|
|
await dialog.getByRole("button", { name: "创建主题", exact: true }).click();
|
|
await expect(dialog).toBeHidden();
|
|
await expect(page.getByRole("button", { name: "选择主题" })).toHaveText("供水异常事件与问题跟踪");
|
|
});
|
|
|
|
test("a topic created during a slow menu read remains available for switching", async ({ page }) => {
|
|
await setupTopics(page);
|
|
let release!: () => void;
|
|
const gate = new Promise<void>((resolve) => { release = resolve; });
|
|
let writes = 0;
|
|
await page.route("**/api/v1/topics", async (route) => {
|
|
if (route.request().method() === "GET") await gate;
|
|
if (route.request().method() === "POST") writes++;
|
|
await route.fallback();
|
|
});
|
|
try {
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const trigger = page.getByRole("button", { name: "选择主题", exact: true });
|
|
await trigger.click();
|
|
await page.getByRole("menuitem", { name: "新建主题", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", { name: "新建主题", exact: true });
|
|
await dialog.getByRole("textbox", { name: "主题名称" }).fill("新建后持续跟踪");
|
|
await dialog.getByRole("button", { name: "创建主题", exact: true }).click();
|
|
await expect(dialog.locator('button[type="submit"]')).toHaveAttribute("aria-busy", "true");
|
|
expect(writes).toBe(0);
|
|
release();
|
|
await expect(dialog).toBeHidden();
|
|
await expect(trigger).toHaveText("新建后持续跟踪");
|
|
await expect(page.locator("body")).not.toHaveCSS("pointer-events", "none");
|
|
await trigger.click();
|
|
await page.getByRole("menuitemradio", { name: "压力异常事件", exact: true }).click();
|
|
await expect(trigger).toHaveText("压力异常事件");
|
|
await trigger.click();
|
|
await page.getByRole("menuitemradio", { name: "新建后持续跟踪", exact: true }).click();
|
|
await expect(trigger).toHaveText("新建后持续跟踪");
|
|
expect(writes).toBe(1);
|
|
} finally { release(); }
|
|
});
|
|
|
|
test("a failed topic creation keeps the name and allows retry", async ({ page }) => {
|
|
await setupTopics(page);
|
|
let failed = false;
|
|
await page.route("**/api/v1/topics", (route) => {
|
|
if (route.request().method() !== "POST" || failed) return route.fallback();
|
|
failed = true;
|
|
return route.fulfill({ status: 503, headers: { "Access-Control-Allow-Origin": "*" }, json: { message: "主题暂时无法保存,请重试" } });
|
|
});
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const trigger = page.getByRole("button", { name: "选择主题", exact: true });
|
|
await trigger.click();
|
|
await page.getByRole("menuitem", { name: "新建主题", exact: true }).click();
|
|
const dialog = page.getByRole("dialog", { name: "新建主题", exact: true });
|
|
await dialog.getByRole("textbox", { name: "主题名称" }).fill("保留输入并重试");
|
|
await dialog.getByRole("button", { name: "创建主题", exact: true }).click();
|
|
await expect(dialog.getByRole("alert")).toContainText("主题暂时无法保存");
|
|
await expect(dialog.getByRole("textbox", { name: "主题名称" })).toHaveValue("保留输入并重试");
|
|
await expect(dialog.getByRole("button", { name: "关闭主题窗口" })).toBeEnabled();
|
|
await dialog.getByRole("button", { name: "创建主题", exact: true }).click();
|
|
await expect(dialog).toBeHidden();
|
|
await expect(trigger).toHaveText("保留输入并重试");
|
|
await expect(trigger).toBeFocused();
|
|
});
|