| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- 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);
- });
|