feat: Playwright 三Agent应用 — Generator→Planner→Healer 完整流水线

三Agent流程:
1. Generator: codegen 录制操作生成测试代码草稿
2. Planner: @playwright/test 框架编排 8 个测试用例
   - describe/test/expect 结构化
   - playwright.config.ts 并行执行 + HTML 报告
3. Healer: trace + retries + screenshot 自动修复
   - 失败自动重试 2 次
   - 首次重试时保存 trace.zip
   - 生成 playwright-report/ 可视化报告

测试结果: 8/8 passed (23秒)
产物: test-results/ 含 trace.zip, playwright-report/ HTML报告

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-06-16 11:03:21 +08:00
parent 07308cae99
commit 100aaa3880
3 changed files with 341 additions and 0 deletions
+262
View File
@@ -0,0 +1,262 @@
import { test, expect } from '@playwright/test';
/**
* 人才测评系统 — 端到端测试
*
* Generator → 录制操作
* Planner → 编排测试结构 (describe + test + expect)
* Healer → 自动重试 + Trace 快照 (通过 playwright.config.ts 配置)
*/
const ADMIN = { username: 'admin', password: 'admin123' };
// ── 辅助函数 ──
/** 通过 native setter 触发 React onChange */
async function fillReactInput(page: any, selector: string, text: string) {
await page.waitForSelector(selector, { timeout: 10000 });
await page.evaluate(({ sel, txt }: { sel: string; txt: string }) => {
const el = document.querySelector(sel);
if (!el) return;
const tag = el.tagName.toLowerCase();
const setter = Object.getOwnPropertyDescriptor(
tag === 'textarea' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype,
'value'
)?.set;
setter?.call(el, txt);
el.dispatchEvent(new Event('input', { bubbles: true }));
}, { sel: selector, txt: text });
}
/** 等待 spinner 消失 */
async function waitForIdle(page: any) {
await page.waitForFunction(() => !document.querySelector('.animate-spin'), { timeout: 90000 }).catch(() => {});
await page.waitForTimeout(1500);
}
/** 关闭可能存在的确认弹窗 */
async function dismissModal(page: any) {
await page.evaluate(() => {
document.querySelectorAll('[class*="fixed"][class*="inset-0"]').forEach(el => {
const btn = Array.from(el.querySelectorAll('button')).find(b =>
b.textContent?.includes('继续答题')
);
if (btn) btn.click();
});
});
}
// ══════════════════════════════════════════
// 测试套件
// ══════════════════════════════════════════
test.describe('人才测评系统 — 端到端验证', () => {
// ── 1. 登录 ──
test.describe('1. 登录', () => {
test('admin 登录成功,页面跳转到工作台', async ({ page }) => {
await page.goto('/login');
await page.waitForTimeout(1000);
await expect(page.locator('input[type="text"]').first()).toBeVisible();
await page.locator('input[type="text"]').first().fill(ADMIN.username);
await page.locator('input[type="password"]').first().fill(ADMIN.password);
await page.locator('button[type="submit"]').click();
// 登录成功后 URL 不再是 /login
await page.waitForURL('**/');
expect(page.url()).not.toContain('/login');
});
test('错误密码显示错误提示', async ({ page }) => {
await page.goto('/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill('admin');
await page.locator('input[type="password"]').first().fill('wrongpass');
await page.locator('button[type="submit"]').click();
await page.waitForTimeout(2000);
// 应该还停留在登录页且有错误提示
expect(page.url()).toContain('/login');
});
});
// ── 2. 考核模板 ──
test.describe('2. 考核模板', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill(ADMIN.username);
await page.locator('input[type="password"]').first().fill(ADMIN.password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
});
test('两个考核模板均可见', async ({ page }) => {
await page.goto('/assessment');
await page.waitForTimeout(3000);
const techTpl = page.locator('button:has-text("AI协作技巧-对话测评")');
const nonTechTpl = page.locator('button:has-text("AI协作-非技术人员测评")');
await expect(techTpl.first()).toBeVisible({ timeout: 10000 });
await expect(nonTechTpl.first()).toBeVisible({ timeout: 10000 });
});
test('选择模板后显示开始评估按钮', async ({ page }) => {
await page.goto('/assessment');
await page.waitForTimeout(3000);
await page.locator('button:has-text("AI协作技巧-对话测评")').first().click();
await page.waitForTimeout(500);
const startBtn = page.locator('button:has-text("开始专业评估")');
await expect(startBtn).toBeVisible({ timeout: 5000 });
});
});
// ── 3. 选择题答题 ──
test.describe('3. 选择题答题', () => {
test('点击选择后确认答案按钮可用', async ({ page }) => {
// 登录
await page.goto('/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill(ADMIN.username);
await page.locator('input[type="password"]').first().fill(ADMIN.password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
// 开始考核
await page.goto('/assessment');
await page.waitForTimeout(2000);
await page.locator('button:has-text("AI协作技巧-对话测评")').first().click();
await page.waitForTimeout(500);
await page.locator('button:has-text("开始专业评估")').first().click();
// 等题目
for (let i = 0; i < 60; i++) {
const text = await page.textContent('body').catch(() => '');
if (text.includes('问题 ') || text.includes('Question ')) break;
await new Promise(r => setTimeout(r, 2000));
}
await waitForIdle(page);
// 检查是否是选择题
const hasChoice = await page.evaluate(() => {
return document.querySelectorAll('button.w-full.text-left.px-5.py-4').length > 0;
});
if (hasChoice) {
await dismissModal(page);
// 用 evaluate 点击选项
await page.evaluate(() => {
const opts = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4'))
.filter(b => /^[A-D]/.test(b.textContent || ''));
if (opts.length > 0) (opts[0] as HTMLButtonElement).click();
});
await page.waitForTimeout(500);
const confirmBtn = page.locator('button:has-text("确认答案")');
await expect(confirmBtn).toBeEnabled({ timeout: 5000 });
} else {
// 如果是简答题,测试 textarea 可见
const ta = page.locator('textarea');
await expect(ta.first()).toBeVisible({ timeout: 5000 });
}
});
});
// ── 4. 简答题输入 ──
test.describe('4. 简答题输入', () => {
test('简答题 textarea 可输入并发送', async ({ page }) => {
// 登录 + 开始考核
await page.goto('/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill(ADMIN.username);
await page.locator('input[type="password"]').first().fill(ADMIN.password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
await page.goto('/assessment');
await page.waitForTimeout(2000);
await page.locator('button:has-text("AI协作技巧-对话测评")').first().click();
await page.waitForTimeout(500);
await page.locator('button:has-text("开始专业评估")').first().click();
for (let i = 0; i < 60; i++) {
const text = await page.textContent('body').catch(() => '');
if (text.includes('问题 ') || text.includes('Question ')) break;
await new Promise(r => setTimeout(r, 2000));
}
await waitForIdle(page);
await dismissModal(page);
// 检查如果是简答题
const hasSA = await page.evaluate(() => {
const ta = document.querySelector('textarea');
return ta !== null && ta.offsetParent !== null;
});
if (hasSA) {
await fillReactInput(page, 'textarea', '端到端测试回答 — 验证Playwright Planner编排功能');
await page.waitForTimeout(500);
// 发送按钮应可用
const sendBtn = page.locator('button:has(svg.lucide-send)');
await expect(sendBtn.last()).toBeEnabled({ timeout: 5000 });
} else {
// 选择题——选一个
await page.evaluate(() => {
const opts = Array.from(document.querySelectorAll('button.w-full.text-left.px-5.py-4'))
.filter(b => /^[A-D]/.test(b.textContent || ''));
if (opts.length > 0) (opts[0] as HTMLButtonElement).click();
});
await page.waitForTimeout(500);
const confirmBtn = page.locator('button:has-text("确认答案")');
await expect(confirmBtn).toBeEnabled({ timeout: 5000 });
}
});
});
// ── 5. 设置页验证 ──
test.describe('5. 设置页 — 测评模板', () => {
test('设置页有测评模板 Tab', async ({ page }) => {
await page.goto('/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill(ADMIN.username);
await page.locator('input[type="password"]').first().fill(ADMIN.password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
await page.goto('/settings');
await page.waitForTimeout(3000);
const templateTab = page.locator('button:has-text("测评模板")');
await expect(templateTab).toBeVisible({ timeout: 5000 });
});
test('测评模板 Tab 可点击', async ({ page }) => {
await page.goto('/login');
await page.waitForTimeout(500);
await page.locator('input[type="text"]').first().fill(ADMIN.username);
await page.locator('input[type="password"]').first().fill(ADMIN.password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
await page.goto('/settings');
await page.waitForTimeout(3000);
const templateTab = page.locator('button:has-text("测评模板")');
if (await templateTab.isVisible().catch(() => false)) {
await templateTab.click();
await page.waitForTimeout(2000);
// 点开模板列表应该能看到模板
const tplList = page.locator('text=AI协作技巧-对话测评');
await expect(tplList.first()).toBeVisible({ timeout: 5000 });
}
});
});
});