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
+41
View File
@@ -0,0 +1,41 @@
# Playwright 三 Agent 应用计划
## 目标
**Generator → Planner → Healer** 三个 Agent 完整走通一次人才测评的自动化测试。
## 阶段设计
```
Phase 1: Generator 录制
Phase 2: Planner 编排(本次核心)
Phase 3: Healer 验证
```
## 本次测试内容
人才测评系统端到端考核流程:
1. 登录页面 → 输入账号密码 → 提交
2. 进入考核页 → 确认两个模板可见
3. 选择技术人员模板 → 点开始评估
4. 等题目出现 → 答选择题
5. 答简答题 → 处理追问
6. 完成考核 → 查看结果
7. 管理员登录 → 设置页 → 查看测评模板配置
## 工具链
| Agent | 命令 |
|-------|------|
| Generator | `npx playwright codegen http://localhost:13001` |
| Planner | `@playwright/test` 框架 + `defineConfig` |
| Healer | `trace: 'on-first-retry'` + `retries: 2` |
## 输出产物
- `playwright.config.ts` — 框架配置
- `tests/assessment.e2e.spec.ts` — 主测试套件
- 运行结果 + Trace 文件
+38
View File
@@ -0,0 +1,38 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Playwright 配置 — 三 Agent 集成
* Planner: 测试结构 + 并行执行 + HTML 报告
* Healer: 自动重试 + Trace 快照
* Generator: 通过 codegen 命令配合使用
*/
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 1, // ← Healer: 失败自动重试
workers: process.env.CI ? 1 : 3, // ← Planner: 并行执行
reporter: [
['html', { outputFolder: 'playwright-report' }], // ← Planner: HTML 报告
['list'], // ← Planner: 控制台实时输出
],
// ← Healer: 失败时保存 Trace 和截图
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'on-first-retry',
timeout: 120000, // 单测超时 2 分钟
expect: { timeout: 10000 },
use: {
baseURL: 'http://localhost:13001',
headless: true,
viewport: { width: 1440, height: 900 },
ignoreHTTPSErrors: true,
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
],
});
+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 });
}
});
});
});