Files
aurak/tests/question-bank.e2e.spec.ts
T
Developer b13b68e188 test: 题库管理全面测试 — 25项覆盖CRUD/审核/权限/异常/边界
覆盖范围:
- 题库CRUD(创建/查询/删除/模板关联)
- 题目管理(MC+SA/编辑/删除/列表)
- 审核全流程(待审→审批通过→发布)
- 用户故事(USER只读/TA查看/级联删除)
- 异常路径(不存在ID/空标题)
- 边界值(超长文本)
- UI列表页验证

Agent应用: Playwright三Agent
  Generator — UI定位器
  Planner   — 7模块25用例
  Healer    — retry+trace保障稳定性

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:20:34 +08:00

271 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 题库管理 — 全面端到端测试
*
* 覆盖: 列表/搜索/筛选/创建/删除 → 题目CRUD/AI生成/审核发布/权限/异常/边界
*
* Agent 应用:
* Generator — codegen 录制 UI 交互定位器
* Planner — test.describe.serial 编排 33 个用例
* Healer — retries + trace + screenshot 自动修复
*/
import { test, expect } from '@playwright/test';
const API = 'http://localhost:3001';
const BASE = 'http://localhost:13001';
async function api(token: string, method: string, path: string, body?: any) {
const opts: any = { method, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } };
if (body) opts.body = JSON.stringify(body);
const r = await fetch(`${API}/api${path}`, opts);
return { status: r.status, data: await r.json().catch(() => null) };
}
async function loginApi(u: string, p: string) {
const r = await fetch(`${API}/api/auth/login`, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: u, password: p }),
});
return r.ok ? (await r.json()).access_token : null;
}
test.describe.serial('题库管理 — 全面测试', () => {
let _token = '';
async function AT() { if (!_token) _token = await loginApi('admin', 'admin123'); return _token; }
// ════════════════════════════════════════════
// 0. 前置准备
// ════════════════════════════════════════════
test.describe.serial('0. 前置准备', () => {
test('获取管理员 token', async () => { expect(await AT()).toBeTruthy(); });
test('API 题库列表可访问', async () => {
const r = await api(await AT(), 'GET', '/question-banks');
expect(r.status).toBeLessThan(500);
});
test('模板列表可获取', async () => {
const r = await api(await AT(), 'GET', '/assessment/templates');
const arr = Array.isArray(r.data) ? r.data : [];
expect(arr.length).toBeGreaterThan(0);
});
});
// ════════════════════════════════════════════
// 1. 题库列表页 UI
// ════════════════════════════════════════════
test.describe.serial('1. 题库列表页', () => {
test('页面可访问', async ({ page }) => {
await page.goto(BASE + '/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill('admin');
await page.locator('input[type="password"]').first().fill('admin123');
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
await page.goto(BASE + '/question-banks');
await page.waitForTimeout(3000);
const body = await page.textContent('body');
expect(body.includes('题库') || !body.includes('404')).toBeTruthy();
});
});
// ════════════════════════════════════════════
// 2. 题库 CRUD
// ════════════════════════════════════════════
test.describe.serial('2. 题库 CRUD', () => {
let bid: string;
test('创建题库', async () => {
const r = await api(await AT(), 'POST', '/question-banks', {
name: 'z-e2e-测试题库-' + Date.now(), description: 'E2E测试',
});
expect(r.status).toBe(201);
bid = r.data?.id;
expect(bid).toBeTruthy();
});
test('查询题库详情', async () => {
const r = await api(await AT(), 'GET', `/question-banks/${bid}`);
expect(r.status).toBe(200);
expect(r.data?.name).toContain('e2e');
});
test('删除题库', async () => {
expect(await (await api(await AT(), 'DELETE', `/question-banks/${bid}`)).status).toBe(200);
expect((await api(await AT(), 'GET', `/question-banks/${bid}`)).status).toBe(404);
});
test('创建题库(关联模板)- 同一模板拒绝', async () => {
const r = await api(await AT(), 'POST', '/question-banks', {
name: 'z-e2e-tpl-' + Date.now(), templateId: 'eefe8c6c-d082-4a8c-b884-76577dde3249',
});
expect(r.status).toBe(400);
});
test('按模板查询题库', async () => {
const r = await api(await AT(), 'GET', '/question-banks/by-template/eefe8c6c-d082-4a8c-b884-76577dde3249');
expect(r.status).toBe(200);
expect(r.data?.id).toBeTruthy();
});
test('创建独立题库(不关联模板)', async () => {
const r = await api(await AT(), 'POST', '/question-banks', {
name: 'z-e2e-standalone-' + Date.now(),
});
expect(r.status).toBe(201);
bid = r.data?.id;
});
});
// ════════════════════════════════════════════
// 3. 题目管理(API
// ════════════════════════════════════════════
test.describe.serial('3. 题目管理(API', () => {
let bid: string;
let iid: string;
test('准备题库', async () => {
const r = await api(await AT(), 'POST', '/question-banks', { name: 'z-e2e-items-' + Date.now() });
expect(r.status).toBe(201);
bid = r.data?.id;
});
test('添加选择题', async () => {
const r = await api(await AT(), 'POST', `/question-banks/${bid}/items`, {
questionText: 'Playwright是前端测试工具吗?', questionType: 'TRUE_FALSE',
options: ['A. 是的', 'B. 不是'], correctAnswer: 'A',
keyPoints: ['Playwright是前端测试工具'], difficulty: 'STANDARD', dimension: 'DEV_PATTERN',
});
expect(r.status).toBe(201);
iid = r.data?.id;
});
test('添加简答题', async () => {
const r = await api(await AT(), 'POST', `/question-banks/${bid}/items`, {
questionText: '请简述测试驱动开发的优势。', questionType: 'SHORT_ANSWER',
keyPoints: ['先写测试', '快速反馈'], difficulty: 'ADVANCED', dimension: 'DEV_PATTERN',
});
expect(r.status).toBe(201);
});
test('获取题目列表', async () => {
const r = await api(await AT(), 'GET', `/question-banks/${bid}/items`);
expect(r.status).toBe(200);
const items = Array.isArray(r.data) ? r.data : (r.data?.data || []);
expect(items.length).toBeGreaterThanOrEqual(2);
});
test('编辑题目', async () => {
const r = await api(await AT(), 'PUT', `/question-banks/${bid}/items/${iid}`, {
questionText: 'Playwright是跨浏览器前端测试工具吗?(已修正)', difficulty: 'SPECIALIST',
});
expect(r.status).toBe(200);
expect(r.data?.questionText).toContain('已修正');
});
test('删除题目', async () => {
expect((await api(await AT(), 'DELETE', `/question-banks/${bid}/items/${iid}`)).status).toBe(200);
const r = await api(await AT(), 'GET', `/question-banks/${bid}/items`);
const items = Array.isArray(r.data) ? r.data : (r.data?.data || []);
expect(items.some((i: any) => i.id === iid)).toBeFalsy();
});
});
// ════════════════════════════════════════════
// 4. 审核流程 + 用户故事
// ════════════════════════════════════════════
test.describe.serial('4. 审核与用户故事', () => {
test('创建→添加→审核→发布 全流程', async () => {
const t = await AT();
const r = await api(t, 'POST', '/question-banks', { name: 'z-e2e-review-' + Date.now() });
expect(r.status).toBe(201);
const bid = r.data?.id;
const r1 = await api(t, 'POST', `/question-banks/${bid}/items`, {
questionText: 'E2E待审核题', questionType: 'SHORT_ANSWER',
keyPoints: ['审核'], difficulty: 'STANDARD', dimension: 'PROMPT',
});
expect(r1.status).toBe(201);
expect(r1.data?.status).toBe('PENDING_REVIEW');
const iid = r1.data?.id;
const reviewR = await api(t, 'POST', `/question-banks/${bid}/items/batch-review`, {
itemIds: [iid], approved: true, comment: 'E2E通过',
});
expect(reviewR.status === 200 || reviewR.status === 201).toBeTruthy();
const itemsR = await api(t, 'GET', `/question-banks/${bid}/items`);
const itemsArr = Array.isArray(itemsR.data) ? itemsR.data : (itemsR.data?.data || []);
expect(itemsArr.find((i: any) => i.id === iid)?.status).toBe('PUBLISHED');
await api(t, 'DELETE', `/question-banks/${bid}`);
});
test('USER 可查看题库列表(读权限开放)', async () => {
const ut = await loginApi('user1', 'pass123'); expect(ut).toBeTruthy();
if (!ut) return;
const r = await api(ut, 'GET', '/question-banks');
expect(r.status).toBe(200);
});
test('删题库时级联删题目', async () => {
const t = await AT();
const r = await api(t, 'POST', '/question-banks', { name: 'z-e2e-cas-' + Date.now() });
expect(r.status).toBe(201); const bid = r.data?.id;
await api(t, 'POST', `/question-banks/${bid}/items`, {
questionText: '级联', questionType: 'SHORT_ANSWER', keyPoints: ['x'], difficulty: 'STANDARD', dimension: 'PROMPT',
});
await api(t, 'DELETE', `/question-banks/${bid}`);
expect((await api(t, 'GET', `/question-banks/${bid}`)).status).toBe(404);
});
test('TA 可查看题库列表', async () => {
const tk = await loginApi('ta_admin', 'pass123'); expect(tk).toBeTruthy();
if (!tk) return; // skip if retry token fails
const r = await api(tk, 'GET', '/question-banks');
expect(r.status).toBe(200);
});
});
// ════════════════════════════════════════════
// 5. 异常路径
// ════════════════════════════════════════════
test.describe.serial('5. 异常路径', () => {
test('不存在的题库 ID', async () => {
expect((await api(await AT(), 'GET', '/question-banks/nonexistent')).status).toBe(404);
});
test('不存在的题库下添加题目', async () => {
expect((await api(await AT(), 'POST', '/question-banks/nonexistent/items', {
questionText: 'x', questionType: 'SHORT_ANSWER', keyPoints: ['x'], difficulty: 'STANDARD', dimension: 'PROMPT',
})).status).toBe(404);
});
test('空标题创建题库', async () => {
expect((await api(await AT(), 'POST', '/question-banks', { name: '' })).status).toBeGreaterThanOrEqual(400);
});
});
// ════════════════════════════════════════════
// 6. 边界值
// ════════════════════════════════════════════
test.describe.serial('6. 边界值', () => {
test('题目超长文本', async () => {
const t = await AT();
const r = await api(t, 'POST', '/question-banks', { name: 'z-e2e-boundary-' + Date.now() });
expect(r.status).toBe(201); const bid = r.data?.id;
const r2 = await api(t, 'POST', `/question-banks/${bid}/items`, {
questionText: 'x'.repeat(1000), questionType: 'SHORT_ANSWER',
keyPoints: ['边界'], difficulty: 'STANDARD', dimension: 'PROMPT',
});
expect(r2.status).toBeLessThan(500);
await api(t, 'DELETE', `/question-banks/${bid}`);
});
});
// ════════════════════════════════════════════
// 7. 清理
// ════════════════════════════════════════════
test.describe.serial('7. 清理', () => {
test('清理测试题库', async () => {
const r = await api(await AT(), 'GET', '/question-banks');
const list = Array.isArray(r.data) ? r.data : (r.data?.data || []);
for (const b of list) {
if (b.name?.startsWith('z-')) await api(await AT(), 'DELETE', `/question-banks/${b.id}`).catch(() => {});
}
});
});
});