@epoint-playwright/codegen-cli
v1.6.0
Published
Playwright 自动化测试代码生成 CLI 工具
Maintainers
Readme
@epoint/codegen-cli
Playwright 自动化测试代码生成 CLI 工具,将录制 JSON 转为 TypeScript spec 文件。
安装
# 全局安装
npm install -g @epoint/codegen-cli
# 或项目内安装
npm install -D @epoint/codegen-cli规则
元素过滤规则
以下操作不生成代码(element-mapping.mjs isUselessOperation L45-92):
- [x] 弹窗(
mini-window)上的blur事件 → L53-55 - [x] 空的
change/input事件(无value且无text) → L57-58 - [x] 空值操作:
mini-treeselect、mini-combobox无value且无text→ L60-61 - [x] 空值操作:
mini-datepicker无value且无text→ L63-64 - [x] 无效点击:
click事件无text、无id、无miniui.control→ L66-67 - [x]
mini-buttonedit-button/mini-buttonedit-trigger(日期选择器触发按钮) → L69-70 - [x]
mini-checkbox-icon点击 → L72-73 - [x]
laydate/layui-样式类元素 → L75-76 - [x]
mini-tree-node-ecicon/mini-tree-nodetitle树节点图标点击 → L78-79 - [x]
mini-grid-row/mini-grid-cell表格行/单元格点击 → L81-82 - [x]
mini-popup弹层中的操作(由 MiniUI 封装方法内部处理) → L84-86 - [x]
menu-name点击(由 clickMenu 统一处理) → L87-88 - [x]
mini-tree-node-ecicon/mini-tree-nodetitle/mini-tree-checkbox树节点操作(breadcrumbs 检查) → L67-L69 - [x]
mini-tabstreeselect中间操作(展开/搜索,selected为空)不生成代码 → L72-L75 - [x] 日期面板上的纯数字点击(1-31,由 selectMiniDatePicker 内部处理) → L89-90
- [x]
fui-primary-search搜索框的click/blur等非change事件(仅change生成代码,中间态过滤) → L73-74
MiniUI 控件映射规则
有 miniui.control 数据时,按控件类型生成对应的 MiniDriver 方法(element-mapping.mjs MAPPING_RULES_TS L11-30 + generateFromRuleTs L96-128):
| control | 生成方法 | 定位策略 |
|---|---|---|
| mini-textbox | await driver.inputMiniTextBox(ByT.id(...), value, label) | byId |
| mini-textarea | await driver.inputMiniTextAreaBox(ByT.id(...), value, label) | byId |
| mini-spinner | await driver.setMiniSpinnerByElement(ByT.id(...), value, label) | byId |
| mini-buttonedit-search | await driver.inputMiniButtonEditSearch(By.id(...), value, label) | byId |
| mini-combobox | await driver.selectMiniComboBox(ByT.id(...), value, label) | byId |
| mini-treeselect | 见下方 treeselect 分支 | byId |
| mini-radiobuttonlist | await driver.selectMiniRadioButton(ByT.id(...), value, label) | byId |
| mini-checkboxlist | await driver.selectMiniCheckBoxList(ByT.id(...), value, label) | byId |
| mini-datepicker | await driver.selectMiniDatePicker(ByT.label(...), value, label) | byLabel |
| mini-daterangepicker | await driver.selectMiniDatePicker(ByT.id(...), value, label) | byId |
| mini-tabstreeselect | await driver.selectTabsTreeSelectByElement(By.id(...), [selected], label) | byId |
| mini-button | await driver.clickMiniButton(ByT.buttonText(...)) | byButtonText |
| mini-webuploader | await driver.miniUploadFile(ByT.id(...), value, label) | byId |
mini-treeselect 三分支(generateTreeselectCode):
| 预处理结果 | 生成方法 |
|---|---|
| miniui.checkPaths.length > 1 | selectMiniCheckTreeMultiple(By.id(...), [paths], label) |
| miniui.checkPaths.length === 1 | selectMiniCheckTree(By.id(...), path, label) |
| 仅有 miniui.value(单选 radio 树) | selectMiniTree(By.id(...), value, label) |
定位策略优先级(generateFromRuleTs):
byButtonText(useTextAsBy时)→ByT.buttonText("text")byId→ByT.id("id")byLabel→ByT.label("label")(datepicker 专用,无稳定 id 时用 label 定位)回退 →
ByT.id("")[x] 有
miniui数据时根据相同id去重,取最新操作中记录最全的数据 → dedupByMiniuiId[x]
mini-tabstreeselect去重按selected.length打分(而非value,其value是搜索框文本不代表选中状态) → L73-L80[x]
mini-treeselect单选路径提取(radio 树) → resolveTreeSelectPaths- 同一
miniui.id的mini-treeselect元素构成一个区间(首次出现到末次出现) - 在区间内搜索
mini-filtertree的click事件 - 取最后一个匹配的
treePath数组,用-连接 - 用连接后的值替换区间最后一个 treeselect 元素的
miniui.value - 例:treeselect
prfzrguid区间内找到 treePath["系统管理部", "曹凯伦"]→ value 更新为"系统管理部-曹凯伦"
- 同一
[x]
mini-treeselect复选树多选路径聚合 → resolveCheckTreeSelectPaths- 在同一 treeselect 区间内,收集 breadcrumbs 含
.mini-tree-checkbox的mini-filtertreeclick - ecicon 展开(
.mini-tree-node-ecicon)不计入选中路径 - 去重后写入最后一个 treeselect 元素的
miniui.checkPaths(字符串数组) checkPaths.length > 1→selectMiniCheckTreeMultiple;=== 1→selectMiniCheckTree- 与单选逻辑互斥:已有
checkPaths时resolveTreeSelectPaths跳过该 id - 例:
["所有部门-系统管理部-曹凯伦", "所有部门-系统管理部-系统管理员", "所有部门-系统管理部-陈敏"]
- 在同一 treeselect 区间内,收集 breadcrumbs 含
mini-buttonedit-search 搜索框(fui-primary-search)
mini-buttonedit + fui-primary-search 类的搜索框(文本框 + 触发按钮,如「项目名称」搜索)。
- [x] 录制端识别:
findMiniElement在通用buttonedit检测前用.fui-primary-search命中外层 span,写入miniui.control = "mini-buttonedit-search",placeholder作为label兜底(recording-record.js) - [x] 旧录制兼容(
miniui为空):仅靠 breadcrumbs 含fui-primary-search识别,给change事件打control标记使其通过控件守卫 → resolveButtonEditSearch- 只处理
change事件且value非空;非change事件由isUselessOperation过滤
- 只处理
- [x] id 回退提取:
miniui.id为空时,generateFromRuleTs从 breadcrumbs 提取外层 span id → extractPrimarySearchId、L148-151- 形如
span#toolbar-search-projectName.mini-buttonedit.fui-primary-search.r→toolbar-search-projectName
- 形如
- [x]
value取自change事件的el.value,label取自miniui.label(新录制为 placeholder) - [x] 生成
await driver.inputMiniButtonEditSearch(By.id("toolbar-search-projectName"), "123123", "请输入项目名称");
纯 HTML 元素回退规则
(element-mapping.mjs mapPlainHtmlToCode L128-172)
无 miniui.control、无 iframe(level 0)→ 使用 Playwright 原生 page.locator() 操作。
- [x] 从 breadcrumbs 最后一段提取 CSS 选择器 → extractSelector
- 有稳定非动态 ID →
#id - 无 ID → 取有意义的 class 拼接为
.class1.class2
- 有稳定非动态 ID →
- [x]
change/blur/input事件且有值 →await page.locator('#sel').fill("val"); - [x] 密码字段(id 或 class 含
password) →await page.locator('#pwd').pressSequentially("val", { delay: 100 }); - [x]
click事件 →await page.locator('#btn').click(); - [x] 动态 ID 过滤(
dialog-*、tab-content-*、UUID 格式) → xpath-utils.mjs isDynamicId - [x] iframe 内无
miniui.control的纯 HTML 元素回退 → generateIframeStep L217-L222generatePlainHtmlCode(el, pageVar)接受自定义变量名,iframe 内传入 frame 变量(如nestedFrame3)- 生成
await nestedFrame3.locator('.fui-search-trigger.r').click();
菜单导航规则
(menu-navigation.mjs extractMenuNavigation L11-45)
- [x] 检测元素列表开头的连续
menu-name点击 → L16-33- 判断条件:
breadcrumbs或className包含menu-name+click事件 + 有textL21-22
- 判断条件:
- [x] 去重相邻重复项,合并为
await clickMenu(page, '菜单路径', 'idea');→ L25-30 - [x] 菜单点击元素不生成独立的操作代码(整个 action 只生成 clickMenu 一步)
- [x] 菜单点击之间的
menu-linkblur 事件跳过,不打断连续扫描 → L33-34
步骤导航菜单(new-step-item / new-step-group)
- [x] 同层级
new-step-item点击 → 各生成一条await clickMenu(page, '步骤名', 'step'); - [x]
new-step-group+ 首个new-step-item→await clickMenu(page, '父组-子步骤', 'step');(先展开父组再点子项) - [x] 仅
new-step-group→await clickMenu(page, '父组名', 'step-group'); - [x] 运行时子项通过
data-step定位,不依赖易变 class
iframe 处理规则
(codegen-cli.mjs generateIframeStep L99-175)
- [x] 根据
iframeChain计算嵌套层级 → xpath-utils.mjs getIframeLevel - [x] Level 0(无 iframe)→ 走纯 HTML 回退规则
- [x] Level ≥ 1:逐层生成
resolveContentFrame调用 - [x] iframe 类型自动识别 → determineIframeType
- breadcrumbs 含
tab-content→TAB_IFRAME(主内容区) - 其他(含
dialog-等)→DIALOG_IFRAME(弹窗) - 嵌套层始终使用
DIALOG_IFRAME
- breadcrumbs 含
- [x] iframe 复用追踪:使用
iframe.attributes.name作为唯一标识 → L112-L114- 同一 name 的 iframe 只首次插入
waitForLoadState('networkidle') - 后续复用时不重复等待
- 同一 name 的 iframe 只首次插入
- [x] 嵌套 iframe:外层传入
!非空断言 →resolveContentFrame(dialogFrame!, DIALOG_IFRAME) - [x] 多层嵌套变量名去重 → L154-L163
- level 0:
mainFrame(TAB_IFRAME)/dialogFrame(DIALOG_IFRAME) - level 1:
nestedFrame - level 2+:
nestedFrame2、nestedFrame3… 避免重复const声明
- level 0:
消息框关闭检测
(element-mapping.mjs detectMessageBoxClose L177-208)
- [x] 检测 breadcrumbs 含
mini-messagebox+click事件 +text === "确定"→ 找到关闭按钮 - [x] messagebox 元素不参与
dedupByMiniuiId(避免 click 被 blur 替换) - [x] 在点击元素前后 ±5 范围内搜索 messagebox 的
blur事件,提取消息文本 - [x] 消息文本非空时生成
await driver.closeMessageBox(CloseType.Confirm, '消息');;为空时await driver.closeMessageBox(CloseType.Confirm); - [x] 按出现位置内联生成
closeMessageBox(多次消息框各生成一次,保持与前后操作的顺序) - [x] 跳过 messagebox 内其余事件(blur / mini-button 映射),不生成
clickMiniButton
autofill 自动填充检测
(element-mapping.mjs detectAutofill L287-299 + isUselessOperation L60-61)
- [x] 检测 breadcrumbs 含
__epoint_autofill_btn__+af-btn-fill+click事件 → 找到自动填充按钮 - [x] 拖拽手柄(
div.af-handle > svg)不生成代码 → isUselessOperation L60-61 - [x] 生成代码模板:
const ok = await injectAutoFill(frameVar); if (ok) { await autoFill(frameVar, true); } - [x] autofill 代码在
waitForLoadState之后、new MiniDriver之前生成 → generateIframeStep - [x] 所有
__epoint_autofill_btn__元素跳过常规 MiniUI 元素映射 → codegen-cli.mjs#L200
代码生成结构规则
- [x] 每个 action 生成
steps数组中的一个async (page) => { ... }函数 → generateStep - [x] 无操作的空 action(如纯弹窗 blur)自动跳过,不生成步骤 → L87-L92
- [x]
test()块设置 120s 超时,用page.goto()访问首个 URL - [x] 元素操作代码通过
seenOperationsSet 去重 → element-mapping.mjs L133-L134 - [x] 条件导入:按需引入
MiniDriver、ByT、clickMenu、resolveContentFrame等
CLI 用法
# 代码生成(默认命令)
epoint-codegen <json> <output> <name> [--action-range N-M] [--id <uuid>]
# 代码比对子命令
epoint-codegen diff <path> [--format text|json]
# 项目内安装
npx epoint-codegen <json> <output> <name> [--action-range N-M] [--id <uuid>]
# 或直接用 node 运行
node codegen-cli.mjs <json> <output> <name> [--action-range N-M] [--id <uuid>]
node codegen-cli.mjs diff <path> [--format text|json]参数
| 参数 | 说明 |
|------|------|
| <json> | 必填,录制文件路径(.json) |
| <output> | 必填,输出文件路径(.ts),文件名(去掉扩展名)自动转换为 PascalCase 作为 className |
| <name> | 必填,步骤中文名称,对应生成代码中 super({ name }) 的值 |
选项
| 选项 | 说明 |
|------|------|
| --action-range <N-M> | 截取录制中第 N 到第 M 个 action(含两端,从 0 开始),不传则使用全部 |
| --id <uuid> | 自定义步骤 ID,不传则自动生成 UUID |
| -V, --version | 显示版本号 |
| -h, --help | 显示帮助 |
className 推导规则
输出文件名去掉 .ts 扩展名后,按非字母数字字符分割,每段首字母大写后拼接:
| 文件名 | className |
|--------|-----------|
| purchase-request.ts | PurchaseRequest |
| form-fill.ts | FormFill |
| login.ts | Login |
示例
# 基本用法:全部 action
node codegen-cli.mjs recording.json steps/purchase-request.ts 采购需求申请
# 截取部分 action(第 3 到第 7 个)
node codegen-cli.mjs recording.json steps/form-fill.ts 表单填写 --action-range 3-7
# 指定自定义 ID
node codegen-cli.mjs recording.json steps/form-fill.ts 表单填写 --id my-custom-id基线(.codegen-baseline)
生成主文件时,会同步在 <outputDir>/.codegen-baseline/ 下写入一份只读基线副本(lib/baseline.mjs writeBaseline)。
- 配对口径:按生成代码内的
id字段配对(super({ id: '...' }),见 diff-core.mjs extractStepId) - 覆盖策略:保留最新,不保留历史版本。重新生成会先解除旧基线只读位(
chmod 0o666)→ 覆盖写入 → 重新置为只读(chmod 0o444) - 自递归保护:输出文件本身已位于
.codegen-baseline内时跳过,不再向其下级写基线 - 只读:基线文件权限
0o444,避免被手工误改;Windows 上chmod可能无效,但写入逻辑一致
steps/
├── purchase-request.ts ← 生成的主文件(可改)
└── .codegen-baseline/
└── purchase-request.ts ← 只读基线(覆盖最新,不留历史)代码比对(diff 子命令)
epoint-codegen diff <path> 将工作副本与 .codegen-baseline 基线进行行级比对,输出准确率报告。diff 核心调用 vscode-diff(VS Code 同款 DiffComputer),见 lib/diff-core.mjs。
# 目录模式:比对整个 pages 目录,基线默认 <pages-dir>/.codegen-baseline
epoint-codegen diff <pages-dir>
# 单文件模式:基线默认 <fileDir>/.codegen-baseline/<basename>.ts
epoint-codegen diff <file.ts>
# JSON 输出(便于 CI 解析)
epoint-codegen diff <file.ts> --format json比对口径
- 仅比对
steps数组体,忽略import/ 文件头注释 /class壳 /page.goto/source元数据(extractStepsBody) - 按
id配对:工作目录有、基线无 → 计为「新增/手写」,不计入准确率;基线有、工作无 → 计为「丢失」 - 行级准确率 = 未改行 / 基线总行数;新增行不降低准确率(分母为基线行数)
退出码
| 场景 | 退出码 |
|------|--------|
| 全部配对项准确率 100% 且无丢失 | 0 |
| 任一配对项准确率 < 100%,或存在丢失基线 | 1 |
便于在 CI 中作为回归门禁:epoint-codegen diff steps/ 非零退出即代表生成代码与基线存在偏差。
报告示例(text)
代码准确率报告(行级,仅比对 steps 内容)
============================================================
[stepId] 0F70049A-5A27-47A9-8726-76A428E73C6C
工作文件: demo.ts 基线文件: demo.ts
基线 9 行 未改 8 删除 1 新增 1 准确率 88.9%
改动明细:
- [基线:5] await page.locator('.login-submit').click();
+ [工作:5] await page.locator('button.login-submit').click();
============================================================
配对 1 丢失 0 新增/手写 0
总体准确率: 88.9%(8/9 行未改)