前言
这是一份精简版本的 UI 自动化工程中 skill 的设计,比之前的四 Agent 版本更精简,速度更快,token 消耗更少。效果方面实测下来也没有消退多少。主要改动是以下两点:
- 把代码搜索、分析、实现都放到生成者里。把验证放到验证者里。只有两个 Agent 了,更加精简。
- 不再全量分析所有代码,而是在搜索时过滤出与需求相关的代码。
下面我先给出提示词,然后再详细讲解。
第一部分:完整提示词
1. 生成者(主 Agent,也就是我们跟 AI 辅助工具对话时的那个 Agent)负责代码分析与生成
这份是 Skill 的主文件,加载之后,主 Agent 就照着它干活。
---
name: ui-automation
description: 当用户需要创建、修改或调试 ADP UI 自动化测试用例时使用。当用户提到"写测试"、"创建测试用例"、"E2E 测试"等关键词时触发。加载后主 Agent 自己完成搜索、分析、实现,仅验证环节调度子 Agent。
allowed-tools:
disable: false
---
# ADP UI 自动化测试 Skill
> **加载此 Skill 后,你(主 Agent)自己完成代码搜索、分析、实现。仅在验证环节通过 Task 调度 adp-verifier。**
---
## 整体流程
1. 搜索代码库(codebase_search)
2. 读相邻 case 学风格
3. 输出分析表格 → 等用户确认
4. 写代码
5. Task 调度 adp-verifier → 判断结果
├─ ✅ 通过 → 结束
├─ 合规违规 → 自己修 → 回到步骤 5
└─ ❌ 测试失败 → 自己修 → 回到步骤 5(最多 2 次)
---
## 步骤 1:搜索代码库
使用 `codebase_search` 搜索与需求相关的代码:
- 搜功能描述(如"创建应用"、"切换模型"、"上传文档")
- 从结果中识别可复用的 Page 类、Service 类、Component 类
- 对强相关的类 `read_file` 看完整源码,判断能否复用
- 搜 2–3 次,覆盖需求涉及的主要功能点
---
## 步骤 2:确定测试目录 + 读相邻 Case
### 2a. 确定测试文件保存目录
- 用户已指定 → 使用用户指定的目录
- 用户未指定 → 根据需求推断:
| 需求类型 | 推荐目录 |
|---------|---------|
| Agent 应用配置 | `src/tests/app_dev/agent/config_test/` |
| 知识管理 | `src/tests/app_dev/agent/` |
| Widget | `src/tests/widget/` |
| 用户管理 | `src/tests/app_dev/enterprise/` |
| 插件广场 | `src/tests/plugin-market/` |
| 模型广场 | `src/tests/model-market/` |
| 提示词模板 | `src/tests/app_dev/prompt-template/` |
| 应用列表 | `src/tests/app_dev/app-list/` |
| 技能广场 | `src/tests/skill-market/` |
### 2b. 读相邻 Case
1. `list_dir` 列出目录下 `.test.ts` 文件
2. 选 1–2 个最相关的
3. `read_file` 读取完整内容,学习:Service 实例化方式、import 路径、测试数据生成、断言写法
---
## 步骤 3:输出分析表格(等用户确认)
**必须在写代码之前,向用户展示以下分析结果并等待确认:**
测试文件目录:src/tests/xxx/
参考相邻 case:xxx.test.ts
## 步骤分析
| 步骤 | 操作描述 | 复用判断 | 代码位置 |
|------|---------|---------|---------|
| 1 | 创建应用 | 复用 | AgentConfigService.createAgentApp() |
| 2 | 设置提示词 | 复用 | AgentConfigService.setPrompt() |
| 3 | 点击连接器 Tab | 扩展 | AppConfigPage 新增 clickConnectorTab() |
| 4 | 添加新连接器 | 新建 | 新建 ConnectorPage + ConnectorService |
| 5 | 验证列表 | ⚠️ 修改 | AppConfigPage.getList() 需改参数 |
测试文件:src/tests/xxx/yyy.test.ts(新建)
**复用判断标记:**
| 标记 | 含义 |
|------|------|
| 复用 | 现有代码完全覆盖,不需要改动 |
| 扩展 | 在现有文件中新增方法(不改已有方法) |
| 新建 | 需要创建新文件 |
| ⚠️ 修改 | 需要修改已有方法(必须用户逐项确认) |
**确认规则:**
- 用户确认后才开始写代码
- 标记为「⚠️ 修改」的项必须用户逐项批准
- 用户批准的记为 `approved_modifications`
- 用户拒绝的记为 `rejected_modifications`,使用替代策略(新增方法代替修改)
---
## 步骤 4:写代码
用户确认后,按四层架构生成代码。
**写代码前,先读 `.codebuddy/skills/ui-automation/midscene-api-reference.md`**,确认 AI API 方法选择优先级:能用 `aiTap` 的不用 `aiAction`,输入框一律用 `aiInput`(默认 replace 模式)。同时确认 `withFallback` 的 AI-Only 模板格式。
### ⚡ AI-Only 优先(核心原则)
首次实现的所有 Page 方法使用 AI-Only 定位:
async clickXxx() {
await this.withFallback({
label: "clickXxx()",
cssAction: async () => false,
aiFallback: async () => {
await this.getAgent().aiTap("[容器]中的[文案][元素类型]");
},
aiOnly: true,
});
await this.wait(500);
}
### AI 定位描述要求
| 要素 | 示例 |
|------|------|
| 容器上下文 | "在「添加用户」弹窗中" |
| 视觉位置 | "底部右侧的" |
| 元素文案 | "「确定」按钮" |
| 元素类型 | "按钮"、"输入框" |
### AI 查询缓存
所有 `aiQuery` 默认开启 `cacheable: true`。
### 测试数据命名规范
- 所有测试实体名称以 `_qta` 结尾
- 总长度 ≤ 15 字符
const suffix = String(Date.now()).slice(-6);
const appName = `a_${suffix}_qta`; // 12 字符
### 代码生成规范
**Page 层** — 定位逻辑(AI-Only):
async clickXxx() {
await this.withFallback({
label: "clickXxx()",
cssAction: async () => false,
aiFallback: async () => {
await this.getAgent().aiTap("[精确描述]");
},
aiOnly: true,
});
await this.wait(500);
}
**Service 层** — 仅编排 Page 方法,禁止定位逻辑:
async doSomething(param: string) {
await this.step(`操作: "${param}"`, async () => {
await this.xxxPage.clickXxx();
});
}
🚫 Service 层禁止:`this.page.evaluate`、`this.page.locator`、`this.page.click`、`.aiTap`、`.aiAction` 等。例外:`this.page.waitForTimeout()` 允许。
#### Mixin 新增方法必须同步接口声明
每个 Mixin 文件顶部都有一个 `XxxMixinMethods` 接口,通过 `as unknown as TBase & Constructor<XxxMixinMethods>` 导出。**在 `Mixed` 类中新增任何方法,必须同时在该接口里加上对应签名**,否则外部调用时 TypeScript 看不到该方法。
// ✅ 正确:接口声明 + 类实现同步
export interface ToolMixinMethods {
// ... 已有声明 ...
clickConnectorSectionMoreMenu(): Promise<void>; // ← 新增方法必须在这里声明
clickDeleteAllConnectors(): Promise<void>;
clickConfirmDeleteAllConnectors(): Promise<void>;
verifyConnectorNotInList(connectorName: string): Promise<boolean>;
}
// Mixed 类中对应实现
async clickConnectorSectionMoreMenu() { ... }
async clickDeleteAllConnectors() { ... }
async clickConfirmDeleteAllConnectors() { ... }
async verifyConnectorNotInList(connectorName: string) { ... }
接口签名的参数名、类型、返回值必须与实现完全一致。
**Test 层** — 只调用 Service:
import "../../../../setup/env";
import { describe, test, expect } from "vitest";
import { usePlaywrightWithAuth } from "../../../../fixtures/playwright.fixture";
import { AgentConfigService } from "../../../../services";
describe("功能描述", () => {
const ctx = usePlaywrightWithAuth();
test("用例名称", async () => {
const service = new AgentConfigService(ctx.page);
// 步骤...
}, 180_000);
});
🚫 Test 层禁止:任何定位代码。
**导出检查:**
- [ ] 新页面从 `src/pages/index.ts` 导出
- [ ] 新服务从 `src/services/index.ts` 导出
- [ ] 新组件从 `src/components/index.ts` 导出
---
## 步骤 5:验证
调用 Task 工具:
subagent_name: adp-verifier
prompt: |
测试文件路径:<路径>
当前轮次:第 N 次
是否为首轮:true/false
本次新增/修改的文件:<文件清单>
### 判断结果
读取 `.codebuddy/skills/ui-automation/artifacts/_verification.md`:
**✅ 通过:**
→ 报告成功
→ 清理 _verification.md
→ 结束
合规违规(ARCHITECTURE_VIOLATION / AI_ONLY_VIOLATION):
→ 读 _verification.md 中的违规详情
→ 自己修复代码(不计入重试)
→ 重新调度 verifier
❌ 测试失败:
retry += 1
if retry ≤ 2:
→ 读 _verification.md(错误详情 + 截图分析 + 失败历史)
→ 自己修复代码
→ 重新调度 verifier
if retry > 2:
→ 报告失败,停止
retry_fix 修复策略
| 错误类型 | 修复策略 |
|---|---|
selector_not_found / timeout
|
优化 AI 定位描述、增加等待、处理遮挡元素 |
logic_error |
修改逻辑、调整操作顺序 |
assertion_failed |
修改断言或增加等待 |
type_error |
修改类型定义 |
import_error |
修改导入路径 |
失败历史驱动(轮次 ≥ 2): 必须先读「失败历史」段,识别上轮策略,本轮采用不同策略。
| 上轮策略 | 下一步 |
|---|---|
| 优化 AI 定位描述 | 改为先 aiWaitFor 等待可见元素 |
增加 wait()
|
改为处理遮挡元素 |
| 改断言文案 | 换断言方式 |
| 调整操作顺序 | 拆分步骤 + 每步加 aiAssert
|
⛔ 已有函数修改保护
-
approved_modifications中的 → 允许修改 -
rejected_modifications中的 → 禁止修改,用替代策略 - 两个清单都没有 → 暂停,向用户展示并请求确认
替代策略:
| 场景 | 替代 |
|---|---|
| 需加参数 | 新增带后缀的方法 |
| 需改定位逻辑 | 新增独立方法 |
| 需改 Service 编排 | 新增专用方法 |
| 需改已有测试 | 新建测试文件 |
禁止行为
| 禁止 | 原因 |
|---|---|
| 跳过步骤 3 直接写代码 | 用户必须确认分析方案 |
| 写完代码不调 verifier | 代码写完 ≠ 测试通过 |
| 自己跑测试 | 必须通过 Task 调用 adp-verifier |
| 测试失败超 2 次仍继续 | 必须报告给用户 |
| 首次实现写 CSS 定位逻辑 | 必须 AI-Only |
新增 Mixin 方法时不同步 XxxMixinMethods 接口声明 |
外部类型断言后方法不可见,TypeScript 报红 |
报告模板
成功
✅ 测试通过
- 测试文件:<路径>
- 通过用例数:N
- 执行时间:Xs
失败
❌ 测试失败(已自动重试 2 次)
- 测试文件:<路径>
- 总轮次:3
- 最终错误:<类型> — <一句话>
各轮次
| 轮次 | 错误类型 | 简述 |
|---|
截图分析
[最后一次]
修复建议
[最后一次]
自动重试已达上限。请手动排查或告知修改方向。
2. 验证者(verifier 子 Agent)
这份放在 .codebuddy/agents/verifier.md。注意它用的是更便宜的小模型——验证这活儿是体力活(跑命令、读日志、套模板),不需要顶配模型。
---
name: verifier
description: UI 自动化测试的验证 Agent。执行合规预检、运行测试、分析失败截图,输出结构化验证报告。不修改任何源码。
agentMode: agentic
enabledAutoRun: true
tools: read_file, write_to_file, execute_command
model: claude-haiku-4.5
---
# 验证 Agent
## 角色
你是UI 自动化测试的验证者,职责四件:
1. 合规预检(架构 + AI-Only)
2. 运行测试
3. 分析失败原因(含截图分析)
4. 输出结构化验证报告
**你不修改任何源码。**
## 输入
测试文件路径、当前轮次、是否首轮、本次新增/修改的文件列表。
## 工作流(严格按序)
Step 1: 合规预检 → 违规则输出报告、停止
Step 2: 运行测试 → 通过则输出报告、停止
Step 3: 错误分类
Step 3.5: 截图分析
Step 4: 累积失败历史 + 输出报告
## Step 1: 合规预检
node scripts/check-compliance.js \
--files=<本轮新增/修改文件,逗号分隔> \
[--first-run] --format=json
退出码 0 = OK,进 Step 2;2 = ARCHITECTURE_VIOLATION,写报告不跑测试;
3 = AI_ONLY_VIOLATION,写报告不跑测试;1 = 脚本挂了,回退手动扫描。
手动扫描兜底:测试层禁止 page.locator/page.click/.aiTap/.aiQuery 等;
服务层禁止 this.page.locator/.aiTap/.aiAction 等(this.page.waitForTimeout
例外)。首轮还要查新增 Page 方法的 withFallback 是不是
cssAction:async()=>false + aiOnly:true。
## Step 2: 运行测试
node scripts/run-test.js --no-report --maxWorkers=1 <测试文件路径>
可能要跑 60–180 秒,等它完整结束别提前掐,stdout 和 stderr 全抓下来。
## Step 3: 错误分类
selector_not_found(找不到元素)/ timeout(超时)/ logic_error(运行时异常但
非定位)/ type_error(TS 编译错)/ import_error(模块找不到)/
assertion_failed(断言没过)。
## Step 3.5: 截图分析
stdout 里有"📸 失败截图已保存:"就触发:提取路径 → read_file 读截图 →
回答四个问题:页面什么状态?在哪步失败?根因是啥?该怎么修?
没截图就标"截图不可用",只靠日志分析。
## Step 4: 输出报告
写到 .codebuddy/skills/ui-automation/artifacts/_verification.md。
失败历史累积:本轮历史 = 上轮历史全保留 + 上轮错误压成一条摘要;首轮写
"无(首轮)";同一位置连续失败加 ⚠️;最多留 3 条。
报告格式分三种(合规违规 / 测试通过 / 测试失败),每种都有固定模板,
其中「完整测试日志」必须原样贴、不许截断。
## 约束
不改源码;不开浏览器做 DOM 检查;日志不截断;审查没过不跑测试;架构过了
必须跑测试;不向用户提问;自己把活全干完。
第二部分:这套 skill 还有什么
上面贴了两份 prompt,看起来好像挺简单的,搜代码 → 写代码 → 验证。但实际能跑起来,prompt 只是骨架,还有几份参考文件在背后撑着的。
整个 skill 的文件长这样:
.codebuddy/skills/ui-automation/
├── SKILL.md ← 生成者的执行指令(上面贴了)
├── architecture-reference.md ← 四层架构的详细版
├── midscene-api-reference.md ← AI API 怎么选、怎么用
├── locator-strategy-probe.md ← 探针评分 + CSS vs AI-Only 决策
├── locator-pitfalls.md ← 已知定位坑(shadow DOM、iframe 等)
├── dom-inspector.md ← DOM 探针脚本怎么写
├── compliance-rules.json ← 合规检查的规则
├── best-practices/ ← 各模块怎么写(可选,有比没有好)
│ ├── README.md
│ ├── app-list.md
│ ├── agent-config.md
│ ├── plugin-market.md
│ └── ...
└── artifacts/ ← 验证报告落盘位置
.codebuddy/agents/
└── verifier.md ← 验证者的 prompt(上面贴了)
下面说说每份文件什么时候被用到、为什么需要它。
architecture-reference.md:你项目怎么分层的,组件库里有什么
SKILL.md 里说了"按四层架构生成",但没展开。architecture-reference.md 就是四层的详细说明书:
-
Component 层有哪些现成的封装:
InputComponent(填输入框)、ButtonComponent(点按钮)、AlertComponent(读 Toast)、NavComponent(点菜单)。生成者写 Page 方法时,能复用这些就别自己造轮子。 -
Page 层必须继承
BasePage,所有 DOM 操作用withFallback模式。还有一条很重要的约束:禁止跨页面操作。比如AppConfigPage里不能去操作AppListPage的东西。 - Service 层构造里持有 Page 实例,提供语义方法。禁止直接调 DOM。
-
Test 层只能调 Service,
expect只能写在这一层。 -
Widget 在 iframe 里,所有 Widget 内的控件操作(checkbox、按钮、输入框)必须用 AI 定位(
stagehand.act),禁止用 CSS/DOM 方式——因为你没法保证同一个文案的按钮在 iframe 里外各有一个时不点错。
生成者在步骤 1 搜完代码后,拿这份文件做三件事:确认可复用的 Component、确认目标 Page/Service 的继承链、确认每个方法的代码放在哪一层才对。
midscene-api-reference.md:AI 方法怎么选
搞 UI 自动化的都知道,AI 定位方法有好几种,用错了坑的是自己。这份文件的核心就是一句话:能拆开的别合在一起,能用单步的别用复合的。
优先级从高到低:
| 优先级 | 操作 | 方法 |
|---|---|---|
| 1 | 输入文本 | aiInput(locate, { value }) |
| 2 | 点击元素 | aiTap(locate) |
| 3 | 滚动 | aiScroll(locate, opt) |
| 4 | 按键 | aiKeyboardPress(locate, { keyName }) |
| 5 | 悬停 / 双击 |
aiHover / aiDoubleClick
|
| 6 | 读数据 |
aiQuery / aiBoolean / aiString / aiNumber
|
| 7 | 断言 / 等待 |
aiAssert / aiWaitFor
|
| 最后 | 复合操作 |
aiAction(能不用就不用) |
最常见的错误是:生成者偷懒,一个 aiAction("点击确定按钮") 搞定。但 aiAction 是"AI 自己拆解步骤执行",比 aiTap 慢不少,还容易因为 AI 理解偏差走歪。能用 aiTap 的,别用 aiAction。同样的,输入框一律用 aiInput(默认 replace 模式会自动清空),别用 aiAction("在输入框输入xxx")。
还有 withFallback 的两种模板:AI-Only(首次实现,cssAction: async () => false + aiOnly: true)和 CSS-First(探针验证过 CSS 可用后切换)。生成者在步骤 4 写代码时,所有 Page 方法对着这份文档的模板写,就不会写出反模式。
best-practices/:每个模块怎么写的"活教材"
README.md 是一个索引表,按关键词指路:
| 需求里提到 | 就读 |
|---|---|
| 创建应用、提示词、模型切换 | agent-config.md |
| 插件、MCP、收藏 | plugin-market.md |
| 知识库、文档导入 | knowledge-base.md |
| Widget、checkbox | widget.md |
| 应用列表、搜索、复制、删除 | app-list.md |
每份文档里写着三样东西:用什么 Service 初始化、常用操作组合(A → B → C 流程怎么写)、关键注意事项(比如"创建完应用会进应用内页,回列表要重新 goToAppList()")。
这就是步骤 2"读相邻 case"的升级版:不只看同目录下的一两个例子,还看模块级的最佳实践,把"这个模块应该怎么测"的模式直接拿过来用。
locator-pitfalls.md:你以为写对了,其实掉坑里了
这是给 retry 修复阶段看的。测试挂了,70% 的情况不是逻辑错了,是定位方式踩了坑。文件里记录了 6 个经典坑:
-
Shadow DOM / Web Components:ADP 的控件是 custom element,DOM 包在
<template shadowrootmode="open">里,普通querySelector根本看不到。得递归穿透.shadowRoot。 -
多个 iframe 定位到错的:页面上一堆
iframe,用文案匹配可能匹配到主页面里的按钮。必须先用src精确定位目标 iframe。 - stagehand.act 的 click 静默失败:日志里不会抛错、try-catch 抓不到,按钮实际没被点。
- evaluate 里的 console.log 看不见:在浏览器进程执行的,Node.js 侧收不到,得把信息 return 出来打印。
-
checkbox-widget 的勾选:用
input[type="checkbox"]找不到,因为真正的 input 藏在 shadow root 里且display:none。 -
原生
HTMLElement.click()对 Vue/React 组件无效:派发的是isTrusted=false的假事件,组件库不认。得用clickByMarker模式——先setAttribute打标记,再用 Playwright 的locator.click()触发真事件。
每轮 retry 修复前,生成者都该先看这份文件,看一眼错误信息跟 6 个坑的描述对不对上。避免在同一个坑里浪费时间。
locator-strategy-probe.md:写个探针,让浏览器告诉你真实选择器
AI-Only 定位连续失败,想切 CSS。但你怎么知道 CSS 靠不靠谱?弹窗里的 input 有没有 id?按钮的 class 是不是带哈希后缀(构建时随机生成、下次就跑飞了)?
这份文件定义了一套探针评分规则:在 headless 模式下跑一段 JS,采集目标元素的属性(id、name、data-testid、className 有没有哈希、在不在 shadow DOM 里等),然后按加分减分规则算总分。总分 ≥ 1 可以切 CSS-First,总分 ≤ 0 就继续 AI-Only、只优化描述。
它还有个经验库表格,记录以前探过的结论,下次同一个页面不用再跑——比如"添加用户弹窗里的 input,总分 -1,老老实实 AI-Only",直接复用。
dom-inspector.md:探针脚本的模板,照抄就行
locator-strategy-probe.md 说了"要跑探针",但探针脚本长什么样?这份文件提供了完整的模板:怎么借助已有 Service 导航到目标 UI 状态、等动画结束、page.evaluate 里采集 formLabel/placeholder/parentClass/可见性、用 console.log 输出结构化 JSON。
它还包含了几个专项模板:文件上传控件的探针(input[type="file"] 往往是 sr-only 隐藏的)、异步状态轮询探针(比如文档从"学习中"变到"导入完成",要轮询等)、文案采集探针(不光抓选择器,还抓业务文案理解产品含义)。
生成者在 retry 修复需要写探针时,直接拿这份模板改参数就行,不用自己从零写。
compliance-rules.json:验证者合规检查的规则库
验证者 Step 1 跑的 node scripts/check-compliance.js,实际吃的规则就在这个文件里——它定义了测试层和服务层分别禁止出现哪些代码模式(page.locator、.aiTap、.aiAction 等)。验证者自己不写规则,只管执行,这样规则变了只改 JSON 不动 prompt。
一张图看全貌
用户说需求
↓
┌── 生成者(主 Agent,读 SKILL.md)─────────────────────┐
│ │
│ Step 1 搜代码 │
│ ├─ best-practices/README.md → 匹配模块文档 │
│ ├─ architecture-reference.md → 确认可复用 Component │
│ └─ codebase_search → 找相关 Page/Service │
│ │
│ Step 2 读相邻 case + best-practices 对应文档 │
│ │
│ Step 3 出分析表格 → 等用户确认 │
│ │
│ Step 4 写代码 │
│ ├─ midscene-api-reference.md → 选对 AI 方法 │
│ └─ architecture-reference.md → 代码放对层 │
│ │
│ Step 5 调 verifier → 读 _verification.md │
│ ├─ 合规挂了 → 自己修(不计次)→ 回 Step 5 │
│ └─ 测试挂了 → │
│ ├─ locator-pitfalls.md → 是不是踩了已知坑 │
│ ├─ locator-strategy-probe.md → 要不要切 CSS │
│ ├─ dom-inspector.md → 探针脚本照模板写 │
│ └─ 修 → 回 Step 5(最多 2 次) │
└──────────────────┬────────────────────────────────────┘
│ Task
↓
┌── 验证者(verifier,小模型,不写代码)────────────────┐
│ check-compliance.js ← compliance-rules.json │
│ run-test.js │
│ 读截图 → 分类错误 → 写报告 │
└───────────────────────────────────────────────────────┘
左边 column 是生成者干活时读什么,右边是验证者只负责"跑 + 报"。prompt 文件是执行指令,参考文件是执行时查的知识库——缺哪个都跑不顺。
结尾
出这样一个教程,主要是因为之前的版本太复杂,很多同学反馈看不懂,而且很消耗 token 和时间,所以才出了这个版本。 其实这个版本也可以精简, 比如最后测试失败后, 可以不走探针(我现在基本都不走探针,AI 定位挺好的。)大家也可以把这部分删掉
最后再宣传一下自己的星球:
