npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@epoint-playwright/codegen-cli

v1.3.0

Published

Playwright 自动化测试代码生成 CLI 工具

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-treeselectmini-comboboxvalue 且无 textL60-61
  • [x] 空值操作:mini-datepickervalue 且无 textL63-64
  • [x] 无效点击:click 事件无 text、无 id、无 miniui.controlL66-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):

  • byButtonTextuseTextAsBy 时)→ ByT.buttonText("text")

  • byIdByT.id("id")

  • byLabelByT.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.idmini-treeselect 元素构成一个区间(首次出现到末次出现)
    • 在区间内搜索 mini-filtertreeclick 事件
    • 最后一个匹配的 treePath 数组,用 - 连接
    • 用连接后的值替换区间最后一个 treeselect 元素的 miniui.value
    • 例:treeselect prfzrguid 区间内找到 treePath ["系统管理部", "曹凯伦"] → value 更新为 "系统管理部-曹凯伦"
  • [x] mini-treeselect 复选树多选路径聚合 → resolveCheckTreeSelectPaths

    • 在同一 treeselect 区间内,收集 breadcrumbs 含 .mini-tree-checkboxmini-filtertree click
    • ecicon 展开(.mini-tree-node-ecicon)不计入选中路径
    • 去重后写入最后一个 treeselect 元素的 miniui.checkPaths(字符串数组)
    • checkPaths.length > 1selectMiniCheckTreeMultiple=== 1selectMiniCheckTree
    • 与单选逻辑互斥:已有 checkPathsresolveTreeSelectPaths 跳过该 id
    • 例:["所有部门-系统管理部-曹凯伦", "所有部门-系统管理部-系统管理员", "所有部门-系统管理部-陈敏"]

纯 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
  • [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-L222
    • generatePlainHtmlCode(el, pageVar) 接受自定义变量名,iframe 内传入 frame 变量(如 nestedFrame3
    • 生成 await nestedFrame3.locator('.fui-search-trigger.r').click();

菜单导航规则

menu-navigation.mjs extractMenuNavigation L11-45)

  • [x] 检测元素列表开头的连续 menu-name 点击 → L16-33
    • 判断条件:breadcrumbsclassName 包含 menu-name + click 事件 + 有 text L21-22
  • [x] 去重相邻重复项,合并为 await clickMenu(page, '菜单路径', 'idea');L25-30
  • [x] 菜单点击元素不生成独立的操作代码(整个 action 只生成 clickMenu 一步)
  • [x] 菜单点击之间的 menu-link blur 事件跳过,不打断连续扫描 → L33-34

步骤导航菜单(new-step-item / new-step-group)

  • [x] 同层级 new-step-item 点击 → 各生成一条 await clickMenu(page, '步骤名', 'step');
  • [x] new-step-group + 首个 new-step-itemawait clickMenu(page, '父组-子步骤', 'step');(先展开父组再点子项)
  • [x] 仅 new-step-groupawait 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-contentTAB_IFRAME(主内容区)
    • 其他(含 dialog- 等)→ DIALOG_IFRAME(弹窗)
    • 嵌套层始终使用 DIALOG_IFRAME
  • [x] iframe 复用追踪:使用 iframe.attributes.name 作为唯一标识 → L112-L114
    • 同一 name 的 iframe 只首次插入 waitForLoadState('networkidle')
    • 后续复用时不重复等待
  • [x] 嵌套 iframe:外层传入 ! 非空断言 → resolveContentFrame(dialogFrame!, DIALOG_IFRAME)
  • [x] 多层嵌套变量名去重 → L154-L163
    • level 0:mainFrame(TAB_IFRAME)/ dialogFrame(DIALOG_IFRAME)
    • level 1:nestedFrame
    • level 2+:nestedFrame2nestedFrame3 … 避免重复 const 声明

消息框关闭检测

element-mapping.mjs detectMessageBoxClose L177-208)

  • [x] 检测 breadcrumbs 含 mini-messagebox + click 事件 + text === "确定" → 找到关闭按钮
  • [x] 检测须在 dedupByMiniuiId 去重之前codegen-cli.mjs#L242
    • 去重会用更高 index 的 blur 事件替换 click 事件,导致检测不到 click
  • [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] 元素操作代码通过 seenOperations Set 去重 → element-mapping.mjs L133-L134
  • [x] 条件导入:按需引入 MiniDriverByTclickMenuresolveContentFrame

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