213 lines
16 KiB
TypeScript
213 lines
16 KiB
TypeScript
import { responsesFixture } from "./support/responses-fixture";
|
|
import { expect, test, type Locator } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
|
|
|
// Product display stays UTC+8 even when the browser itself is configured for UTC.
|
|
test.use({ timezoneId: "UTC" });
|
|
|
|
const taskId = "22222222-2222-4222-8222-222222222222";
|
|
const runId = "33333333-3333-4333-8333-333333333333";
|
|
const projectId = "11111111-1111-4111-8111-111111111111";
|
|
const now = "2026-10-13T08:00:00Z";
|
|
const run = { run_id: runId, task_id: taskId, generation: 2, execution_request_id: runId, status: "succeeded", revision: 1,
|
|
agent_session_id: "investigation-run", lease_expires_at: null, heartbeat_at: null, started_at: now, finished_at: now,
|
|
failure_code: null, failure_detail: null, dispatch_attempts: 1, next_dispatch_at: now, created_at: now, updated_at: now };
|
|
const task = { project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint",
|
|
lifecycle_status: "completed", active_run: null, latest_run: run, status: "completed", priority: "normal", revision: 1,
|
|
execution_generation: 2, execution_request_id: runId, title: "居民异味投诉调查", summary: "现场记录待复核",
|
|
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "water_quality_complaint" },
|
|
trigger: { kind: "manual", occurred_at: now, detail: "居民异味投诉" }, scheduled_for: null, agent_session_id: "investigation-run",
|
|
agent_lease_expires_at: null, current_plan_revision: null, current_report_revision: null, created_at: now, updated_at: now,
|
|
latest_plan: null, latest_report: {
|
|
revision: 3, execution_generation: 1, source_session_id: "investigation-run", published_at: now, published_by: "agent",
|
|
report: { title: "居民异味投诉调查报告", current_conclusion: "现场记录待复核", conclusion_boundary: "仅覆盖已有监测记录",
|
|
evidence_refs: ["monitor-1"], recommendations: [], pending_items: [], stage: "final", outcome: "concluded",
|
|
stop_reason: null, data_gaps: [], diagram: null }
|
|
}, latest_progress: null,
|
|
evidence: [{ evidence_id: "monitor-1", kind: "system_data", summary: "上游监测数据质量已核对", source_label: "SCADA 水质监测",
|
|
source_ref: "WQ-01", observed_at: now, recorded_at: now, recorded_by: "agent", has_snapshot: false }],
|
|
actions: [], access_requests: [], work_order_snapshot: null,
|
|
permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false,
|
|
can_decide_work_orders: false, can_execute_work_orders: false, can_decide_commands: false, can_execute_commands: false, can_decide_access: false } };
|
|
|
|
test("证据提问在预览、展开及更多依据中使用统一实体按钮", async ({ page }, testInfo) => {
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await page.route("http://127.0.0.1:8787/api/v1/**", (route) => route.fulfill({ json: { items: [] } }));
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.setContent('<html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="report" style="max-width:640px;margin:auto;padding:16px"></main></body></html>');
|
|
await page.evaluate(async ({ fixture, cachePath }) => {
|
|
const reactModule = await import(`${cachePath}/react.js`);
|
|
const React = reactModule.default ?? reactModule;
|
|
const clientModule = await import(`${cachePath}/react-dom_client.js`);
|
|
const { createRoot } = clientModule.default ?? clientModule;
|
|
const { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx");
|
|
const evidence = Array.from({ length: 4 }, (_, index) => ({ ...fixture.evidence[0], evidence_id: `monitor-${index + 1}` }));
|
|
createRoot(document.getElementById("report")).render(React.createElement(InvestigationTaskReport, {
|
|
task: { ...fixture, evidence, latest_report: { ...fixture.latest_report, report: { ...fixture.latest_report.report, evidence_refs: evidence.map((item) => item.evidence_id) } } },
|
|
onContinue: () => undefined, renderAction: () => null,
|
|
renderEvidence: (item: { summary: string }) => React.createElement("p", null, item.summary)
|
|
}));
|
|
}, { fixture: task, cachePath: `/node_modules/.vite-playwright-${process.env.PLAYWRIGHT_PORT || "5191"}/deps` });
|
|
const preview = page.getByRole("region", { name: "主要依据", exact: true });
|
|
await expect(page.getByText("任务更新于 2026/10/13 16:00:00", { exact: true })).toBeVisible();
|
|
await expect(preview).toContainText("2026/10/13 16:00:00");
|
|
for (const width of [1440, 320]) {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
const buttons = preview.getByRole("button", { name: "围绕此证据提问", exact: true });
|
|
await expect(buttons).toHaveCount(3);
|
|
for (const button of await buttons.all()) {
|
|
await expect(button).toHaveAttribute("data-slot", "button");
|
|
await expect(button).toHaveCSS("border-top-width", "1px");
|
|
await expect(button).toHaveCSS("height", "32px");
|
|
}
|
|
await preview.screenshot({ path: testInfo.outputPath(`evidence-buttons-${width}.png`) });
|
|
await preview.getByRole("button", { name: "展开主要依据" }).click();
|
|
const dialog = page.getByRole("dialog");
|
|
await dialog.getByText("更多引用依据 · 1 条", { exact: true }).click();
|
|
const expandedButtons = dialog.getByRole("button", { name: "围绕此证据提问", exact: true });
|
|
await expect(expandedButtons).toHaveCount(4);
|
|
for (const button of await expandedButtons.all()) {
|
|
await expect(button).toBeVisible();
|
|
await expect(button).toHaveAttribute("data-slot", "button");
|
|
await expect(button).toHaveCSS("border-top-width", "1px");
|
|
expect(await button.evaluate((element) => element.getBoundingClientRect().right <= innerWidth)).toBe(true);
|
|
}
|
|
await dialog.screenshot({ path: testInfo.outputPath(`evidence-buttons-expanded-${width}.png`) });
|
|
await page.keyboard.press("Escape");
|
|
await expect(dialog).toHaveCount(0);
|
|
}
|
|
});
|
|
|
|
async function expectTaskReferenceLayout(panel: Locator) {
|
|
await expect(panel).toHaveAttribute("data-agent-starting", "true");
|
|
await expect(panel.getByRole("group", { name: "对话引用" })).toBeVisible();
|
|
await expect(panel.getByRole("group", { name: "推荐问题" })).toHaveCount(0);
|
|
await expect(panel.locator("form")).toHaveCount(1);
|
|
// Existing task state keeps the normal reading area, as required by agent-chat-ui-design.md.
|
|
const status = panel.getByRole("region", { name: "任务协作状态" });
|
|
await expect(status).toBeVisible();
|
|
await expect.poll(async () => {
|
|
const panelBox = (await panel.boundingBox())!;
|
|
const composer = (await panel.locator("form").boundingBox())!;
|
|
return panelBox.y + panelBox.height - composer.y - composer.height;
|
|
}).toBeLessThan(32);
|
|
const statusBox = (await status.boundingBox())!;
|
|
const composerBox = (await panel.locator("form").boundingBox())!;
|
|
expect(composerBox.y).toBeGreaterThanOrEqual(statusBox.y + statusBox.height);
|
|
}
|
|
|
|
test("单一协作入口展示任务引用,保留草稿且仅手动发送,并适配窄屏", async ({ page }, testInfo) => {
|
|
test.setTimeout(60_000);
|
|
await page.emulateMedia({ reducedMotion: "reduce" });
|
|
await mockWorkbenchContext(page);
|
|
await mockAgentApi(page, { sessions: [{ session_id: "ordinary-main", title: "供水核查主聊天", purpose: "ordinary_chat", created_at: now, updated_at: now }] });
|
|
let submitted = 0;
|
|
let inboxReads = 0;
|
|
let incoming = false;
|
|
page.on("request", (request) => {
|
|
if (request.method() === "POST" && (new URL(request.url()).pathname.endsWith("/responses") || new URL(request.url()).pathname.endsWith("/investigations"))) submitted += 1;
|
|
});
|
|
const { latest_plan, latest_report, latest_progress, evidence, actions, access_requests, work_order_snapshot, permissions, ...summary } = task;
|
|
void latest_plan; void latest_report; void latest_progress; void evidence; void actions; void access_requests; void work_order_snapshot; void permissions;
|
|
await page.route("**/workbench/sync-snapshot", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
|
schema: "workbench-sync-snapshot@1", event_cursor: 0, runtime: { availability: "not_configured", source: "none", scenario_availability: "not_configured",
|
|
artifact_availability: "not_configured", schedule_availability: "not_configured", work_order_availability: "not_configured", alert_availability: "not_configured",
|
|
generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] }, tasks: [summary], notifications: [], generated_at: now
|
|
} }));
|
|
await page.route("**/operational-tasks/**", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: task }));
|
|
await page.route("**/agent-messages?**", (route) => {
|
|
inboxReads += 1;
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { items: incoming ? [{ message_id: "msg1", task_id: taskId, run_id: runId,
|
|
source_session_id: "investigation-run", target_session_id: "ordinary-main", direction: "to_chat", text: "已核对上游监测,请补充现场采样记录。",
|
|
status: "pending", in_reply_to: null, created_at: now, failure_detail: null }] : [], next_cursor: null } });
|
|
});
|
|
await page.route("**/agent-messages/stream", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, contentType: "text/event-stream", body: 'event: agent_message\ndata: {"session_id":"ordinary-main"}\n\n' }));
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
|
|
const agent = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
await expect(agent.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0);
|
|
await agent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /^供水核查主聊天/ }).click();
|
|
const prompt = agent.locator("textarea");
|
|
await prompt.fill("保留的现场补充草稿");
|
|
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
|
const feed = page.getByRole("region", { name: "工况任务", exact: true });
|
|
await expect(feed).toBeVisible();
|
|
await feed.getByRole("button", { name: "展开工况任务" }).click();
|
|
await feed.getByRole("button", { name: /调查任务/ }).click();
|
|
await feed.getByText("居民异味投诉调查", { exact: true }).first().click();
|
|
await expect(feed.getByText("16:00", { exact: true })).toBeVisible();
|
|
await expect(feed.getByRole("region", { name: "调查任务标识" })).toHaveCount(0);
|
|
await expect(feed.getByRole("button", { name: "在主聊天中编辑" })).toHaveCount(0);
|
|
await expect(feed).not.toContainText(taskId);
|
|
await expect(feed).not.toContainText(runId);
|
|
await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveCount(1);
|
|
await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveAttribute("title", "打开调查协作对话,引用此任务,不会直接发送。");
|
|
await feed.screenshot({ path: testInfo.outputPath("investigation-entry-desktop.png") });
|
|
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
|
|
await expect(agent.getByText("任务协作问答", { exact: true }).first()).toBeVisible();
|
|
const reference = agent.getByRole("group", { name: "对话引用" });
|
|
await expect(reference).toContainText("居民异味投诉调查");
|
|
await expect(reference).toContainText("第 1 轮 · 报告第 3 版");
|
|
await expect(reference).not.toContainText(/当前|第 2 轮|已结束|调查完成|正在调查/);
|
|
await expect(prompt).toHaveAttribute("placeholder", "可询问调查进展、讨论报告或补充信息");
|
|
await expect(prompt).toHaveValue("保留的现场补充草稿");
|
|
await expect(prompt).toBeFocused();
|
|
expect(submitted).toBe(0);
|
|
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
|
|
await expect(prompt).toBeFocused();
|
|
await expect(prompt).toHaveValue("保留的现场补充草稿");
|
|
await feed.getByLabel("主要依据").getByRole("button", { name: "围绕此证据提问" }).click();
|
|
await expect(prompt).toHaveValue("保留的现场补充草稿\n\n请解释“上游监测数据质量已核对”这条证据的范围、来源和判断影响。");
|
|
await expect(prompt).toBeFocused();
|
|
await expect(reference).toContainText("已引用证据");
|
|
await expectTaskReferenceLayout(agent);
|
|
expect(submitted).toBe(0);
|
|
await agent.screenshot({ path: testInfo.outputPath("investigation-reference-desktop.png") });
|
|
await feed.getByRole("button", { name: /定时任务/ }).click();
|
|
await page.setViewportSize({ width: 375, height: 900 });
|
|
await page.getByRole("button", { name: "打开 Agent 面板", exact: true }).click();
|
|
const mobileAgent = page.getByRole("complementary", { name: "Agent 命令面板" });
|
|
await expect(mobileAgent.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0);
|
|
await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toContainText("居民异味投诉调查");
|
|
await expectTaskReferenceLayout(mobileAgent);
|
|
await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-reference-narrow.png") });
|
|
await page.setViewportSize({ width: 320, height: 900 });
|
|
await expectTaskReferenceLayout(mobileAgent);
|
|
await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-reference-320.png") });
|
|
expect(await mobileAgent.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true);
|
|
await page.setViewportSize({ width: 375, height: 900 });
|
|
let sentContext: unknown;
|
|
await page.route("**/api/v1/responses", async (route) => {
|
|
if (route.request().method() !== "POST") return route.fallback();
|
|
sentContext = route.request().postDataJSON().tjwater.task_context;
|
|
expect(route.request().postDataJSON().conversation).toBe(`collaboration-${taskId}`);
|
|
await route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "X-Vercel-AI-UI-Message-Stream": "v1" }, contentType: "text/event-stream",
|
|
body: responsesFixture([{ type: "start", messageId: "reply" }, { type: "finish", finishReason: "stop" }]) });
|
|
});
|
|
expect(submitted).toBe(0);
|
|
await mobileAgent.getByRole("button", { name: "发送 Agent 指令" }).click();
|
|
await expect.poll(() => sentContext).toEqual({ report_revision: 3, evidence_id: "monitor-1" });
|
|
expect(submitted).toBe(1);
|
|
await expect(mobileAgent).toHaveAttribute("data-agent-starting", "false");
|
|
await expect(mobileAgent.locator(".agent-panel-user-message")).toContainText("保留的现场补充草稿");
|
|
await expect.poll(async () => {
|
|
const panelBox = (await mobileAgent.boundingBox())!;
|
|
const composer = (await mobileAgent.locator("form").boundingBox())!;
|
|
return panelBox.y + panelBox.height - composer.y - composer.height;
|
|
}).toBeLessThan(32);
|
|
await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0);
|
|
incoming = true;
|
|
await mobileAgent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /^供水核查主聊天/ }).click();
|
|
await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toHaveCount(0);
|
|
await expect(mobileAgent.getByText("已核对上游监测,请补充现场采样记录。")).toBeVisible();
|
|
await expect(mobileAgent.getByText("待接收", { exact: true })).toBeVisible();
|
|
await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0);
|
|
expect(inboxReads).toBeGreaterThan(1);
|
|
await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-inbox-narrow.png") });
|
|
expect(await mobileAgent.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true);
|
|
expect(submitted).toBe(1);
|
|
});
|