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.6.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
  • [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):

  • 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
    • 例:["所有部门-系统管理部-曹凯伦", "所有部门-系统管理部-系统管理员", "所有部门-系统管理部-陈敏"]

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 → extractPrimarySearchIdL148-151
    • 形如 span#toolbar-search-projectName.mini-buttonedit.fui-primary-search.rtoolbar-search-projectName
  • [x] value 取自 change 事件的 el.valuelabel 取自 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
  • [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] 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] 元素操作代码通过 seenOperations Set 去重 → element-mapping.mjs L133-L134
  • [x] 条件导入:按需引入 MiniDriverByTclickMenuresolveContentFrame

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 行未改)