import { expect, test } from "@playwright/test"; const frame = (event) => "data: " + JSON.stringify(event) + "\n\n"; const response = (events) => ({ status: 200, contentType: "text/event-stream", headers: { "X-Session-Id": "frontend-test", "X-Agent-Name": "main" }, body: events.map(frame).join(""), }); test("Vue history renders GFM and sanitizes HTML through the actual page", async ({ page, }) => { await page.goto("/"); await expect(page.locator("#result")).toHaveClass("pass"); await expect(page.locator("#result")).toContainText("8项通过"); }); test("streamed tokens retain tool order and request identity", async ({ page, }) => { let submitted; await page.route("**/api/chat", async (route) => { submitted = route.request().postData(); await route.fulfill( response([ { type: "token", content: "**之前**" }, { type: "tool_call", tool: "test_tool", tool_call_id: "tool-1", args: { x: 1 }, }, { type: "tool_result", tool: "test_tool", tool_call_id: "tool-1", content: { ok: true }, }, { type: "token", content: "## 之后\n\n**末尾**" }, { type: "done" }, ]), ); }); await page.goto("/static/index.html"); await page.getByLabel("输入消息").fill("测试流式渲染"); await page.getByRole("button", { name: "发送消息", exact: true }).click(); await expect( page.locator(".assistant-body .markdown").last().locator("h2"), ).toHaveText("之后"); const tags = await page .locator(".assistant-body") .evaluate((node) => [...node.children].map((child) => child.className)); expect(tags[0]).toBe("markdown"); expect(tags[1]).toContain("tool-card"); expect(tags[2]).toBe("markdown"); await expect(page.locator(".tool-card")).not.toHaveAttribute("open"); expect(submitted).toContain('name="request_id"'); }); test("batch approvals preserve all actions and the current interrupt token", async ({ page, }) => { await page.route("**/api/chat", (route) => route.fulfill( response([ { type: "interrupt", tool_approval: { tool: "one", interrupt_token: "current-batch-token", actions: [ { name: "one", args: { path: "a" } }, { name: "two", args: { path: "b" } }, ], }, }, ]), ), ); let payload; await page.route("**/api/chat/resume", async (route) => { payload = route.request().postDataJSON(); await route.fulfill( response([{ type: "token", content: "已批准" }, { type: "done" }]), ); }); await page.goto("/static/index.html"); await page.getByLabel("输入消息").fill("需要审批"); await page.getByRole("button", { name: "发送消息", exact: true }).click(); const approve = page.getByRole("button", { name: "批准全部 2 项", exact: true, }); await expect(approve).toBeEnabled(); await expect(page.locator(".approval-card")).toContainText("one"); await expect(page.locator(".approval-card")).toContainText("two"); await approve.click(); await expect(page.locator(".approval-card")).toContainText("已提交确认"); expect(payload.interrupt_token).toBe("current-batch-token"); expect(payload.approved).toBe(true); }); test("ask-user answers keep the original card available when a resume request fails", async ({ page, }) => { await page.route("**/api/chat", (route) => route.fulfill( response([ { type: "interrupt", interrupt_token: "question-token", ask_user: { questions: ["请选择区域", "需要什么格式"] }, }, ]), ), ); let attempts = 0, payload; await page.route("**/api/chat/resume", async (route) => { payload = route.request().postDataJSON(); attempts++; await route.fulfill( attempts === 1 ? { status: 503, contentType: "application/json", body: JSON.stringify({ detail: { message: "临时不可用" } }), } : response([{ type: "done" }]), ); }); await page.goto("/static/index.html"); await page.getByLabel("输入消息").fill("提问"); await page.getByRole("button", { name: "发送消息", exact: true }).click(); await expect(page.locator(".approval-card textarea")).toHaveCount(2); await page.locator(".approval-card textarea").nth(0).fill("一区"); await page.locator(".approval-card textarea").nth(1).fill("表格"); const submit = page.getByRole("button", { name: "提交回答", exact: true }); await expect(submit).toBeEnabled(); await submit.click(); await expect(page.locator(".notice.error")).toContainText("临时不可用"); await expect(submit).toBeEnabled(); await submit.click(); await expect(page.locator(".approval-card")).toContainText("已提交确认"); expect(payload.answers).toEqual(["一区", "表格"]); expect(payload.interrupt_token).toBe("question-token"); }); test("stopping a stream keeps the partial Markdown reply", async ({ page }) => { await page.addInitScript(() => { const original = window.fetch; window.fetch = (url, options) => url === "/api/chat" ? Promise.resolve( new Response( new ReadableStream({ start(controller) { controller.enqueue( new TextEncoder().encode( 'data: {"type":"token","content":"**保留部分回复**"}\n\n', ), ); options.signal.addEventListener("abort", () => controller.error(new DOMException("Stopped", "AbortError")), ); }, }), { headers: { "Content-Type": "text/event-stream", "X-Session-Id": "frontend-test", }, }, ), ) : original(url, options); }); await page.goto("/static/index.html"); await page.getByLabel("输入消息").fill("停止测试"); await page.getByRole("button", { name: "发送消息", exact: true }).click(); await expect(page.locator(".assistant-body strong").first()).toHaveText( "保留部分回复", ); await page.getByRole("button", { name: "停止生成", exact: true }).click(); await expect(page.locator(".assistant-body")).toContainText("已停止生成"); await expect(page.locator(".assistant-body strong").first()).toHaveText( "保留部分回复", ); }); test("mobile navigation and report history fit the actual viewport", async ({ page, }) => { await page.setViewportSize({ width: 390, height: 844 }); await page.goto("/static/index.html"); await page.getByRole("button", { name: "打开导航", exact: true }).click(); await page .getByRole("button", { name: "Markdown历史测试", exact: true }) .click(); await expect(page.locator(".markdown h1")).toHaveText("Markdown 标题"); expect( await page.evaluate( () => document.documentElement.scrollWidth > innerWidth, ), ).toBe(false); await page.getByRole("button", { name: "打开导航", exact: true }).click(); await page.getByRole("tab", { name: /智能体测试/ }).click(); await expect(page.locator(".lab-page-heading h2")).toBeVisible(); expect( await page.evaluate( () => document.documentElement.scrollWidth > innerWidth, ), ).toBe(false); }); const simulationDefaults = { mine_code: "mock-zy", mine_name: "陕西众源煤业集团有限公司", direction: "1_to_2", stopped_at: "2026-10-15T10:00:00+08:00", switch_interval_minutes: 6, before_flow_m3_min: 3800, after_flow_m3_min: 3610, switch_source: "simulation", flow_source: "simulation", switch_asset_id: null, flow_asset_id: null, }; async function setupThreeInputLab(page) { const asset = { id: "sample-pdf", filename: "通风机性能测定报告.pdf", kind: "pdf", source: "sample", size: 11775998, url: "/pdf", preview_url: "/pdf", }; await page.route("**/api/test-lab/catalog?*", (route) => route.fulfill({ json: { agents: [ { name: "main_fan", display_name: "矿井主通风机研判智能体", presets: [], }, ], assets: [asset], main_fan_simulation: simulationDefaults, }, }), ); await page.route("**/api/test-lab/runs?*", (route) => route.fulfill({ json: { items: [] } }), ); await page.goto("/static/index.html#/test"); await expect(page.getByLabel("切换间隔(分钟)")).toHaveValue("6"); } test("three input cards send adjustable inputs through the complete agent on every run", async ({ page, }) => { await setupThreeInputLab(page); await expect(page.locator(".fan-input-card")).toHaveCount(3); await expect(page.locator(".preset-card")).toHaveCount(0); await expect(page.getByLabel("选择 通风机性能测定报告.pdf")).toBeChecked(); await expect(page.locator(".execution-modes")).toHaveCount(0); await page.getByLabel("切换间隔(分钟)").fill("6.5"); await page.getByLabel("切换后风量(m³/min)").fill("3344"); await expect( page.locator('[data-testid="switch-input"] svg[role="img"]'), ).toHaveAttribute("aria-label", /6.5分钟/); await expect(page.locator('[data-testid="flow-input"]')).toContainText( "12.00%", ); const submitted = []; await page.route("**/api/test-lab/runs?*", async (route) => { if (route.request().method() !== "POST") return route.fulfill({ json: { items: [] } }); const body = route.request().postDataJSON(); submitted.push(body); return route.fulfill({ status: 202, json: { id: "run-" + submitted.length, label: body.label, agent_name: "main_fan", status: "completed", execution_mode: "agent", created_at: "2026-10-15 12:00:00", finished_at: "2026-10-15 12:00:01", request: { submitted: body, assets: [] }, artifacts: [], stages: [], result: { result: "本次报告", platform_payload: { judgment: { resultCode: "WARNING" } }, }, }, }); }); await page.getByRole("button", { name: /运行测试/ }).click(); await expect(page.locator(".result-heading .result-pill")).toHaveText( "WARNING", ); await expect(page.getByRole("button", { name: /运行测试/ })).toBeEnabled(); await page.getByLabel("切换后风量(m³/min)").fill("3610"); await page.getByRole("button", { name: /运行测试/ }).click(); await expect.poll(() => submitted.length).toBe(2); expect(submitted[0].execution_mode).toBe("agent"); expect(submitted[0].input).toEqual({}); expect(submitted[0].asset_ids).toEqual(["sample-pdf"]); expect(submitted[0].main_fan_simulation.switch_interval_minutes).toBe(6.5); expect(submitted[0].main_fan_simulation.after_flow_m3_min).toBe(3344); expect(submitted[1].main_fan_simulation.after_flow_m3_min).toBe(3610); expect(submitted[0].request_id).not.toBe(submitted[1].request_id); }); test("three input mobile layout requires selected JSON when switching sources", async ({ page, }) => { await page.setViewportSize({ width: 390, height: 844 }); await setupThreeInputLab(page); await page .locator('[data-testid="switch-input"]') .getByRole("button", { name: "选择 JSON", exact: true }) .click(); await expect(page.getByRole("button", { name: /运行测试/ })).toBeDisabled(); await expect(page.locator(".run-note")).toContainText("请选择切换曲线JSON"); await page .locator('[data-testid="switch-input"]') .getByRole("button", { name: "设置模拟曲线", exact: true }) .click(); await expect(page.getByRole("button", { name: /运行测试/ })).toBeEnabled(); expect( await page.evaluate( () => document.documentElement.scrollWidth > innerWidth, ), ).toBe(false); });