@epoint-testtech/ep-stage-skill 0.0.9 → 0.5.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/SKILL.md +27 -27
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -223
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -275
  4. package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
  5. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -95
  6. package/codex-skill/ep-stage/glue-run-test/SKILL.md +273 -296
  7. package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -30
  8. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -132
  9. package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -44
  10. package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
  11. package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +127 -166
  12. package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
  13. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
  14. package/codex-skill/ep-stage/scripts/validate-skill.mjs +95 -98
  15. package/dist/src/cli/generate-testcase.d.ts +0 -1
  16. package/dist/src/cli/generate-testcase.d.ts.map +1 -1
  17. package/dist/src/cli/generate-testcase.js +1 -6
  18. package/dist/src/cli/index.d.ts.map +1 -1
  19. package/dist/src/cli/index.js +14 -38
  20. package/dist/src/cli/run.d.ts +0 -2
  21. package/dist/src/cli/run.d.ts.map +1 -1
  22. package/dist/src/cli/run.js +21 -70
  23. package/dist/src/cli/spec.d.ts.map +1 -1
  24. package/dist/src/cli/spec.js +0 -9
  25. package/dist/src/contracts/glue-contract-v1.d.ts +0 -35
  26. package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -1
  27. package/dist/src/extractors/code-list-json5.d.ts +1 -34
  28. package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
  29. package/dist/src/extractors/code-list-json5.js +0 -1
  30. package/dist/src/extractors/html-page.d.ts +0 -9
  31. package/dist/src/extractors/html-page.d.ts.map +1 -1
  32. package/dist/src/extractors/html-page.js +2 -26
  33. package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
  34. package/dist/src/generators/stage-skeleton-script.d.ts +0 -50
  35. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  36. package/dist/src/generators/stage-skeleton-script.js +116 -449
  37. package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -1
  38. package/dist/src/runtime/ai-runtime-trace.js +0 -8
  39. package/dist/src/testcase/contract-testcase-assembler.d.ts +0 -28
  40. package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
  41. package/dist/src/testcase/contract-testcase-assembler.js +75 -329
  42. package/dist/src/validation/contract.d.ts.map +1 -1
  43. package/dist/src/validation/contract.js +0 -29
  44. package/dist/test/cli/generate-testcase-json5.test.d.ts +2 -0
  45. package/dist/test/cli/generate-testcase-json5.test.d.ts.map +1 -0
  46. package/dist/test/cli/generate-testcase-json5.test.js +154 -0
  47. package/dist/test/cli/index.test.d.ts +2 -0
  48. package/dist/test/cli/index.test.d.ts.map +1 -0
  49. package/dist/test/cli/index.test.js +114 -0
  50. package/dist/test/cli/run.test.d.ts +2 -0
  51. package/dist/test/cli/run.test.d.ts.map +1 -0
  52. package/dist/test/cli/run.test.js +429 -0
  53. package/dist/test/cli/spec.test.d.ts +2 -0
  54. package/dist/test/cli/spec.test.d.ts.map +1 -0
  55. package/dist/test/cli/spec.test.js +112 -0
  56. package/dist/test/code-list-json5.test.d.ts +2 -0
  57. package/dist/test/code-list-json5.test.d.ts.map +1 -0
  58. package/dist/test/code-list-json5.test.js +84 -0
  59. package/dist/test/code-list.test.d.ts +2 -0
  60. package/dist/test/code-list.test.d.ts.map +1 -0
  61. package/dist/test/code-list.test.js +39 -0
  62. package/dist/test/gap-inference.test.d.ts +2 -0
  63. package/dist/test/gap-inference.test.d.ts.map +1 -0
  64. package/dist/test/gap-inference.test.js +597 -0
  65. package/dist/test/generalization.test.d.ts +2 -0
  66. package/dist/test/generalization.test.d.ts.map +1 -0
  67. package/dist/test/generalization.test.js +73 -0
  68. package/dist/test/material-inventory.test.d.ts +2 -0
  69. package/dist/test/material-inventory.test.d.ts.map +1 -0
  70. package/dist/test/material-inventory.test.js +155 -0
  71. package/dist/test/observable-chain.test.d.ts +2 -0
  72. package/dist/test/observable-chain.test.d.ts.map +1 -0
  73. package/dist/test/observable-chain.test.js +123 -0
  74. package/dist/test/page-structure.test.d.ts +2 -0
  75. package/dist/test/page-structure.test.d.ts.map +1 -0
  76. package/dist/test/page-structure.test.js +45 -0
  77. package/dist/test/runtime/ai-runtime-trace.test.d.ts +2 -0
  78. package/dist/test/runtime/ai-runtime-trace.test.d.ts.map +1 -0
  79. package/dist/test/runtime/ai-runtime-trace.test.js +78 -0
  80. package/dist/test/scenario-inference.test.d.ts +2 -0
  81. package/dist/test/scenario-inference.test.d.ts.map +1 -0
  82. package/dist/test/scenario-inference.test.js +73 -0
  83. package/dist/test/stage-context.test.d.ts +2 -0
  84. package/dist/test/stage-context.test.d.ts.map +1 -0
  85. package/dist/test/stage-context.test.js +395 -0
  86. package/dist/test/testcase/testcase-markdown.test.d.ts +2 -0
  87. package/dist/test/testcase/testcase-markdown.test.d.ts.map +1 -0
  88. package/dist/test/testcase/testcase-markdown.test.js +48 -0
  89. package/dist/test/testcase-generator.test.d.ts +2 -0
  90. package/dist/test/testcase-generator.test.d.ts.map +1 -0
  91. package/dist/test/testcase-generator.test.js +276 -0
  92. package/dist/test/testcase-skeleton.test.d.ts +2 -0
  93. package/dist/test/testcase-skeleton.test.d.ts.map +1 -0
  94. package/dist/test/testcase-skeleton.test.js +185 -0
  95. package/dist/test/testcase-spec-assembly.test.d.ts +2 -0
  96. package/dist/test/testcase-spec-assembly.test.d.ts.map +1 -0
  97. package/dist/test/testcase-spec-assembly.test.js +105 -0
  98. package/dist/test/util/credentials.test.d.ts +2 -0
  99. package/dist/test/util/credentials.test.d.ts.map +1 -0
  100. package/dist/test/util/credentials.test.js +64 -0
  101. package/dist/test/util/i18n-testcase.test.d.ts +2 -0
  102. package/dist/test/util/i18n-testcase.test.d.ts.map +1 -0
  103. package/dist/test/util/i18n-testcase.test.js +119 -0
  104. package/dist/test/util/softlink.test.d.ts +2 -0
  105. package/dist/test/util/softlink.test.d.ts.map +1 -0
  106. package/dist/test/util/softlink.test.js +82 -0
  107. package/dist/test/validation/contract.test.d.ts +2 -0
  108. package/dist/test/validation/contract.test.d.ts.map +1 -0
  109. package/dist/test/validation/contract.test.js +116 -0
  110. package/dist/test/validation/credentials.test.d.ts +2 -0
  111. package/dist/test/validation/credentials.test.d.ts.map +1 -0
  112. package/dist/test/validation/credentials.test.js +74 -0
  113. package/dist/test/validation/projects-index.test.d.ts +2 -0
  114. package/dist/test/validation/projects-index.test.d.ts.map +1 -0
  115. package/dist/test/validation/projects-index.test.js +48 -0
  116. package/dist/test/validation/testcase-v1-draft.test.d.ts +2 -0
  117. package/dist/test/validation/testcase-v1-draft.test.d.ts.map +1 -0
  118. package/dist/test/validation/testcase-v1-draft.test.js +173 -0
  119. package/dist/vitest.config.d.ts +3 -0
  120. package/dist/vitest.config.d.ts.map +1 -0
  121. package/dist/vitest.config.js +7 -0
  122. package/docs/README.md +124 -124
  123. package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
  124. package/examples/schemeresource.module-hints.json +38 -38
  125. package/examples/schemeresource.observable.code_list.md +37 -37
  126. package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
  127. package/examples/zwplace-placecategory-validation.json +29 -29
  128. package/examples/zwplace.module-hints.json +69 -69
  129. package/examples/zwplace.observable.code_list.md +37 -37
  130. package/package.json +19 -13
  131. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +0 -72
  132. package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +0 -233
  133. package/codex-skill/ep-stage/page-element-scan/SKILL.md +0 -91
  134. package/codex-skill/ep-stage/page-element-scan/evals/evals.json +0 -37
  135. package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +0 -85
  136. package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +0 -153
  137. package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +0 -233
  138. package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +0 -298
  139. package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +0 -749
  140. package/dist/src/cli/probe.d.ts +0 -46
  141. package/dist/src/cli/probe.d.ts.map +0 -1
  142. package/dist/src/cli/probe.js +0 -447
  143. package/dist/src/cli/run-report.d.ts +0 -46
  144. package/dist/src/cli/run-report.d.ts.map +0 -1
  145. package/dist/src/cli/run-report.js +0 -316
  146. package/dist/src/extractors/prd-extractor.d.ts +0 -70
  147. package/dist/src/extractors/prd-extractor.d.ts.map +0 -1
  148. package/dist/src/extractors/prd-extractor.js +0 -248
  149. package/dist/src/testcase/button-interaction-inference.d.ts +0 -37
  150. package/dist/src/testcase/button-interaction-inference.d.ts.map +0 -1
  151. package/dist/src/testcase/button-interaction-inference.js +0 -385
  152. package/dist/src/testcase/prd-button-mapper.d.ts +0 -72
  153. package/dist/src/testcase/prd-button-mapper.d.ts.map +0 -1
  154. package/dist/src/testcase/prd-button-mapper.js +0 -148
  155. package/dist/src/testcase/testcase-v2.d.ts +0 -50
  156. package/dist/src/testcase/testcase-v2.d.ts.map +0 -1
  157. package/dist/src/testcase/testcase-v2.js +0 -1
  158. package/docs/mvp-usage-guide.md +0 -298
