@epoint-playwright/codegen-cli
v1.3.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
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-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 含
纯 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] 检测须在
dedupByMiniuiId去重之前 → codegen-cli.mjs#L242- 去重会用更高 index 的
blur事件替换click事件,导致检测不到 click
- 去重会用更高 index 的
- [x] 在点击元素前后 ±5 范围内搜索 messagebox 的
blur事件,提取消息文本 → L184-195 - [x] 消息文本格式:
"提醒\n\n<消息内容>\n\n确定",取第二行 - [x] 消息文本非空时生成
await driver.closeMessageBox(CloseType.Confirm, '消息');;为空时await driver.closeMessageBox(CloseType.Confirm); - [x] 替换消息框内所有"确定"按钮点击(跳过所有
mini-messagebox上下文中的text === "确定"元素)
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 用法
# npm 全局安装后
epoint-codegen <json> <output> <name> [--action-range N-M] [--id <uuid>]
# 项目内安装
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>]参数
| 参数 | 说明 |
|------|------|
| <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