/** * E2E tests: key user flows for the JRXML Agent frontend. * * Pre-requisites: npm run dev (reuseExistingServer in playwright.config). * API calls are intercepted by page.route() — no real backend needed. */ import { test, expect } from "@playwright/test"; // ── helpers ──────────────────────────────────────────────────── function mockApi(page: any) { page.route("**/api/health", (route: any) => route.fulfill({ json: { status: "ok", version: "5.0" } }) ); page.route("**/api/sessions", (route: any) => { if (route.request().method() === "POST") { return route.fulfill({ json: { session_id: "test12345678", session_name: "新建报表 2026-05-22", created_at: "2026-05-22T10:00:00.000Z", updated_at: "2026-05-22T10:00:00.000Z", }, }); } return route.fulfill({ json: { sessions: [] } }); }); page.route("**/api/sessions/*/chat", (route: any) => { const sseBody = [ "event: node_start", 'data: {"node":"classify_intent","label":"识别意图","step_index":1}', "", "event: node_complete", 'data: {"node":"classify_intent","label":"识别意图","detail":"意图: 新建报表"}', "", "event: agent_complete", 'data: {"reason":"done","intent":"initial_generation","status":"pass","jrxml_length":42,"versions":1,"total_duration_ms":1200}', "", "", ].join("\n"); return route.fulfill({ status: 200, headers: { "content-type": "text/event-stream" }, body: sseBody, }); }); page.route("**/api/upload", (route: any) => route.fulfill({ json: { file_id: "f001122334455", filename: "test.png", size: 1024 }, }) ); // Catch-all for GET/DELETE /api/sessions/:id (must fallback for POST to let chat route match) page.route("**/api/sessions/**", (route: any) => { if (route.request().method() === "DELETE") { return route.fulfill({ json: { status: "deleted" } }); } if (route.request().method() === "GET") { return route.fulfill({ json: { session_id: "test12345678", session_name: "测试会话", agent_state: { current_jrxml: "" }, }, }); } return route.fallback(); }); } // ── tests ────────────────────────────────────────────────────── test.describe("Page load", () => { test("renders sidebar and input area", async ({ page }) => { await mockApi(page); await page.goto("/"); await expect(page.locator("aside.sidebar")).toBeVisible(); await expect(page.locator("h2")).toContainText("JRXML"); await expect(page.locator(".unified-input")).toBeVisible(); }); test("sidebar shows session list header and new button", async ({ page }) => { await mockApi(page); await page.goto("/"); await expect(page.getByText("会话列表")).toBeVisible(); await expect(page.locator('button[title="新建会话"]')).toBeVisible(); }); }); test.describe("Session management", () => { test("creates new session on button click", async ({ page }) => { await mockApi(page); await page.goto("/"); await page.locator('button[title="新建会话"]').click(); await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 }); await expect(page.locator(".session-item.active")).toBeVisible(); }); test("can delete current session", async ({ page }) => { await mockApi(page); await page.goto("/"); await page.locator('button[title="新建会话"]').click(); await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 }); page.on("dialog", (dialog) => dialog.accept()); await page.locator(".btn-delete").click(); await expect(page.locator(".session-item")).toHaveCount(0, { timeout: 5000 }); }); }); test.describe("Chat flow", () => { test("sends text and displays user message + process section", async ({ page }) => { await mockApi(page); await page.goto("/"); await page.locator('button[title="新建会话"]').click(); await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 }); const textarea = page.locator(".unified-input textarea"); await textarea.fill("生成一个员工名册报表"); await page.locator(".send-btn").click(); await expect( page.locator(".chat-messages .message.msg-user").filter({ hasText: "员工名册" }) ).toBeVisible({ timeout: 10000 }); await expect(page.locator(".process-section")).toBeVisible({ timeout: 10000 }); }); test("summary card appears after stream complete", async ({ page }) => { await mockApi(page); await page.goto("/"); await page.locator('button[title="新建会话"]').click(); await expect(page.locator(".session-item")).toBeVisible({ timeout: 5000 }); await page.locator(".unified-input textarea").fill("生成报表"); await page.locator(".send-btn").click(); await expect(page.locator(".summary-card")).toBeVisible({ timeout: 15000 }); }); }); test.describe("Input UX", () => { test("send button disabled when input empty", async ({ page }) => { await mockApi(page); await page.goto("/"); await expect(page.locator(".send-btn")).toBeDisabled(); }); test("send button enabled when text entered", async ({ page }) => { await mockApi(page); await page.goto("/"); await page.locator(".unified-input textarea").fill("Hi"); await expect(page.locator(".send-btn")).toBeEnabled(); }); }); // ── KB (Knowledge Base) API mocks ─────────────────────────────── function mockKbApi(page: any) { mockApi(page); page.route("**/api/users", (route: any) => { if (route.request().method() === "POST") { return route.fulfill({ json: { user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" }, }); } return route.fulfill({ json: { users: [{ user_id: "u_e2e_test_001", name: "E2E用户", created_at: "2026-05-23T00:00:00Z" }] }, }); }); page.route("**/api/users/*/kbs", (route: any) => { if (route.request().method() === "POST") { return route.fulfill({ json: { kb_id: "kb_e2e_001", user_id: "u_e2e_test_001", name: "E2E测试库", description: "", created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z", fields: [], templates: [], file_count: 0, chunk_count: 0, parse_status: "empty", }, }); } return route.fulfill({ json: { kbs: [{ kb_id: "kb_e2e_001", name: "E2E测试库", description: "", created_at: "2026-05-23T00:00:00Z", updated_at: "2026-05-23T00:00:00Z", field_count: 10, template_count: 3, file_count: 2, chunk_count: 50, parse_status: "ready", }], }, }); }); page.route("**/api/kbs/*/status", (route: any) => route.fulfill({ json: { parse_status: "ready", file_count: 2, chunk_count: 50 } }) ); page.route("**/api/kbs/*/fields", (route: any) => route.fulfill({ json: { fields: [{ name: "billNo", description: "工单号", type: "String" }], templates: [{ name: "结算单", file: "结算单.jrxml" }], }}) ); page.route("**/api/kbs/*", (route: any) => { if (route.request().method() === "DELETE") { return route.fulfill({ json: { status: "deleted" } }); } return route.fulfill({ json: { kb_id: "kb_e2e_001", user_id: "u_e2e_test_001", name: "E2E测试库", description: "", fields: [], templates: [], file_count: 2, chunk_count: 50, parse_status: "ready", }, }); }); page.route("**/api/kbs/*/upload", (route: any) => route.fulfill({ json: { filename: "test.jrxml", type: "jrxml", error: null } }) ); page.route("**/api/sessions/*/kb", (route: any) => { if (route.request().method() === "PUT") { return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } }); } return route.fulfill({ json: { kb_id: "kb_e2e_001", kb_name: "E2E测试库" } }); }); } // ── KB feature tests ──────────────────────────────────────────── test.describe("KB selector", () => { test("KB selector renders in chat interface", async ({ page }) => { await mockKbApi(page); await page.goto("/"); await expect(page.locator(".kb-selector")).toBeVisible({ timeout: 5000 }); await expect(page.locator(".kb-label")).toContainText("知识库"); await expect(page.locator(".kb-select")).toBeVisible(); await expect(page.locator(".kb-manage-btn")).toBeVisible(); }); test("can select a KB from dropdown", async ({ page }) => { await mockKbApi(page); await page.goto("/"); const select = page.locator(".kb-select"); await expect(select).toBeVisible({ timeout: 5000 }); await select.selectOption({ label: "E2E测试库 (10字段, 3模板)" }); await expect(page.locator(".kb-badge")).toContainText("E2E测试库"); }); }); test.describe("KB manager", () => { test("opens KB manager overlay", async ({ page }) => { await mockKbApi(page); await page.goto("/"); await page.locator(".kb-manage-btn").click(); await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 }); await expect(page.locator(".kb-manager h3")).toContainText("知识库管理"); }); test("can close KB manager", async ({ page }) => { await mockKbApi(page); await page.goto("/"); await page.locator(".kb-manage-btn").click(); await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 }); await page.locator(".close-btn").click(); await expect(page.locator(".kb-manager")).not.toBeVisible({ timeout: 3000 }); }); test("create form has name input and create button", async ({ page }) => { await mockKbApi(page); await page.goto("/"); await page.locator(".kb-manage-btn").click(); await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 }); await expect(page.locator('.kb-manager .create-form .kb-input').first()).toBeVisible(); await expect(page.locator('.kb-manager .create-form button.primary')).toBeVisible(); }); test("KB cards show name, status, and actions", async ({ page }) => { await mockKbApi(page); await page.goto("/"); await page.locator(".kb-manage-btn").click(); await expect(page.locator(".kb-manager")).toBeVisible({ timeout: 3000 }); await expect(page.locator(".kb-card")).toBeVisible({ timeout: 3000 }); await expect(page.locator(".kb-card strong")).toContainText("E2E测试库"); await expect(page.locator(".kb-status.ready")).toContainText("就绪"); await expect(page.locator(".kb-actions button")).toHaveCount(3); }); }); test.describe("JRXML upload in chat", () => { test("file input accepts .jrxml extension", async ({ page }) => { await mockKbApi(page); await page.goto("/"); const input = page.locator('.unified-input input[type="file"]'); await expect(input).toHaveAttribute("accept", /\.jrxml/); }); });