@@ -1,166 +1,127 @@
1
- # playwright-cli O-D-A-V
2
-
3
- 本文件用于 `/glue-run-test` 中的 Agent Runtime 推理阶段。`playwright-cli` 是 Playwright 官方为 coding agent 设计的一次性子命令 + 后台 daemon 工具(`https://playwright.dev/agent-cli/introduction`,v0.1.15+)。Agent 每次跑单个子命令,浏览器状态由后台 daemon 跨命令保持;不是 REPL、不需要长驻 stdin 通道,与 Claude Code 无头 Bash 单次调用模型天然对齐。
4
-
5
- `playwright-cli` 不是推理引擎;Agent 读取 `ai-runtime-trace.json5.tasks[]` 后,用它观察和操作真实页面,然后按 dev-rules.md 把原料改写为 `test()` snippet 写入 `ai-snippets.json5`。
6
-
7
- ## 前置
8
-
9
- - 本机全局安装 `playwright-cli`:`which playwright-cli` 有输出。
10
- - 目标业务系统网络可达(`credentials.json5.loginUrl` / `contract.base.navigation.directUrl`)。
11
- - 已有 `credentials.json5`,至少一条完整业务角色凭据(`url / username / password` 非空)。
12
-
13
- ## 输入
14
-
15
- - `<caseDir>/.ep-stage/ai-runtime-trace.json5`:CLI `prepare-tasks` 产出的任务包(tasks[])。
16
- - `<caseDir>/testcase.json5`:`testAccess.requiredRoles[0]` = primary role,`contract.base.navigation.directUrl` = 列表页直达链接。
17
- - `<caseDir>/credentials.json5`:按 primary role 取 `loginUrl / username / password`。
18
- - `docs/00-memory/dev-rules.md`:snippet 代码规范,必须先读。
19
- - `<caseDir>/crud.spec.ts`:glue 骨架 spec,参考已就位的 `crudSlots / exportSlots / directListUrl / credentialsPath / primaryRole` 常量。
20
-
21
- ## Session 生命周期
22
-
23
- 每次 AI runtime 用一个 daemon session 名,建议 `<moduleId>-<timestamp>`(如 `zwplace-20260703011800`)。**所有子命令都必须传 `-s=<name>`**,daemon 会在浏览器进程里保持登录状态 / cookies / localStorage:
24
-
25
- - 开始:`playwright-cli -s=<name> open <loginUrl> --headed`。**必须传 `--headed`**:AI runtime 钻探期间要求人工旁观真实浏览器,验证 O-D-A-V 步骤符合业务意图;不允许 headless 默认,避免"看不见的黑盒操作"导致 snippet 幻觉进入 spec。
26
- - 全程:命名不变,Agent 每步传 `-s=<name>`。
27
- - 结束:`playwright-cli -s=<name> close`;异常兜底 `playwright-cli kill-all`。
28
- - 清理:`playwright-cli -s=<name> delete-data` 清 session 存储。
29
- - 枚举:`playwright-cli list` 查现存 session。
30
-
31
- ## 执行剧本
32
-
33
- ### Step 1 — 登录
34
-
35
- ```bash
36
- SESSION="<moduleId>-$(date +%Y%m%d%H%M%S)"
37
- playwright-cli -s="$SESSION" open <loginUrl> --headed # 强制有头:AI runtime 钻探必须可视化,禁止无头
38
- playwright-cli -s="$SESSION" snapshot # 输出 .playwright-cli/page-*.yml,含 accessibility tree + refs
39
- # 读 snapshot yml 找账号 / 密码 / 登录按钮的 ref(形如 e12, e13, e15)
40
- playwright-cli -s="$SESSION" fill e12 "<username>"
41
- playwright-cli -s="$SESSION" fill e13 "<password>"
42
- playwright-cli -s="$SESSION" click e15
43
- playwright-cli -s="$SESSION" snapshot # 验证登录成功(URL 或首页 DOM 特征)
44
- playwright-cli -s="$SESSION" state-save <caseDir>/.ep-stage/auth.json # 可选:状态保存
45
- ```
46
-
47
- ### Step 2 — 直达列表页
48
-
49
- ```bash
50
- playwright-cli -s="$SESSION" goto <directUrl>
51
- playwright-cli -s="$SESSION" snapshot # 观察列表页 iframe / 表格 / 按钮 refs
52
- ```
53
-
54
- ### Step 3 — 对每条 AI task 做 O-D-A-V
55
-
56
- | 阶段 | Agent 行为 | playwright-cli 原子命令 |
57
- |---|---|---|
58
- | Observe | 观察当前页面 / iframe / 控件 / 弹窗 / 列表结构,读 accessibility tree yml 拿 refs | `snapshot` |
59
- | Decide | 对照 task.businessIntent / assertionExpectation 决定下一动作 + 期望断言 | Agent 推理(不调 CLI) |
60
- | Act | 在真实页面试操作 | `click <ref>` / `fill <ref> <text>` / `select <ref> <val>` / `check <ref>` / `hover <ref>` / `press <key>` / `drag <src> <dst>` / `upload <ref> <file>` |
61
- | Verify | 二次 snapshot 或抽取具体值形成断言 | `snapshot` / `eval "<js>"` / `console` / `network` |
62
-
63
- 关键点:
64
-
65
- - **每一步 Act 之后必须 snapshot / eval 验证动作生效**,才能进入下一步 Act。`click` / `fill` / `select` 等 CLI 命令 exit=0 只代表 shell 调用无异常,**不代表页面 state 真的变了**:hover-only 展开的下拉、异步加载的 iframe、表单前置校验拦截、遮罩层拦截都可能让浏览器静默无响应而 CLI 依然 pass。发现动作无生效 → 先补前置动作(`hover` 触发展开、`snapshot` 等待重挂载、`press Tab` 触发 blur)再 snapshot 二次确认,**不要**在假设动作已生效的前提下径直进入下一步 Act,否则会产生"silent no-op"假通过污染 snippet。
66
- - **refs 每次 snapshot 后重新编号**,不要跨 snapshot 缓存 ref。每次动作前先 snapshot 拿新 ref。
67
- - `eval "<js>"` 用于抽取具体 DOM 内容作为断言值(如 `document.querySelector('#toast').innerText`);只用于验证不用于动作。
68
- - 弹窗遮罩 / iframe 变化 → 必须 snapshot 重新拿 refs。
69
- - 每条 task 探索完,把当前 snapshot yml 路径记入 result.evidence,支撑可观测性 gate("无来源不入契约")。
70
-
71
- ### Step 3b表单填充策略(新增弹框 / 编辑弹框)
72
-
73
- 弹窗表单在子 iframe 内,**playwright-cli `click <ref>` / `fill <ref>` 够不着 iframe 元素**(snapshot refs 只覆盖主 frame)。iframe 内操作一律走 `run-code` + `page.frames()` 定位:
74
-
75
- ```bash
76
- playwright-cli -s=$SESSION run-code "async page => { const f = page.frames().find(fr => fr.url().includes('<addIframeSrcKeyword>')); ... }"
77
- ```
78
-
79
- 1. **优先策略 一键填充(clickIfExist 语义)**:
80
- - 读取 `crud.spec.ts` 中 `crudSlots.locators.addAutofillTrigger`(典型值为 `xpath=//*[@title="默认填充"]`)。
81
- - 用 run-code 在弹窗 frame 内探测该按钮,**找到才点,找不到(约 2 秒)直接跳过**——与骨架 `clickIfExist({ timeout: 2_000 })` 行为一致;部分页面本来就没有此按钮,不算失败。
82
- - 点击后验证字段已自动填入。一键填充能正确触发 MiniUI 组件验证、正确选择下拉 option value。
83
-
84
- 2. **兜底策略 — run-code + Playwright locator API**(一键填充不存在时使用):
85
- - **文本字段**:`f.locator('xpath=//div[@label="场所名称"]//input[contains(@class,"mini-textbox-input")]').fill(value)`。Playwright `fill` 会触发完整事件链(focus → input → change),MiniUI 验证状态正常。
86
- - **下拉框(listbox / combobox)**:`f.locator('//div[@label="场所分类"]//span[@name="trigger"]').filter({ visible: true }).click()` 展开 → 选项定位**必须加 `.filter({ visible: true })`**:`f.locator('//table[contains(@class,"mini-listbox-items")]').filter({ visible: true }).locator('xpath=.//td[...]').first()`。MiniUI 的选项表在 DOM 中有**常驻隐藏副本**,不加 visible 过滤会拿到隐藏副本导致 waitFor 超时(与骨架 `listbox-component.ts` 行为一致)。
87
- - **禁止**用 `eval` 直接赋值 `input.value = "xxx"`——这会绕过 MiniUI 内部验证状态,保存时字段被识别为空(实测踩坑:值在 input 里可见,但保存报"不能为空")。
88
- - **禁止**直接填 option text 到 hidden value(如下拉隐藏域 `placecategory$value`)——显示文本与值会脱节。
89
- - 所有填写动作后必须验证字段值已生效(run-code 读回或补扫 elementMap),再进入下一步。
90
-
91
- ### Step 3c — 弹窗与 iframe 操作要点
92
-
93
- 1. **点击触发按钮(新增/修改/查看)后必须等弹窗 iframe 注册**:`page.waitForTimeout(1500~2000)` 或轮询 `page.frames().find(fr => fr.url().includes('<keyword>'))`。**找不到 frame 不要立即重点触发按钮**——弹窗可能正在打开,重复点击会被遮罩层拦截,造成假超时假象。
94
- 2. **弹窗 frame 定位优先用 contract 关键字**:`crudSlots.frames.addIframeSrcKeyword / editIframeSrcKeyword`(如 `gxhzwplaceadd` / `gxhzwplaceedit`;详情页形如 `gxhzwplace<module>detail`)。不要临时发明泛指关键字(如 `frameUrlSecretParam`)——会同时匹配新增/编辑/详情多个弹窗 frame,定位失真。
95
- 3. **run-code 沙箱能力边界**:沙箱内只有 Playwright API(`page` 及入参),**无 Node 内置模块**——`require` 未定义、dynamic `import()` 报 `ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING`。需要 fs 能力时(如验证下载文件大小):run-code 内只取 `download.path()` / `download.suggestedFilename()` 返回,文件大小在 bash 侧 `ls -la` 验证。
96
- 4. **弹窗关闭判断**:`!page.frames().some(fr => fr.url().includes('<keyword>'))` 为 true 即弹窗已关。「保存」类按钮业务语义是不关弹窗(草稿继续编辑),「保存并关闭」才关——不要用错预期。
97
-
98
- ### Step 4 Snippet 改写
99
-
100
- 按 `dev-rules.md` 把 playwright-cli 观察 / 动作原料转成 `test()` snippet:
101
-
102
- - 不直接照搬 playwright-cli 里的 ref 编号(`e12`)到 spec;spec 里用 Playwright locator API(`page.getByRole` / `page.getByLabel` / `page.locator('xpath=...')`),从 snapshot 里读元素的 role/name/text 语义信息生成稳定 locator。
103
- - 元素定义变量统一 `repo` 前缀:`repoBtn同步` / `repoInput场所名称` / `repoTable列表`。
104
- - 用锚点作用域缩小定位范围(列表行、弹框内),避免全页裸定位。
105
- - 保留 caseId 注释 + 标题前缀:`// caseId: TC01011` 注释与 test 标题 `TC01011 用例标题` 并存,便于 trace 对齐与报告按标题回显用例 ID。
106
- - 复用 `crud.spec.ts` 已声明的 `directListUrl / credentialsPath / primaryRole / crudSlots / exportSlots` 常量,不重复声明。
107
-
108
- ## 输出
109
-
110
- `<caseDir>/.ep-stage/ai-snippets.json5`:
111
-
112
- ```json5
113
- {
114
- schemaVersion: 'ep-stage-ai-snippets/v1',
115
- testcasePath: '<testcase.json5绝对路径>',
116
- generatedAt: '<ISO时间>',
117
- results: [
118
- {
119
- caseId: '<task.caseId>',
120
- status: 'resolved',
121
- observation: { summary: '<观察摘要>' },
122
- decision: { action: '<动作决策>' },
123
- actionFlow: ['<已验证动作>'],
124
- verification: { assertionExpectation: '<断言依据>' },
125
- specSnippet: "test('...', async ({ page }) => {\\n ...\\n});",
126
- evidence: [{ source: 'playwright-cli', note: '<证据说明,含 snapshot yml 路径>' }],
127
- },
128
- ],
129
- }
130
- ```
131
-
132
- > **specSnippet 必须是合法 JSON5 字符串**(单/双引号 + 转义),禁止用反引号模板字符串书写整个值——JSON5 不支持 `` ` ``,`run` 子命令解析 ai-snippets.json5 时会直接抛 `JSON5: invalid character '`'` 崩溃。snippet 代码内部含模板字符串(`` `...${...}...` ``)没问题,它只是字符串内容;建议用脚本 `JSON.stringify` 生成文件而非手写。
133
-
134
- 无法访问 `playwright-cli`、目标系统网络不通、页面信息不足或断言无法确认时,不强造 snippet:
135
-
136
- ```json5
137
- {
138
- caseId: '<task.caseId>',
139
- status: 'needs_review',
140
- observation: { summary: '<无法继续的原因>' },
141
- evidence: [{ source: 'agent-runtime', note: '<需要人审补充的信息>' }],
142
- }
143
- ```
144
-
145
- ## 附录 A — MiniUI 常见控件定位模式
146
-
147
- O-D-A-V 中反复用到的稳定模式,优先于临时推导:
148
-
149
- - **行勾选**:`#datagrid tr[<行条件>]//td[contains(@class,'checkcolumn')]//span`。**禁止**用 `td:first-child` / `td.first()`——首列未必是勾选列,点击不落 checkbox 时后续删除按钮静默无反应。勾选生效信号:行 `class` 含 `mini-grid-row-selected`。
150
- - **下拉弹层**:选项表在 DOM 中有常驻隐藏副本,必须 `.filter({ visible: true })` 锁定展开的那个(见 Step 3b)。
151
- - **列索引(td nth)**:行结构 = 前导列(勾选列 + 图标列,通常 2 个)+ `grid.columns` 业务列 + 操作列。写 snippet 前查 `elementmap.json` 里 grid 条目的 `columns` 数组做字段→列号映射:`nth = columns.indexOf(字段名) + 前导列数`,并对一行已知值实测验证(本模块实测:nth(3)=场所名称、nth(4)=场所分类、nth(6)=统一对外服务时间、nth(9)=状态、nth(10)=版本号)。不要凭肉眼从 snapshot 数格子。
152
- - **行内操作按钮**:`#datagrid tr[.//div[text()='<唯一值>']]//i[@data-tooltip='修改'|'查看']`(contract `listEditActionLocatorTemplate` 的 `{{value}}` 替换)。
153
- - **弹窗按钮**:`xpath=//a[contains(@class,'mini-button')][normalize-space()='保存'|'保存并关闭'|'关闭']` + `.filter({ visible: true })`;确认弹窗按钮 `text=确定`。
154
-
155
- ## 常用官方命令速查
156
-
157
- - Session:`-s=<name>` 前缀 / `list` / `close-all` / `kill-all` / `delete-data`
158
- - 页面:`open <url>` / `goto <url>` / `close` / `go-back` / `go-forward` / `reload`
159
- - 观察:`snapshot` / `screenshot` / `console` / `network` / `run-code`
160
- - 动作:`click / dblclick / hover <ref>` / `fill <ref> <text>` / `type <text>` / `select <ref> <val>` / `check / uncheck <ref>` / `press <key>` / `drag <src> <dst>` / `upload <ref> <file>`
161
- - 状态:`state-save [file]` / `state-load <file>` / `cookie-*` / `localstorage-*` / `sessionstorage-*`
162
- - 求值:`eval "<js表达式>"`(仅单行单表达式;CLI 会包成 `() => (参数)`,含顶层 `;` / 多语句 / 函数声明直接报 `Passed function is not well-serializable!`)/ `run-code "page => expr"`(Node 侧函数,参数收到 Playwright `page` 对象;多语句用 `async page => { ... }` 块体,页面内 JS 走 `page.evaluate(() => { ... })`,iframe 走 `page.frames()`)
163
- - 标签:`tab-list` / `tab-new` / `tab-select` / `tab-close`
164
- - 对话框:`dialog-accept` / `dialog-dismiss`
165
-
166
- 如需最新完整命令列表,跑 `playwright-cli --help` 或访问 `https://playwright.dev/agent-cli/introduction`。
1
+ # playwright-cli O-D-A-V
2
+
3
+ 本文件用于 `/glue-run-test` 中的 Agent Runtime 推理阶段。`playwright-cli` 是 Playwright 官方为 coding agent 设计的一次性子命令 + 后台 daemon 工具(`https://playwright.dev/agent-cli/introduction`,v0.1.15+)。Agent 每次跑单个子命令,浏览器状态由后台 daemon 跨命令保持;不是 REPL、不需要长驻 stdin 通道,与 Claude Code 无头 Bash 单次调用模型天然对齐。
4
+
5
+ `playwright-cli` 不是推理引擎;Agent 读取 `ai-runtime-trace.json5.tasks[]` 后,用它观察和操作真实页面,然后按 dev-rules.md 把原料改写为 `test()` snippet 写入 `ai-snippets.json5`。
6
+
7
+ ## 前置
8
+
9
+ - 本机全局安装 `playwright-cli`:`which playwright-cli` 有输出。
10
+ - 目标业务系统网络可达(`credentials.json5.loginUrl` / `contract.base.navigation.directUrl`)。
11
+ - 已有 `credentials.json5`,至少一条完整业务角色凭据(`url / username / password` 非空)。
12
+
13
+ ## 输入
14
+
15
+ - `<caseDir>/.ep-stage/ai-runtime-trace.json5`:CLI `prepare-tasks` 产出的任务包(tasks[])。
16
+ - `<caseDir>/testcase.json5`:`testAccess.requiredRoles[0]` = primary role,`contract.base.navigation.directUrl` = 列表页直达链接。
17
+ - `<caseDir>/credentials.json5`:按 primary role 取 `loginUrl / username / password`。
18
+ - `docs/00-memory/dev-rules.md`:snippet 代码规范,必须先读。
19
+ - `<caseDir>/crud.spec.ts`:glue 骨架 spec,参考已就位的 `crudSlots / exportSlots / directListUrl / credentialsPath / primaryRole` 常量。
20
+
21
+ ## Session 生命周期
22
+
23
+ 每次 AI runtime 用一个 daemon session 名,建议 `<moduleId>-<timestamp>`(如 `zwplace-20260703011800`)。**所有子命令都必须传 `-s=<name>`**,daemon 会在浏览器进程里保持登录状态 / cookies / localStorage:
24
+
25
+ - 开始:`playwright-cli -s=<name> open <loginUrl> --headed`。**必须传 `--headed`**:AI runtime 钻探期间要求人工旁观真实浏览器,验证 O-D-A-V 步骤符合业务意图;不允许 headless 默认,避免"看不见的黑盒操作"导致 snippet 幻觉进入 spec。
26
+ - 全程:命名不变,Agent 每步传 `-s=<name>`。
27
+ - 结束:`playwright-cli -s=<name> close`;异常兜底 `playwright-cli kill-all`。
28
+ - 清理:`playwright-cli -s=<name> delete-data` 清 session 存储。
29
+ - 枚举:`playwright-cli list` 查现存 session。
30
+
31
+ ## 执行剧本
32
+
33
+ ### Step 1 — 登录
34
+
35
+ ```bash
36
+ SESSION="<moduleId>-$(date +%Y%m%d%H%M%S)"
37
+ playwright-cli -s="$SESSION" open <loginUrl> --headed # 强制有头:AI runtime 钻探必须可视化,禁止无头
38
+ playwright-cli -s="$SESSION" snapshot # 输出 .playwright-cli/page-*.yml,含 accessibility tree + refs
39
+ # 读 snapshot yml 找账号 / 密码 / 登录按钮的 ref(形如 e12, e13, e15)
40
+ playwright-cli -s="$SESSION" fill e12 "<username>"
41
+ playwright-cli -s="$SESSION" fill e13 "<password>"
42
+ playwright-cli -s="$SESSION" click e15
43
+ playwright-cli -s="$SESSION" snapshot # 验证登录成功(URL 或首页 DOM 特征)
44
+ playwright-cli -s="$SESSION" state-save <caseDir>/.ep-stage/auth.json # 可选:状态保存
45
+ ```
46
+
47
+ ### Step 2 — 直达列表页
48
+
49
+ ```bash
50
+ playwright-cli -s="$SESSION" goto <directUrl>
51
+ playwright-cli -s="$SESSION" snapshot # 观察列表页 iframe / 表格 / 按钮 refs
52
+ ```
53
+
54
+ ### Step 3 — 对每条 AI task 做 O-D-A-V
55
+
56
+ | 阶段 | Agent 行为 | playwright-cli 原子命令 |
57
+ |---|---|---|
58
+ | Observe | 观察当前页面 / iframe / 控件 / 弹窗 / 列表结构,读 accessibility tree yml 拿 refs | `snapshot` |
59
+ | Decide | 对照 task.businessIntent / assertionExpectation 决定下一动作 + 期望断言 | Agent 推理(不调 CLI) |
60
+ | Act | 在真实页面试操作 | `click <ref>` / `fill <ref> <text>` / `select <ref> <val>` / `check <ref>` / `hover <ref>` / `press <key>` / `drag <src> <dst>` / `upload <ref> <file>` |
61
+ | Verify | 二次 snapshot 或抽取具体值形成断言 | `snapshot` / `eval "<js>"` / `console` / `network` |
62
+
63
+ 关键点:
64
+
65
+ - **每一步 Act 之后必须 snapshot / eval 验证动作生效**,才能进入下一步 Act。`click` / `fill` / `select` 等 CLI 命令 exit=0 只代表 shell 调用无异常,**不代表页面 state 真的变了**:hover-only 展开的下拉、异步加载的 iframe、表单前置校验拦截、遮罩层拦截都可能让浏览器静默无响应而 CLI 依然 pass。发现动作无生效 → 先补前置动作(`hover` 触发展开、`snapshot` 等待重挂载、`press Tab` 触发 blur)再 snapshot 二次确认,**不要**在假设动作已生效的前提下径直进入下一步 Act,否则会产生"silent no-op"假通过污染 snippet。
66
+ - **refs 每次 snapshot 后重新编号**,不要跨 snapshot 缓存 ref。每次动作前先 snapshot 拿新 ref。
67
+ - `eval "<js>"` 用于抽取具体 DOM 内容作为断言值(如 `document.querySelector('#toast').innerText`);只用于验证不用于动作。
68
+ - 弹窗遮罩 / iframe 变化 → 必须 snapshot 重新拿 refs。
69
+ - 每条 task 探索完,把当前 snapshot yml 路径记入 result.evidence,支撑可观测性 gate("无来源不入契约")。
70
+
71
+ ### Step 4Snippet 改写
72
+
73
+ `dev-rules.md` playwright-cli 观察 / 动作原料转成 `test()` snippet:
74
+
75
+ - 不直接照搬 playwright-cli 里的 ref 编号(`e12`)到 spec;spec 里用 Playwright locator API(`page.getByRole` / `page.getByLabel` / `page.locator('xpath=...')`),从 snapshot 里读元素的 role/name/text 语义信息生成稳定 locator。
76
+ - 元素定义变量统一 `repo` 前缀:`repoBtn同步` / `repoInput场所名称` / `repoTable列表`。
77
+ - 用锚点作用域缩小定位范围(列表行、弹框内),避免全页裸定位。
78
+ - 保留 caseId 注释:`// caseId: TC01011`,便于 trace 对齐。
79
+ - 复用 `crud.spec.ts` 已声明的 `directListUrl / credentialsPath / primaryRole / crudSlots / exportSlots` 常量,不重复声明。
80
+
81
+ ## 输出
82
+
83
+ 写 `<caseDir>/.ep-stage/ai-snippets.json5`:
84
+
85
+ ```json5
86
+ {
87
+ schemaVersion: 'ep-stage-ai-snippets/v1',
88
+ testcasePath: '<testcase.json5绝对路径>',
89
+ generatedAt: '<ISO时间>',
90
+ results: [
91
+ {
92
+ caseId: '<task.caseId>',
93
+ status: 'resolved',
94
+ observation: { summary: '<观察摘要>' },
95
+ decision: { action: '<动作决策>' },
96
+ actionFlow: ['<已验证动作>'],
97
+ verification: { assertionExpectation: '<断言依据>' },
98
+ specSnippet: "test('...', async ({ page }) => {\\n ...\\n});",
99
+ evidence: [{ source: 'playwright-cli', note: '<证据说明,含 snapshot yml 路径>' }],
100
+ },
101
+ ],
102
+ }
103
+ ```
104
+
105
+ 无法访问 `playwright-cli`、目标系统网络不通、页面信息不足或断言无法确认时,不强造 snippet:
106
+
107
+ ```json5
108
+ {
109
+ caseId: '<task.caseId>',
110
+ status: 'needs_review',
111
+ observation: { summary: '<无法继续的原因>' },
112
+ evidence: [{ source: 'agent-runtime', note: '<需要人审补充的信息>' }],
113
+ }
114
+ ```
115
+
116
+ ## 常用官方命令速查
117
+
118
+ - Session:`-s=<name>` 前缀 / `list` / `close-all` / `kill-all` / `delete-data`
119
+ - 页面:`open <url>` / `goto <url>` / `close` / `go-back` / `go-forward` / `reload`
120
+ - 观察:`snapshot` / `screenshot` / `console` / `network` / `run-code`
121
+ - 动作:`click / dblclick / hover <ref>` / `fill <ref> <text>` / `type <text>` / `select <ref> <val>` / `check / uncheck <ref>` / `press <key>` / `drag <src> <dst>` / `upload <ref> <file>`
122
+ - 状态:`state-save [file]` / `state-load <file>` / `cookie-*` / `localstorage-*` / `sessionstorage-*`
123
+ - 求值:`eval "<js表达式>"` / `run-code "<js多语句>"`
124
+ - 标签:`tab-list` / `tab-new` / `tab-select` / `tab-close`
125
+ - 对话框:`dialog-accept` / `dialog-dismiss`
126
+
127
+ 如需最新完整命令列表,跑 `playwright-cli --help` 或访问 `https://playwright.dev/agent-cli/introduction`。
@@ -1,149 +1,149 @@
1
- #!/usr/bin/env node
2
- import { existsSync, mkdirSync, readFileSync } from 'node:fs';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import { spawnSync } from 'node:child_process';
6
-
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
- const skillRoot = path.resolve(__dirname, '..');
9
- const packageRoot = path.resolve(skillRoot, '../..');
10
- const repoRoot = path.resolve(packageRoot, '../..');
11
-
12
- function help() {
13
- console.log(`Usage:
14
- node scripts/generate-crud-spec.mjs \\
15
- --module-id <module-id> \\
16
- --docs <docs-dir> \\
17
- --code-list <code-list-md> \\
18
- --webapp <html-dir> \\
19
- --java-actions <java-action-dir> \\
20
- --hints <module-hints-json> \\
21
- --contract-out <package-output-json> \\
22
- --spec-out <glue-project-spec-ts> \\
23
- --menu <menu-path>
24
-
25
- Notes:
26
- - Relative upstream paths are resolved from the repository root.
27
- - The contract path is passed to @epoint-testtech/ep-stage-skill relative to its package root.
28
- - The script fails when unresolvedSlots is non-empty.
29
- `);
30
- }
31
-
32
- function parseArgs(argv) {
33
- const args = {};
34
- for (let index = 0; index < argv.length; index += 2) {
35
- const key = argv[index];
36
- const value = argv[index + 1];
37
- if (key === '--help' || key === '-h') {
38
- help();
39
- process.exit(0);
40
- }
41
- if (!key?.startsWith('--') || !value) {
42
- throw new Error(`Invalid argument near ${key ?? '<empty>'}`);
43
- }
44
- args[key.slice(2)] = value;
45
- }
46
- return args;
47
- }
48
-
49
- function resolveFromRepo(value) {
50
- return path.isAbsolute(value) ? value : path.resolve(repoRoot, value);
51
- }
52
-
53
- function requirePath(label, value) {
54
- if (!value) throw new Error(`Missing --${label}`);
55
- const resolved = resolveFromRepo(value);
56
- if (!existsSync(resolved)) throw new Error(`Path for --${label} does not exist: ${resolved}`);
57
- return resolved;
58
- }
59
-
60
- function run(commandArgs) {
61
- const result = spawnSync('pnpm', commandArgs, {
62
- cwd: repoRoot,
63
- stdio: 'inherit',
64
- shell: false
65
- });
66
- if (result.status !== 0) {
67
- process.exit(result.status ?? 1);
68
- }
69
- }
70
-
71
- const args = parseArgs(process.argv.slice(2));
72
- const moduleId = args['module-id'];
73
- if (!moduleId) throw new Error('Missing --module-id');
74
- const docs = requirePath('docs', args.docs);
75
- const codeList = requirePath('code-list', args['code-list']);
76
- const webapp = requirePath('webapp', args.webapp);
77
- const javaActions = requirePath('java-actions', args['java-actions']);
78
- const hints = requirePath('hints', args.hints);
79
- const contractOut = args['contract-out'] ?? `packages/ep-stage-skill/output/${moduleId}.crud.contract.json`;
80
- const specOut = args['spec-out'];
81
- const menu = args.menu;
82
- if (!specOut) throw new Error('Missing --spec-out');
83
- if (!menu) throw new Error('Missing --menu');
84
-
85
- const contractAbs = resolveFromRepo(contractOut);
86
- const contractForPackage = path.relative(packageRoot, contractAbs);
87
- mkdirSync(path.dirname(contractAbs), { recursive: true });
88
-
89
- run([
90
- '--filter',
91
- '@epoint-testtech/ep-stage-skill',
92
- 'generate:crud-contract',
93
- '--',
94
- '--module-id',
95
- moduleId,
96
- '--docs',
97
- docs,
98
- '--code-list',
99
- codeList,
100
- '--webapp',
101
- webapp,
102
- '--java-actions',
103
- javaActions,
104
- '--hints',
105
- hints,
106
- '--out',
107
- contractForPackage
108
- ]);
109
-
110
- const contract = JSON.parse(readFileSync(contractAbs, 'utf8'));
111
- if (contract.unresolvedSlots?.length) {
112
- console.error(JSON.stringify({
113
- error: 'Contract has unresolvedSlots; update ModuleHints before generating a final spec.',
114
- unresolvedSlots: contract.unresolvedSlots.map(({ slotId, reason, suggestedFormat }) => ({
115
- slotId,
116
- reason,
117
- suggestedFormat
118
- }))
119
- }, null, 2));
120
- process.exit(2);
121
- }
122
-
123
- const specAbs = resolveFromRepo(specOut);
124
- mkdirSync(path.dirname(specAbs), { recursive: true });
125
-
126
- run([
127
- '--filter',
128
- '@epoint-testtech/ep-stage-skill',
129
- 'generate:playwright-tests',
130
- '--',
131
- '--contract',
132
- contractForPackage,
133
- '--out',
134
- specAbs,
135
- '--menu',
136
- menu
137
- ]);
138
-
139
- console.log(JSON.stringify({
140
- module: contract.module,
141
- searchConditions: contract.searchConditions?.map(({ field, label, component, value }) => ({
142
- field,
143
- label,
144
- component,
145
- value
146
- })),
147
- contract: contractAbs,
148
- spec: specAbs
149
- }, null, 2));
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawnSync } from 'node:child_process';
6
+
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
8
+ const skillRoot = path.resolve(__dirname, '..');
9
+ const packageRoot = path.resolve(skillRoot, '../..');
10
+ const repoRoot = path.resolve(packageRoot, '../..');
11
+
12
+ function help() {
13
+ console.log(`Usage:
14
+ node scripts/generate-crud-spec.mjs \\
15
+ --module-id <module-id> \\
16
+ --docs <docs-dir> \\
17
+ --code-list <code-list-md> \\
18
+ --webapp <html-dir> \\
19
+ --java-actions <java-action-dir> \\
20
+ --hints <module-hints-json> \\
21
+ --contract-out <package-output-json> \\
22
+ --spec-out <glue-project-spec-ts> \\
23
+ --menu <menu-path>
24
+
25
+ Notes:
26
+ - Relative upstream paths are resolved from the repository root.
27
+ - The contract path is passed to @epoint-testtech/ep-stage-skill relative to its package root.
28
+ - The script fails when unresolvedSlots is non-empty.
29
+ `);
30
+ }
31
+
32
+ function parseArgs(argv) {
33
+ const args = {};
34
+ for (let index = 0; index < argv.length; index += 2) {
35
+ const key = argv[index];
36
+ const value = argv[index + 1];
37
+ if (key === '--help' || key === '-h') {
38
+ help();
39
+ process.exit(0);
40
+ }
41
+ if (!key?.startsWith('--') || !value) {
42
+ throw new Error(`Invalid argument near ${key ?? '<empty>'}`);
43
+ }
44
+ args[key.slice(2)] = value;
45
+ }
46
+ return args;
47
+ }
48
+
49
+ function resolveFromRepo(value) {
50
+ return path.isAbsolute(value) ? value : path.resolve(repoRoot, value);
51
+ }
52
+
53
+ function requirePath(label, value) {
54
+ if (!value) throw new Error(`Missing --${label}`);
55
+ const resolved = resolveFromRepo(value);
56
+ if (!existsSync(resolved)) throw new Error(`Path for --${label} does not exist: ${resolved}`);
57
+ return resolved;
58
+ }
59
+
60
+ function run(commandArgs) {
61
+ const result = spawnSync('pnpm', commandArgs, {
62
+ cwd: repoRoot,
63
+ stdio: 'inherit',
64
+ shell: false
65
+ });
66
+ if (result.status !== 0) {
67
+ process.exit(result.status ?? 1);
68
+ }
69
+ }
70
+
71
+ const args = parseArgs(process.argv.slice(2));
72
+ const moduleId = args['module-id'];
73
+ if (!moduleId) throw new Error('Missing --module-id');
74
+ const docs = requirePath('docs', args.docs);
75
+ const codeList = requirePath('code-list', args['code-list']);
76
+ const webapp = requirePath('webapp', args.webapp);
77
+ const javaActions = requirePath('java-actions', args['java-actions']);
78
+ const hints = requirePath('hints', args.hints);
79
+ const contractOut = args['contract-out'] ?? `packages/ep-stage-skill/output/${moduleId}.crud.contract.json`;
80
+ const specOut = args['spec-out'];
81
+ const menu = args.menu;
82
+ if (!specOut) throw new Error('Missing --spec-out');
83
+ if (!menu) throw new Error('Missing --menu');
84
+
85
+ const contractAbs = resolveFromRepo(contractOut);
86
+ const contractForPackage = path.relative(packageRoot, contractAbs);
87
+ mkdirSync(path.dirname(contractAbs), { recursive: true });
88
+
89
+ run([
90
+ '--filter',
91
+ '@epoint-testtech/ep-stage-skill',
92
+ 'generate:crud-contract',
93
+ '--',
94
+ '--module-id',
95
+ moduleId,
96
+ '--docs',
97
+ docs,
98
+ '--code-list',
99
+ codeList,
100
+ '--webapp',
101
+ webapp,
102
+ '--java-actions',
103
+ javaActions,
104
+ '--hints',
105
+ hints,
106
+ '--out',
107
+ contractForPackage
108
+ ]);
109
+
110
+ const contract = JSON.parse(readFileSync(contractAbs, 'utf8'));
111
+ if (contract.unresolvedSlots?.length) {
112
+ console.error(JSON.stringify({
113
+ error: 'Contract has unresolvedSlots; update ModuleHints before generating a final spec.',
114
+ unresolvedSlots: contract.unresolvedSlots.map(({ slotId, reason, suggestedFormat }) => ({
115
+ slotId,
116
+ reason,
117
+ suggestedFormat
118
+ }))
119
+ }, null, 2));
120
+ process.exit(2);
121
+ }
122
+
123
+ const specAbs = resolveFromRepo(specOut);
124
+ mkdirSync(path.dirname(specAbs), { recursive: true });
125
+
126
+ run([
127
+ '--filter',
128
+ '@epoint-testtech/ep-stage-skill',
129
+ 'generate:playwright-tests',
130
+ '--',
131
+ '--contract',
132
+ contractForPackage,
133
+ '--out',
134
+ specAbs,
135
+ '--menu',
136
+ menu
137
+ ]);
138
+
139
+ console.log(JSON.stringify({
140
+ module: contract.module,
141
+ searchConditions: contract.searchConditions?.map(({ field, label, component, value }) => ({
142
+ field,
143
+ label,
144
+ component,
145
+ value
146
+ })),
147
+ contract: contractAbs,
148
+ spec: specAbs
149
+ }, null, 2));