开篇前端测试脚本的维护成本正在吞噬团队交付效率。React 组件库每增加一个功能Playwright 端到端测试就要手动编写选择器、等待策略、断言逻辑。一个小型组件库20组件的测试脚本维护量半年内可达 5000 行以上。LLM 生成脚本看似能解放人力但直接产出往往充斥虚假选择器、缺失waitForSelector、类型错误导致超 60% 的生成代码无法通过一次运行。本文结合 Prompt 工程与 TypeScript AST 解析提出一套可落地的自动生成→校验→修复流水线将生成代码的可用率从 35% 提升至 82%且执行时间仅增加 12%。1. 测试生成的核心痛点1.1 手动编写的重复性Playwright 端到端测试中80% 的代码是 find element → interact → assert 三板斧。以日历组件为例选择日期、切换月份、验证选中高亮不同测试只有选择器和期望值不同结构高度重复。人工编写容易遗漏异常路径如跨月选择、禁用态判断。1.2 LLM 生成的幻觉与不可用直接让 GPT-4 生成 Playwright 脚本常见问题幻觉类型表现出现频率实验 100 次存在选择器使用#my-button但组件实际渲染为button[class*primary]41%缺少等待直接调用page.click未等待元素出现33%类型不匹配const el await page.$(.cls)后直接el.textContent但 el 可能 null27%断言错误使用assert.equal而非expectPlaywright 断言19%这些问题归因于 LLM 对运行时 DOM 结构和 Playwright 最佳实践的理解存在偏差。2. Prompt 工程技巧结构化少数样本2.1 结构化 Prompt 设计将 Prompt 拆为三部分组件上下文 交互步骤 输出模板。## 组件信息 - 组件名: DatePicker - 选择器: 日期格子在 .day-cell 内当前月份为 .month-title - 交互模式: 点击 .day-cell 选中按 .nav-next 切换下月 ## 测试步骤 1. 打开 DatePicker 示例页 /date-picker 2. 点击下月按钮 3. 选择 15 号格子 4. 验证选中的日期值为 2025-02-15 ## 输出格式必须严格按照 typescript import { test, expect } from playwright/test; test($testName, async ({ page }) { await page.goto($url); // 等待组件渲染 await page.waitForSelector($containerSelector); // 具体步骤 $steps });**关键点**显式写明需要 waitForSelector并给出选择器前缀约束如 .day-cell。对交互步骤加上顺序编号减少 LLM 随意插入步骤的幻觉。 ### 2.2 Few-shot 示例增强 每个组件类按钮、输入框、模态框预先准备 3~5 个完整示例作为 Prompt 前缀。示例中包含 - 正确使用 page.waitForSelector page.click - 使用 locator 链式调用而非 page.$ - 断言使用 expect(locator).toHaveText 实测添加 3 个示例后选择器正确率从 45% 提升至 71%。 ### 2.3 温度与约束 - 温度 temperature: 0.1 减少随机性 - 启用 response_format: { type: json_object } 让 LLM 输出结构化 JSON便于后续解析Playwright 代码可放在 JSON 字段内 - 设置 max_tokens 为 2048防止过长代码被截断 --- ## 3. AST 解析与自动修复 ### 3.1 为什么需要 AST 正则或字符串替换无法理解代码结构。Playwright 脚本可能包含嵌套的 async、条件判断、.waitForSelector 位置错误。使用 TypeScript ASTtypescript-eslint ts-morph可以 - 检测缺失的 await未 await 的 page.click 会导致竞态 - 检查 page.$ 调用后是否有 null 判断或在链式 .waitFor 前是否强制等待 - 识别选择器字符串是否包在 page.locator() 中 ### 3.2 修复流程输入 LLM 生成代码 → 解析 AST → 规则检查 → 生成 patches → 打印修复报告示例修复规则 typescript // 修复规则 1: 将 page.$ 替换为 locator const $calls sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression) .filter(c c.getExpression().getText() page.$); for (const call of $calls) { const arg call.getArguments()[0]; const locatorCall page.locator(${arg.getText()}); call.replaceWithText(locatorCall); } // 规则 2: 在 page.click 前插入 waitForSelector如果不存在 const clickCalls sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression) .filter(c c.getText().includes(.click()); for (const click of clickCalls) { const enclosingTest click.getFirstAncestorByKind(SyntaxKind.ArrowFunction); if (!enclosingTest) continue; const body enclosingTest.getBody(); const clickLine click.getStartLineNumber(); // 检查本测试块中是否有 waitForSelector 在本行之前 const hasWait body.getDescendantsOfKind(SyntaxKind.CallExpression) .some(w w.getText().includes(waitForSelector) w.getStartLineNumber() clickLine); if (!hasWait click.getText().match(/\.click\(/)) { const selector extractSelectorFromClick(click); // 从实参解析 if (selector) { click.insertBeforeText(\n await page.waitForSelector(${selector}, { timeout: 5000 });\n ); } } }3.3 等待策略自动注入使用 AST 分析page.goto后没有waitForNavigation或waitForLoadState时自动添加// 在 goto 下一行插入 await page.waitForLoadState(networkidle);实测数据在 50 个生成脚本中AST 修复前平均失败率 64%修复后下降至 18%。4. Playwright 最佳实践落地4.1 强制稳定的定位与等待生成代码最终通过一个封装层export async function safeClick(page: Page, locator: Locator, timeout 5000) { await locator.waitFor({ state: visible, timeout }); await locator.click(); } // 使用示例 const cell page.locator(.day-cell).nth(14); await safeClick(page, cell);4.2 截图对比用于视觉回归在断言后自动加上 screenshot 对比需配置基准图await page.screenshot({ path: screenshots/${testName}.png }); expect(await page.screenshot()).toMatchSnapshot(${testName}.png);4.3 网络空闲等待与重试LLM 生成的await page.waitForTimeout(2000)是反模式。替换为await page.waitForLoadState(networkidle); // 若组件为动态加载配合 await expect(page.locator(.loading-spinner)).toBeHidden({ timeout: 10000 });4.4 生成脚本示例import { test, expect } from playwright/test; import { safeClick } from ./helpers; test(DatePicker selects next month 15th, async ({ page }) { await page.goto(/date-picker); await page.waitForLoadState(networkidle); const nextBtn page.locator(.nav-next); await safeClick(page, nextBtn); const targetCell page.locator(.day-cell[data-day15]); await safeClick(page, targetCell); await expect(page.locator(#selected-date)).toHaveValue(2025-02-15); // 基线截图 expect(await page.screenshot()).toMatchSnapshot(datepicker-feb-15.png); });5. 效果评估与迭代5.1 对比实验设计选取真实 React 组件库Ant Design 5.x中的 6 个复杂组件DatePicker、Table、Form、Modal、TreeSelect、Upload。每组手工编写以及 LLMAST 生成各 10 个测试用例共计 120 个脚本。环境Playwright 1.45Node 20LLM 使用 GPT-4-turbo。5.2 结果维度手工编写LLMAST 生成差异平均单脚本耗时人分钟150.5生成 1.2修复验证降低 88%首次运行通过率100%调试后82%-18pp选择器正确率-89%-遗漏等待-7%-虚假断言-4%-执行时间秒12.3 ± 2.113.8 ± 2.512%累计维护成本20周60人天8人天初始构建 2人天修正减少 83%5.3 迭代反馈针对失败案例 (18%) 做了根因分析- 6% 是因为组件内部状态依赖如 Modal 动画未结束修复规则增强在clickModal trigger 后自动插入waitForSelector(.ant-modal)。- 4% 是因为 LLM 生成了不存在的 CSS 类名如.popup实际为.ant-popover我们补充了组件选择器映射表到 Prompt 中同时 AST 检测时如果waitForSelector超时回退使用page.locator(textxxx)模糊匹配。- 8% 是因为 AST 修复中漏处理了await修正了规则遍历顺序。迭代后生成脚本首次运行通过率升至 89%。结语Prompt 工程 AST 解析的组合策略将 AI 生成测试脚本从“玩具”推向“生产可用”。关键在于结构化 Prompt 降低幻觉AST 自动修复填补 LLM 对运行时行为的缺失Playwright 最佳实践封装保证稳定。建议团队在实际落地时先给 LLM 提供组件选择器映射表如 JSON 配置再针对库的动画和异步加载特性定制修复规则。每两周收集一次失败日志并更新 Prompt 和修复集可逐步将通过率稳定在 95% 以上。不要期待完全零人工干预——将修复时间控制在单脚本 1-2 分钟已经是大规模提效。