@epoint-testtech/ep-stage-skill 0.0.4-alpha.1 → 0.0.6
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.
- package/SKILL.md +27 -27
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -206
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -274
- package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -112
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -72
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +295 -289
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -0
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -145
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -43
- package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +166 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +91 -0
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +37 -0
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +85 -0
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +153 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +298 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +749 -0
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +98 -95
- package/dist/src/cli/generate-testcase.d.ts +17 -51
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +74 -181
- package/dist/src/cli/index.d.ts +3 -2
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +51 -17
- package/dist/src/cli/run.d.ts +136 -29
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +507 -65
- package/dist/src/cli/spec.d.ts +17 -13
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +59 -33
- package/dist/src/contracts/crud-business-module.d.ts +6 -0
- package/dist/src/contracts/crud-business-module.d.ts.map +1 -1
- package/dist/src/contracts/glue-contract-v1.d.ts +98 -0
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -0
- package/dist/src/contracts/glue-contract-v1.js +9 -0
- package/dist/src/contracts/observable-chain.d.ts +4 -0
- package/dist/src/contracts/observable-chain.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +192 -0
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -0
- package/dist/src/extractors/code-list-json5.js +91 -0
- package/dist/src/extractors/html-page.d.ts +9 -0
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +26 -2
- package/dist/src/extractors/prd-extractor.d.ts +70 -0
- package/dist/src/extractors/prd-extractor.d.ts.map +1 -0
- package/dist/src/extractors/prd-extractor.js +248 -0
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +87 -20
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +647 -238
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/normalizers/crud-contract.d.ts +3 -0
- package/dist/src/normalizers/crud-contract.d.ts.map +1 -1
- package/dist/src/normalizers/crud-contract.js +3 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts +88 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -0
- package/dist/src/runtime/ai-runtime-trace.js +119 -0
- package/dist/src/testcase/button-interaction-inference.d.ts +37 -0
- package/dist/src/testcase/button-interaction-inference.d.ts.map +1 -0
- package/dist/src/testcase/button-interaction-inference.js +385 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts +44 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -0
- package/dist/src/testcase/contract-testcase-assembler.js +806 -0
- package/dist/src/testcase/prd-button-mapper.d.ts +72 -0
- package/dist/src/testcase/prd-button-mapper.d.ts.map +1 -0
- package/dist/src/testcase/prd-button-mapper.js +148 -0
- package/dist/src/testcase/runtime-context.d.ts +9 -0
- package/dist/src/testcase/runtime-context.d.ts.map +1 -0
- package/dist/src/testcase/runtime-context.js +1 -0
- package/dist/src/testcase/test-access.d.ts +38 -0
- package/dist/src/testcase/test-access.d.ts.map +1 -0
- package/dist/src/testcase/test-access.js +106 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts +10 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts.map +1 -0
- package/dist/src/testcase/testcase-execution-fields.js +1 -0
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
- package/dist/src/testcase/testcase-generator.js +6 -0
- package/dist/src/testcase/testcase-markdown.d.ts +11 -0
- package/dist/src/testcase/testcase-markdown.d.ts.map +1 -0
- package/dist/src/testcase/testcase-markdown.js +181 -0
- package/dist/src/testcase/testcase-skeleton.d.ts +4 -0
- package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -1
- package/dist/src/testcase/testcase-spec-assembly.d.ts +4 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -1
- package/dist/src/testcase/testcase-v1-draft.d.ts +58 -0
- package/dist/src/testcase/testcase-v1-draft.d.ts.map +1 -0
- package/dist/src/testcase/testcase-v1-draft.js +13 -0
- package/dist/src/validation/contract.d.ts +14 -0
- package/dist/src/validation/contract.d.ts.map +1 -0
- package/dist/src/validation/contract.js +132 -0
- package/dist/src/validation/credentials.d.ts +7 -2
- package/dist/src/validation/credentials.d.ts.map +1 -1
- package/dist/src/validation/credentials.js +15 -4
- package/dist/src/validation/index.d.ts +1 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +1 -0
- package/dist/src/validation/testcase.d.ts +9 -7
- package/dist/src/validation/testcase.d.ts.map +1 -1
- package/dist/src/validation/testcase.js +181 -34
- package/docs/README.md +124 -134
- package/docs/mvp-usage-guide.md +298 -298
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
- package/examples/schemeresource.module-hints.json +38 -38
- package/examples/schemeresource.observable.code_list.md +37 -37
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
- package/examples/zwplace-placecategory-validation.json +29 -29
- package/examples/zwplace.module-hints.json +69 -69
- package/examples/zwplace.observable.code_list.md +37 -37
- package/package.json +2 -2
- package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +0 -10
- package/dist/test/cli/extract-contract.test.d.ts +0 -2
- package/dist/test/cli/extract-contract.test.d.ts.map +0 -1
- package/dist/test/cli/extract-contract.test.js +0 -32
- package/dist/test/cli/generate-testcase-inference.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase-inference.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase-inference.test.js +0 -63
- package/dist/test/cli/generate-testcase-paths.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase-paths.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase-paths.test.js +0 -165
- package/dist/test/cli/generate-testcase-requirements.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase-requirements.test.js +0 -229
- package/dist/test/cli/generate-testcase.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase.test.js +0 -130
- package/dist/test/cli/index.test.d.ts +0 -2
- package/dist/test/cli/index.test.d.ts.map +0 -1
- package/dist/test/cli/index.test.js +0 -113
- package/dist/test/cli/probe.test.d.ts +0 -2
- package/dist/test/cli/probe.test.d.ts.map +0 -1
- package/dist/test/cli/probe.test.js +0 -31
- package/dist/test/cli/run.test.d.ts +0 -2
- package/dist/test/cli/run.test.d.ts.map +0 -1
- package/dist/test/cli/run.test.js +0 -149
- package/dist/test/cli/spec-boundary.test.d.ts +0 -2
- package/dist/test/cli/spec-boundary.test.d.ts.map +0 -1
- package/dist/test/cli/spec-boundary.test.js +0 -198
- package/dist/test/cli/spec.test.d.ts +0 -2
- package/dist/test/cli/spec.test.d.ts.map +0 -1
- package/dist/test/cli/spec.test.js +0 -196
- package/dist/test/code-list.test.d.ts +0 -2
- package/dist/test/code-list.test.d.ts.map +0 -1
- package/dist/test/code-list.test.js +0 -39
- package/dist/test/crud-contract.test.d.ts +0 -2
- package/dist/test/crud-contract.test.d.ts.map +0 -1
- package/dist/test/crud-contract.test.js +0 -799
- package/dist/test/gap-inference.test.d.ts +0 -2
- package/dist/test/gap-inference.test.d.ts.map +0 -1
- package/dist/test/gap-inference.test.js +0 -597
- package/dist/test/generalization.test.d.ts +0 -2
- package/dist/test/generalization.test.d.ts.map +0 -1
- package/dist/test/generalization.test.js +0 -73
- package/dist/test/inference/module-hints-inference.test.d.ts +0 -2
- package/dist/test/inference/module-hints-inference.test.d.ts.map +0 -1
- package/dist/test/inference/module-hints-inference.test.js +0 -25
- package/dist/test/inference/source-analysis.test.d.ts +0 -2
- package/dist/test/inference/source-analysis.test.d.ts.map +0 -1
- package/dist/test/inference/source-analysis.test.js +0 -23
- package/dist/test/material-inventory.test.d.ts +0 -2
- package/dist/test/material-inventory.test.d.ts.map +0 -1
- package/dist/test/material-inventory.test.js +0 -154
- package/dist/test/observable-chain.test.d.ts +0 -2
- package/dist/test/observable-chain.test.d.ts.map +0 -1
- package/dist/test/observable-chain.test.js +0 -123
- package/dist/test/observable-pipeline.test.d.ts +0 -2
- package/dist/test/observable-pipeline.test.d.ts.map +0 -1
- package/dist/test/observable-pipeline.test.js +0 -470
- package/dist/test/page-structure.test.d.ts +0 -2
- package/dist/test/page-structure.test.d.ts.map +0 -1
- package/dist/test/page-structure.test.js +0 -45
- package/dist/test/scenario-inference.test.d.ts +0 -2
- package/dist/test/scenario-inference.test.d.ts.map +0 -1
- package/dist/test/scenario-inference.test.js +0 -73
- package/dist/test/stage-context.test.d.ts +0 -2
- package/dist/test/stage-context.test.d.ts.map +0 -1
- package/dist/test/stage-context.test.js +0 -395
- package/dist/test/testcase-generator.test.d.ts +0 -2
- package/dist/test/testcase-generator.test.d.ts.map +0 -1
- package/dist/test/testcase-generator.test.js +0 -276
- package/dist/test/testcase-skeleton.test.d.ts +0 -2
- package/dist/test/testcase-skeleton.test.d.ts.map +0 -1
- package/dist/test/testcase-skeleton.test.js +0 -185
- package/dist/test/testcase-spec-assembly.test.d.ts +0 -2
- package/dist/test/testcase-spec-assembly.test.d.ts.map +0 -1
- package/dist/test/testcase-spec-assembly.test.js +0 -105
- package/dist/test/util/credentials.test.d.ts +0 -2
- package/dist/test/util/credentials.test.d.ts.map +0 -1
- package/dist/test/util/credentials.test.js +0 -64
- package/dist/test/util/i18n-testcase.test.d.ts +0 -2
- package/dist/test/util/i18n-testcase.test.d.ts.map +0 -1
- package/dist/test/util/i18n-testcase.test.js +0 -119
- package/dist/test/util/softlink.test.d.ts +0 -2
- package/dist/test/util/softlink.test.d.ts.map +0 -1
- package/dist/test/util/softlink.test.js +0 -82
- package/dist/test/validation/credentials.test.d.ts +0 -2
- package/dist/test/validation/credentials.test.d.ts.map +0 -1
- package/dist/test/validation/credentials.test.js +0 -72
- package/dist/test/validation/projects-index.test.d.ts +0 -2
- package/dist/test/validation/projects-index.test.d.ts.map +0 -1
- package/dist/test/validation/projects-index.test.js +0 -48
- package/dist/test/validation/testcase.test.d.ts +0 -2
- package/dist/test/validation/testcase.test.d.ts.map +0 -1
- package/dist/test/validation/testcase.test.js +0 -129
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -7
|
@@ -0,0 +1,166 @@
|
|
|
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`,便于 trace 对齐。
|
|
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`。
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract-page-elements-eval.mjs
|
|
3
|
+
*
|
|
4
|
+
* 专供 playwright-cli eval 注入使用的浏览器端扫描内核。
|
|
5
|
+
* !!!此文件无任何 import/export,仅包含纯 JavaScript 函数。
|
|
6
|
+
* !!!不要在 Node.js 环境中 import 此文件,会报 SyntaxError。
|
|
7
|
+
*
|
|
8
|
+
* 用法(shell 注入):
|
|
9
|
+
* playwright-cli -s=$SESSION eval "< $(cat extract-page-elements-eval.mjs) ; print(JSON.stringify(extractPageElements()))"
|
|
10
|
+
*
|
|
11
|
+
* 函数签名:
|
|
12
|
+
* extractPageElements() 当前 frame 扫描,返回 { title, elements[] }
|
|
13
|
+
* extractAllFramesElementMap() 所有 frame(含子 iframe)聚合,返回 { [label]: { locator, controlType, visible } }
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/* ─────────────────────────────────────────────────────────────────────────── */
|
|
17
|
+
|
|
18
|
+
function norm(s) { return (s || '').replace(/\s+/g, ' ').trim(); }
|
|
19
|
+
|
|
20
|
+
function isVisible(el) {
|
|
21
|
+
const st = window.getComputedStyle(el);
|
|
22
|
+
if (st.display === 'none' || st.visibility === 'hidden') return false;
|
|
23
|
+
const r = el.getBoundingClientRect();
|
|
24
|
+
return r.width > 0 && r.height > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isEnabled(el) {
|
|
28
|
+
const cls = el.className || '';
|
|
29
|
+
return !el.disabled
|
|
30
|
+
&& el.getAttribute('aria-disabled') !== 'true'
|
|
31
|
+
&& !String(cls).includes('mini-disabled')
|
|
32
|
+
&& !String(cls).includes('disabled');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function fieldLabel(el) {
|
|
36
|
+
// 分页控件不在表单字段采集范围内
|
|
37
|
+
if (el.classList.contains('pagination-number')) return '';
|
|
38
|
+
if (el.closest('.mini-pagination, .pagination-pagerchange')) return '';
|
|
39
|
+
const wrapper = el.closest('[label]');
|
|
40
|
+
if (wrapper) return norm(wrapper.getAttribute('label'));
|
|
41
|
+
if (el.getAttribute('aria-label')) return norm(el.getAttribute('aria-label'));
|
|
42
|
+
if (el.id) {
|
|
43
|
+
const lab = document.querySelector('label[for="' + el.id + '"]');
|
|
44
|
+
if (lab) return norm(lab.innerText);
|
|
45
|
+
}
|
|
46
|
+
const containerLabel = el.closest('div, td').querySelector('label');
|
|
47
|
+
if (containerLabel) return norm(containerLabel.innerText).replace(/[::]$/, '');
|
|
48
|
+
const prev = el.closest('td, div').previousElementSibling;
|
|
49
|
+
if (prev && /label|td/.test(prev.tagName.toLowerCase())) return norm(prev.innerText).replace(/[::]$/, '');
|
|
50
|
+
return norm(el.placeholder || el.getAttribute('bind') || el.name || el.id || '').replace(/^[ ]+|[ ]+$/g, '') || '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function controlType(el) {
|
|
54
|
+
const cls = String(el.className || '');
|
|
55
|
+
const mini = cls.match(/mini-[a-z]+/);
|
|
56
|
+
if (mini) return mini[0];
|
|
57
|
+
if (el.tagName === 'SELECT') return 'html-select';
|
|
58
|
+
if (el.tagName === 'TEXTAREA') return 'html-textarea';
|
|
59
|
+
return 'html-' + (el.type || 'input');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function hiddenInputLocator(el, label) {
|
|
63
|
+
const itemDiv = el.closest('.mini-radiobuttonlist-item, .mini-checkbox');
|
|
64
|
+
if (itemDiv && itemDiv.id) return '#' + itemDiv.id.replace(/[!\\#$%&'()*+,/:;<=>?@[\\]^`{|}~]/g, '\\$&');
|
|
65
|
+
if (label) {
|
|
66
|
+
const type = el.type === 'radio' ? 'mini-radiobuttonlist-item' : 'mini-checkbox';
|
|
67
|
+
return "xpath=//div[contains(@class,'" + type + "')][normalize-space()='" + label + "']";
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function fieldLocator(el, label) {
|
|
73
|
+
const type = controlType(el);
|
|
74
|
+
const tag = el.tagName.toLowerCase();
|
|
75
|
+
if (label && type.startsWith('mini-')) {
|
|
76
|
+
const exact = "xpath=//div[@label='" + label + "']//" + tag + "[contains(@class,'" + type + "') or contains(@class,'" + type + "-input')]";
|
|
77
|
+
if (document.querySelector("div[label='" + label + "']")) return exact;
|
|
78
|
+
var bareLabel = label.replace(/\d+/g, '').replace(/\s+/g, ' ').trim();
|
|
79
|
+
return "xpath=//div[@label and contains(@label,'" + bareLabel.replace(/[^一-龥a-zA-Z]/g, '') + "')]//" + tag + "[contains(@class,'" + type + "') or contains(@class,'" + type + "-input')]";
|
|
80
|
+
}
|
|
81
|
+
if (el.id) {
|
|
82
|
+
if (el.id.includes('$')) {
|
|
83
|
+
var prev = hiddenInputLocator(el, label);
|
|
84
|
+
if (prev) return prev;
|
|
85
|
+
}
|
|
86
|
+
return '#' + el.id.replace(/[!\\#$%&'()*+,/:;<=>?@[\\]^`{|}~]/g, '\\$&');
|
|
87
|
+
}
|
|
88
|
+
if (el.name) return tag + '[name="' + el.name + '"]';
|
|
89
|
+
if (label) return "xpath=//div[@label='" + label + "']//" + tag;
|
|
90
|
+
return tag;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buttonLocator(el, label) {
|
|
94
|
+
var tag = el.tagName.toLowerCase();
|
|
95
|
+
var cls = String(el.className || '');
|
|
96
|
+
if (cls.includes('mini-button')) return 'xpath=//' + tag + "[contains(@class,'mini-button')][normalize-space()='" + label + "']";
|
|
97
|
+
if (cls.includes('cond-srh-btn')) return "xpath=//a[contains(@class,'cond-srh-btn')][normalize-space()='" + label + "']";
|
|
98
|
+
if (el.id) return '#' + el.id;
|
|
99
|
+
return 'role=button[name="' + label + '"]';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* ─── extractPageElements() ─── */
|
|
103
|
+
|
|
104
|
+
function extractPageElements() {
|
|
105
|
+
var elements = [];
|
|
106
|
+
var seen = new Set();
|
|
107
|
+
function push(el) {
|
|
108
|
+
var key = el.type + '|' + el.label + '|' + el.locator;
|
|
109
|
+
if (seen.has(key)) return;
|
|
110
|
+
seen.add(key);
|
|
111
|
+
elements.push(el);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* 按钮 */
|
|
115
|
+
document.querySelectorAll('a.mini-button, button, input[type=button], input[type=submit], input[type=reset], [role=button], a.cond-srh-btn, a.mini-tools, span.mini-tools').forEach(function(el) {
|
|
116
|
+
var label = norm(el.innerText || el.value || el.getAttribute('data-tooltip') || el.title || '');
|
|
117
|
+
if (!label) return;
|
|
118
|
+
push({ type: 'button', label: label, locator: buttonLocator(el, label), visible: isVisible(el), enabled: isEnabled(el) });
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/* 表单字段 */
|
|
122
|
+
document.querySelectorAll('input:not([type=button]):not([type=submit]):not([type=reset]):not([type=hidden]), textarea, select').forEach(function(el) {
|
|
123
|
+
if (!isVisible(el) && el.closest('.mini-textbox, .mini-combobox, .mini-datepicker, .mini-spinner')) return;
|
|
124
|
+
if ((el.type === 'radio' || el.type === 'checkbox') && !isVisible(el)) return;
|
|
125
|
+
var label = fieldLabel(el);
|
|
126
|
+
if (!label) return;
|
|
127
|
+
push({ type: 'field', label: label, controlType: controlType(el), locator: fieldLocator(el, label), visible: isVisible(el), enabled: isEnabled(el) });
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
/* MiniUI RadioButtonList / CheckBox 独立控件 */
|
|
131
|
+
document.querySelectorAll('.mini-radiobuttonlist-item, .mini-checkbox').forEach(function(el) {
|
|
132
|
+
var input = el.querySelector('input[type=radio], input[type=checkbox]');
|
|
133
|
+
if (!input) return;
|
|
134
|
+
var label = norm(input.labels && input.labels[0] ? input.labels[0].innerText : el.innerText || '').replace(/[::]$/, '');
|
|
135
|
+
if (!label) return;
|
|
136
|
+
var numInput = el.querySelector('.mini-export-num');
|
|
137
|
+
if (numInput) label = label.replace(/\d+/g, '');
|
|
138
|
+
var isRadio = input.type === 'radio';
|
|
139
|
+
var itemId = el.id;
|
|
140
|
+
var locator = itemId ? ('#' + itemId.replace(/[!\\#$%&'()*+,/:;<=>?@[\\]^`{|}~]/g, '\\$&'))
|
|
141
|
+
: "xpath=//div[contains(@class,'" + (isRadio ? 'mini-radiobuttonlist-item' : 'mini-checkbox') + "')][contains(normalize-space(),'" + label + "')]";
|
|
142
|
+
push({ type: 'field', label: label, controlType: isRadio ? 'mini-radiobuttonlist' : 'mini-checkbox', locator: locator, visible: isVisible(el), enabled: isEnabled(el) });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/* 列表 */
|
|
146
|
+
document.querySelectorAll('.mini-datagrid').forEach(function(grid) {
|
|
147
|
+
var gridId = grid.id || 'datagrid';
|
|
148
|
+
var headers = [].map.call(grid.querySelectorAll('.mini-grid-headertxt, .mini-grid-headerText, th'), function(h) { return norm(h.innerText); }).filter(Boolean);
|
|
149
|
+
var rowActions = [].map.call(grid.querySelectorAll('[data-tooltip]'), function(a) { return norm(a.getAttribute('data-tooltip')); }).filter(Boolean);
|
|
150
|
+
push({ type: 'grid', label: gridId, locator: '#' + gridId, visible: isVisible(grid), enabled: true, columns: headers, rowActions: rowActions });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
/* 标签页 */
|
|
154
|
+
document.querySelectorAll('.mini-tabs li, [role=tab]').forEach(function(el) {
|
|
155
|
+
var label = norm(el.innerText);
|
|
156
|
+
if (!label) return;
|
|
157
|
+
push({ type: 'tab', label: label, locator: 'role=tab[name="' + label + '"]', visible: isVisible(el), enabled: isEnabled(el) });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/* 普通链接 */
|
|
161
|
+
document.querySelectorAll('a[href]:not(.mini-button)').forEach(function(el) {
|
|
162
|
+
var label = norm(el.innerText);
|
|
163
|
+
if (!label || !isVisible(el)) return;
|
|
164
|
+
push({ type: 'link', label: label, locator: 'role=link[name="' + label + '"]', visible: true, enabled: isEnabled(el), href: el.getAttribute('href') });
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/* MiniUI 分页控件 */
|
|
168
|
+
document.querySelectorAll('.mini-grid-pager').forEach(function(pager) {
|
|
169
|
+
var parentGrid = pager.closest('.mini-datagrid');
|
|
170
|
+
var gridId = (parentGrid && parentGrid.id) ? parentGrid.id : 'datagrid';
|
|
171
|
+
var prefix = parentGrid ? ('#' + gridId.replace(/[!\\#$%&'()*+,/:;<=>?@[\\]^`{|}~]/g, '\\$&')) : '';
|
|
172
|
+
|
|
173
|
+
/* 总条数文本 */
|
|
174
|
+
var pagerInfo = pager.querySelector('.pagination-pagerinfo');
|
|
175
|
+
if (pagerInfo) push({ type: 'pagination', label: norm(pagerInfo.innerText), locator: prefix + ' .pagination-pagerinfo', visible: true, enabled: true });
|
|
176
|
+
|
|
177
|
+
/* 分页按钮 */
|
|
178
|
+
var btns = pager.querySelectorAll('.pagination-buttons a');
|
|
179
|
+
btns.forEach(function(btn) {
|
|
180
|
+
var action = btn.getAttribute('action');
|
|
181
|
+
var isEllipsis = btn.classList.contains('pagination-button-ellipsis');
|
|
182
|
+
var label = norm(btn.innerText) || '';
|
|
183
|
+
var subType = '', suffix = '';
|
|
184
|
+
if (action === 'prev') { subType = 'pagination-prev'; label = '上一页'; suffix = '.pagination-buttons a[action=prev]'; }
|
|
185
|
+
else if (action === 'next') { subType = 'pagination-next'; label = '下一页'; suffix = '.pagination-buttons a[action=next]'; }
|
|
186
|
+
else if (isEllipsis || action === 'none') { subType = 'pagination-ellipsis'; label = '...'; suffix = '.pagination-button-ellipsis'; }
|
|
187
|
+
else { subType = 'pagination-page'; label = norm(btn.innerText); suffix = '.pagination-buttons a.pagination-button:not(.pagination-button-ellipsis)'; }
|
|
188
|
+
push({ type: 'pagination', subType: subType, label: label, locator: prefix + ' ' + suffix, visible: isVisible(btn), enabled: !btn.classList.contains('disabled') });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
/* 页面尺寸下拉 */
|
|
192
|
+
var pageSizeBtn = pager.querySelector('.mini-pagination > span.mini-buttonedit');
|
|
193
|
+
if (pageSizeBtn && isVisible(pageSizeBtn)) {
|
|
194
|
+
var hiddenVal = pageSizeBtn.querySelector('input[type=hidden]');
|
|
195
|
+
push({ type: 'pagination', subType: 'pagination-size', label: '页面尺寸', locator: prefix + ' .mini-pagination > span.mini-buttonedit', value: hiddenVal ? hiddenVal.value : '', visible: true, enabled: true });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* 页码跳转输入框 */
|
|
199
|
+
var jumpInput = pager.querySelector('.pagination-pagerchange input.pagination-number');
|
|
200
|
+
if (jumpInput && isVisible(jumpInput)) {
|
|
201
|
+
push({ type: 'pagination', subType: 'pagination-jump', label: '跳至页', locator: prefix + ' .pagination-pagerchange input.pagination-number', visible: true, enabled: isEnabled(jumpInput) });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
return { title: document.title, elements: elements };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ─── extractAllFramesElementMap() ─── */
|
|
209
|
+
|
|
210
|
+
function extractAllFramesElementMap() {
|
|
211
|
+
var result = {};
|
|
212
|
+
function merge(el) {
|
|
213
|
+
if (!el.label) return;
|
|
214
|
+
if (!result[el.label] || el.visible) result[el.label] = { locator: el.locator, controlType: el.controlType, visible: el.visible };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* 主 frame */
|
|
218
|
+
try { extractPageElements().elements.forEach(merge); } catch(e) {}
|
|
219
|
+
|
|
220
|
+
/* 子 frame */
|
|
221
|
+
try {
|
|
222
|
+
for (var i = 0; i < window.frames.length; i++) {
|
|
223
|
+
try {
|
|
224
|
+
var fr = window.frames[i];
|
|
225
|
+
if (fr.extractPageElements) {
|
|
226
|
+
fr.extractPageElements().elements.forEach(merge);
|
|
227
|
+
}
|
|
228
|
+
} catch(e) {}
|
|
229
|
+
}
|
|
230
|
+
} catch(e) {}
|
|
231
|
+
|
|
232
|
+
return result;
|
|
233
|
+
}
|