test: add unit/integration/E2E test suites, fix create_session bug, update docs

- Unit tests: test_session.py (27), test_error_kb.py (24), test_agent.py hardened
- Integration tests: test_api_integration.py (25) with FastAPI TestClient
- E2E tests: main-flows.spec.ts (8) with Playwright + API mocking
- Bug fix: backend/session.py create_session() missing session_id parameter
- Config: frontend/playwright.config.ts, npm run test:e2e
- Docs: update CLAUDE.md v9, .gitignore for test artifacts/eval reports
This commit is contained in:
2026-05-23 08:38:29 +08:00
parent b444303055
commit 1952d75f13
11 changed files with 1029 additions and 12 deletions
+64
View File
@@ -12,6 +12,7 @@
"vue": "^3.5.34"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^24.12.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
@@ -135,6 +136,22 @@
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@playwright/test": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz",
"integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz",
@@ -1104,6 +1121,53 @@
}
}
},
"node_modules/playwright": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
"integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.60.0"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.60.0",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
"integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=18"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.15",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
+3 -1
View File
@@ -6,13 +6,15 @@
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test:e2e": "playwright test"
},
"dependencies": {
"pinia": "^3.0.4",
"vue": "^3.5.34"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@types/node": "^24.12.3",
"@vitejs/plugin-vue": "^6.0.6",
"@vue/tsconfig": "^0.9.1",
+20
View File
@@ -0,0 +1,20 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
timeout: 60000,
expect: { timeout: 10000 },
retries: 0,
use: {
baseURL: "http://localhost:5173",
headless: true,
screenshot: "only-on-failure",
trace: "retain-on-failure",
},
webServer: {
command: "npm run dev",
url: "http://localhost:5173",
reuseExistingServer: true,
timeout: 30000,
},
});
+168
View File
@@ -0,0 +1,168 @@
/**
* 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: "<jasperReport/>" },
},
});
}
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();
});
});