@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,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
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* extract-page-elements.mjs
|
|
3
|
+
*
|
|
4
|
+
* 可在浏览器上下文直接运行的 DOM 扫描内核(无外部依赖)。
|
|
5
|
+
* 用途:
|
|
6
|
+
* 1. 被 scan-page.mjs import 复用扫描逻辑
|
|
7
|
+
* 2. 被 playwright-cli eval "<call>" 注入到已有 session 中实时采集 element map
|
|
8
|
+
*
|
|
9
|
+
* playwright-cli eval 使用示例(主 frame):
|
|
10
|
+
* playwright-cli -s=$SESSION eval "< $(cat extract-page-elements.mjs) ; print(JSON.stringify(extractPageElements()))"
|
|
11
|
+
*
|
|
12
|
+
* playwright-cli eval 使用示例(遍历所有 frame):
|
|
13
|
+
* playwright-cli -s=$SESSION eval "< $(cat extract-page-elements.mjs) ; print(JSON.stringify(extractAllFramesElementMap()))"
|
|
14
|
+
*
|
|
15
|
+
* 导出函数签名:
|
|
16
|
+
* extractPageElements() -> { title, elements[] } 当前 frame 扫描
|
|
17
|
+
* extractAllFramesElementMap() -> { [label]: { locator, controlType, visible } } 所有 frame 聚合
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 单 frame 扫描内核(与 scan-page.mjs 内部逻辑保持同步)。
|
|
22
|
+
*/
|
|
23
|
+
function extractPageElements() {
|
|
24
|
+
const norm = (s) => (s || '').replace(/\s+/g, ' ').trim();
|
|
25
|
+
|
|
26
|
+
function isVisible(el) {
|
|
27
|
+
const style = window.getComputedStyle(el);
|
|
28
|
+
if (style.display === 'none' || style.visibility === 'hidden') return false;
|
|
29
|
+
const rect = el.getBoundingClientRect();
|
|
30
|
+
return rect.width > 0 && rect.height > 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function isEnabled(el) {
|
|
34
|
+
const cls = el.className || '';
|
|
35
|
+
return !el.disabled
|
|
36
|
+
&& el.getAttribute('aria-disabled') !== 'true'
|
|
37
|
+
&& !String(cls).includes('mini-disabled')
|
|
38
|
+
&& !String(cls).includes('disabled');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fieldLabel(el) {
|
|
42
|
+
// 分页控件(页码输入框、页面尺寸下拉框):不在表单字段采集范围内
|
|
43
|
+
if (el.classList.contains('pagination-number')) return '';
|
|
44
|
+
if (el.closest('.mini-pagination, .pagination-pagerchange')) return '';
|
|
45
|
+
const wrapper = el.closest('[label]');
|
|
46
|
+
if (wrapper) return norm(wrapper.getAttribute('label'));
|
|
47
|
+
if (el.getAttribute('aria-label')) return norm(el.getAttribute('aria-label'));
|
|
48
|
+
if (el.id) {
|
|
49
|
+
const lab = document.querySelector(`label[for="${el.id}"]`);
|
|
50
|
+
if (lab) return norm(lab.innerText);
|
|
51
|
+
}
|
|
52
|
+
const containerLabel = el.closest('div, td')?.querySelector('label');
|
|
53
|
+
if (containerLabel) return norm(containerLabel.innerText).replace(/[::]$/, '');
|
|
54
|
+
const prev = el.closest('td, div')?.previousElementSibling;
|
|
55
|
+
if (prev && /label|td/.test(prev.tagName.toLowerCase())) return norm(prev.innerText).replace(/[::]$/, '');
|
|
56
|
+
return norm(el.placeholder || el.getAttribute('bind') || el.name || el.id || '').replace(/^[ ]+|[ ]+$/g, '') || '';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function controlType(el) {
|
|
60
|
+
const cls = String(el.className || '');
|
|
61
|
+
const mini = cls.match(/mini-[a-z]+/);
|
|
62
|
+
if (mini) return mini[0];
|
|
63
|
+
if (el.tagName === 'SELECT') return 'html-select';
|
|
64
|
+
if (el.tagName === 'TEXTAREA') return 'html-textarea';
|
|
65
|
+
return `html-${el.type || 'input'}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function hiddenInputLocator(el, label) {
|
|
69
|
+
const itemDiv = el.closest('.mini-radiobuttonlist-item, .mini-checkbox');
|
|
70
|
+
if (itemDiv && itemDiv.id) return `#${CSS.escape(itemDiv.id)}`;
|
|
71
|
+
if (label) {
|
|
72
|
+
const type = el.type === 'radio' ? 'mini-radiobuttonlist-item' : 'mini-checkbox';
|
|
73
|
+
return `xpath=//div[contains(@class,'${type}')][normalize-space()='${label}']`;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function fieldLocator(el, label) {
|
|
79
|
+
const type = controlType(el);
|
|
80
|
+
const tag = el.tagName.toLowerCase();
|
|
81
|
+
if (label && type.startsWith('mini-')) {
|
|
82
|
+
const exact = `xpath=//div[@label='${label}']//${tag}[contains(@class,'${type}') or contains(@class,'${type}-input')]`;
|
|
83
|
+
if (document.querySelector(`div[label='${label}']`)) return exact;
|
|
84
|
+
const bareLabel = label.replace(/\d+/g, '').replace(/\s+/g, ' ').trim();
|
|
85
|
+
return `xpath=//div[@label and contains(@label,'${bareLabel.replace(/[^一-龥a-zA-Z]/g, '')}')]//${tag}[contains(@class,'${type}') or contains(@class,'${type}-input')]`;
|
|
86
|
+
}
|
|
87
|
+
if (el.id) {
|
|
88
|
+
if (el.id.includes('$')) {
|
|
89
|
+
const prev = hiddenInputLocator(el, label);
|
|
90
|
+
if (prev) return prev;
|
|
91
|
+
}
|
|
92
|
+
return `#${CSS.escape(el.id)}`;
|
|
93
|
+
}
|
|
94
|
+
if (el.name) return `${tag}[name="${el.name}"]`;
|
|
95
|
+
if (label) return `xpath=//div[@label='${label}']//${tag}`;
|
|
96
|
+
return tag;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buttonLocator(el, label) {
|
|
100
|
+
const tag = el.tagName.toLowerCase();
|
|
101
|
+
const cls = String(el.className || '');
|
|
102
|
+
if (cls.includes('mini-button')) return `xpath=//${tag}[contains(@class,'mini-button')][normalize-space()='${label}']`;
|
|
103
|
+
if (cls.includes('cond-srh-btn')) return `xpath=//a[contains(@class,'cond-srh-btn')][normalize-space()='${label}']`;
|
|
104
|
+
if (el.id) return `#${el.id}`;
|
|
105
|
+
return `role=button[name="${label}"]`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const elements = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
function push(el) {
|
|
111
|
+
const key = `${el.type}|${el.label}|${el.locator}`;
|
|
112
|
+
if (seen.has(key)) return;
|
|
113
|
+
seen.add(key);
|
|
114
|
+
elements.push(el);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 按钮
|
|
118
|
+
document.querySelectorAll(
|
|
119
|
+
'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',
|
|
120
|
+
).forEach((el) => {
|
|
121
|
+
const label = norm(el.innerText || el.value || el.getAttribute('data-tooltip') || el.title || '');
|
|
122
|
+
if (!label) return;
|
|
123
|
+
push({ type: 'button', label, locator: buttonLocator(el, label), visible: isVisible(el), enabled: isEnabled(el) });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// 表单字段
|
|
127
|
+
document.querySelectorAll(
|
|
128
|
+
'input:not([type=button]):not([type=submit]):not([type=reset]):not([type=hidden]), textarea, select',
|
|
129
|
+
).forEach((el) => {
|
|
130
|
+
if (!isVisible(el) && el.closest('.mini-textbox, .mini-combobox, .mini-datepicker, .mini-spinner')) return;
|
|
131
|
+
if ((el.type === 'radio' || el.type === 'checkbox') && !isVisible(el)) return;
|
|
132
|
+
const label = fieldLabel(el);
|
|
133
|
+
if (!label) return;
|
|
134
|
+
push({
|
|
135
|
+
type: 'field', label, controlType: controlType(el),
|
|
136
|
+
locator: fieldLocator(el, label),
|
|
137
|
+
visible: isVisible(el), enabled: isEnabled(el),
|
|
138
|
+
required: el.required || el.getAttribute('required') === 'true' || !!el.closest('.starred, [starred]'),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// MiniUI RadioButtonList / CheckBox 独立控件(父 div 可点击)
|
|
143
|
+
document.querySelectorAll('.mini-radiobuttonlist-item, .mini-checkbox').forEach((el) => {
|
|
144
|
+
const input = el.querySelector('input[type=radio], input[type=checkbox]');
|
|
145
|
+
if (!input) return;
|
|
146
|
+
let label = norm(input.labels?.[0]?.innerText || el.innerText || '').replace(/[::]$/, '');
|
|
147
|
+
if (!label) return;
|
|
148
|
+
const numInput = el.querySelector('.mini-export-num');
|
|
149
|
+
if (numInput) label = label.replace(/\d+/g, '');
|
|
150
|
+
const isRadio = input.type === 'radio';
|
|
151
|
+
const itemId = el.id;
|
|
152
|
+
const locator = itemId ? `#${CSS.escape(itemId)}`
|
|
153
|
+
: `xpath=//div[contains(@class,'${isRadio ? 'mini-radiobuttonlist-item' : 'mini-checkbox'})][contains(normalize-space(),'${label}')]`;
|
|
154
|
+
push({ type: 'field', label, controlType: isRadio ? 'mini-radiobuttonlist' : 'mini-checkbox', locator, visible: isVisible(el), enabled: isEnabled(el) });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 列表
|
|
158
|
+
document.querySelectorAll('.mini-datagrid').forEach((grid) => {
|
|
159
|
+
const gridId = grid.id || 'datagrid';
|
|
160
|
+
const headers = [...grid.querySelectorAll('.mini-grid-headertxt, .mini-grid-headerText, th')].map((h) => norm(h.innerText)).filter(Boolean);
|
|
161
|
+
const rowActions = [...grid.querySelectorAll('[data-tooltip]')].map((a) => norm(a.getAttribute('data-tooltip'))).filter(Boolean);
|
|
162
|
+
push({ type: 'grid', label: gridId, locator: `#${gridId}`, visible: isVisible(grid), enabled: true, columns: headers, rowActions: [...new Set(rowActions)] });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// 标签页
|
|
166
|
+
document.querySelectorAll('.mini-tabs li, [role=tab]').forEach((el) => {
|
|
167
|
+
const label = norm(el.innerText);
|
|
168
|
+
if (!label) return;
|
|
169
|
+
push({ type: 'tab', label, locator: `role=tab[name="${label}"]`, visible: isVisible(el), enabled: isEnabled(el) });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// 普通链接
|
|
173
|
+
document.querySelectorAll('a[href]:not(.mini-button)').forEach((el) => {
|
|
174
|
+
const label = norm(el.innerText);
|
|
175
|
+
if (!label || !isVisible(el)) return;
|
|
176
|
+
push({ type: 'link', label, locator: `role=link[name="${label}"]`, visible: true, enabled: isEnabled(el), href: el.getAttribute('href') });
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// MiniUI 分页控件
|
|
180
|
+
document.querySelectorAll('.mini-grid-pager').forEach((pager) => {
|
|
181
|
+
// 确定所属 grid 的 id
|
|
182
|
+
const parentGrid = pager.closest('.mini-datagrid');
|
|
183
|
+
const gridId = parentGrid?.id || 'datagrid';
|
|
184
|
+
const prefix = parentGrid ? `#${CSS.escape(gridId)}` : '';
|
|
185
|
+
|
|
186
|
+
// 总条数文本
|
|
187
|
+
const pagerInfo = pager.querySelector('.pagination-pagerinfo');
|
|
188
|
+
if (pagerInfo) {
|
|
189
|
+
const label = norm(pagerInfo.innerText); // 如 "共536条"
|
|
190
|
+
push({ type: 'pagination', label, locator: `${prefix} .pagination-pagerinfo`, visible: true, enabled: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 分页按钮组
|
|
194
|
+
const btns = pager.querySelectorAll('.pagination-buttons a');
|
|
195
|
+
btns.forEach((btn) => {
|
|
196
|
+
const action = btn.getAttribute('action');
|
|
197
|
+
const isEllipsis = btn.classList.contains('pagination-button-ellipsis');
|
|
198
|
+
let label = norm(btn.innerText) || '';
|
|
199
|
+
let subType = '';
|
|
200
|
+
let locatorSuffix = '';
|
|
201
|
+
|
|
202
|
+
if (action === 'prev') {
|
|
203
|
+
subType = 'pagination-prev';
|
|
204
|
+
label = '上一页';
|
|
205
|
+
locatorSuffix = `.pagination-buttons a[action=prev]`;
|
|
206
|
+
} else if (action === 'next') {
|
|
207
|
+
subType = 'pagination-next';
|
|
208
|
+
label = '下一页';
|
|
209
|
+
locatorSuffix = `.pagination-buttons a[action=next]`;
|
|
210
|
+
} else if (action === 'none' || isEllipsis) {
|
|
211
|
+
subType = 'pagination-ellipsis';
|
|
212
|
+
label = '...';
|
|
213
|
+
locatorSuffix = `.pagination-button-ellipsis`;
|
|
214
|
+
} else {
|
|
215
|
+
// 页码数字按钮
|
|
216
|
+
subType = 'pagination-page';
|
|
217
|
+
label = norm(btn.innerText); // "1", "2", "49"
|
|
218
|
+
locatorSuffix = `.pagination-buttons a.pagination-button:not(.pagination-button-ellipsis)`;
|
|
219
|
+
}
|
|
220
|
+
push({
|
|
221
|
+
type: 'pagination',
|
|
222
|
+
subType,
|
|
223
|
+
label,
|
|
224
|
+
locator: `${prefix} ${locatorSuffix}`,
|
|
225
|
+
visible: isVisible(btn),
|
|
226
|
+
enabled: !btn.classList.contains('disabled'),
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// 页面尺寸下拉(只采集可见的)
|
|
231
|
+
const pageSizeBtn = pager.querySelector('.mini-pagination > span.mini-buttonedit');
|
|
232
|
+
if (pageSizeBtn && isVisible(pageSizeBtn)) {
|
|
233
|
+
const hiddenVal = pageSizeBtn.querySelector('input[type=hidden]');
|
|
234
|
+
push({
|
|
235
|
+
type: 'pagination',
|
|
236
|
+
subType: 'pagination-size',
|
|
237
|
+
label: '页面尺寸',
|
|
238
|
+
locator: `${prefix} .mini-pagination > span.mini-buttonedit`,
|
|
239
|
+
value: hiddenVal?.value || '',
|
|
240
|
+
visible: true,
|
|
241
|
+
enabled: true,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 页码跳转输入框
|
|
246
|
+
const jumpInput = pager.querySelector('.pagination-pagerchange input.pagination-number');
|
|
247
|
+
if (jumpInput && isVisible(jumpInput)) {
|
|
248
|
+
push({
|
|
249
|
+
type: 'pagination',
|
|
250
|
+
subType: 'pagination-jump',
|
|
251
|
+
label: '跳至页',
|
|
252
|
+
locator: `${prefix} .pagination-pagerchange input.pagination-number`,
|
|
253
|
+
visible: true,
|
|
254
|
+
enabled: isEnabled(jumpInput),
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return { title: document.title, elements };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 遍历所有 frame(含子 iframe),对每个 frame 执行 extractPageElements(),
|
|
264
|
+
* 聚合所有 elements,去重后以 { label → { locator, controlType, visible } } 返回。
|
|
265
|
+
* 专供 playwright-cli eval 在 O-D-A-V Act 之后使用:捕获弹窗/子表等动态新增元素。
|
|
266
|
+
*/
|
|
267
|
+
function extractAllFramesElementMap() {
|
|
268
|
+
const result = {};
|
|
269
|
+
// 主 frame
|
|
270
|
+
try {
|
|
271
|
+
const main = extractPageElements();
|
|
272
|
+
for (const el of main.elements) {
|
|
273
|
+
if (!result[el.label] || el.visible) result[el.label] = { locator: el.locator, controlType: el.controlType, visible: el.visible };
|
|
274
|
+
}
|
|
275
|
+
} catch (_) {}
|
|
276
|
+
|
|
277
|
+
// 子 frame(通过 window.frames 枚举)
|
|
278
|
+
try {
|
|
279
|
+
for (let i = 0; i < window.frames.length; i++) {
|
|
280
|
+
const fr = window.frames[i];
|
|
281
|
+
try {
|
|
282
|
+
const r = fr.extractPageElements ? fr.extractPageElements() : null;
|
|
283
|
+
if (!r) continue;
|
|
284
|
+
for (const el of r.elements) {
|
|
285
|
+
if (!result[el.label] || el.visible) result[el.label] = { locator: el.locator, controlType: el.controlType, visible: el.visible };
|
|
286
|
+
}
|
|
287
|
+
} catch (_) { /* 跨域 frame 跳过 */ }
|
|
288
|
+
}
|
|
289
|
+
} catch (_) {}
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Node.js 环境:CommonJS 导出(被 scan-page.mjs import)
|
|
295
|
+
// 浏览器 / playwright-cli eval 环境:全局函数直接可用
|
|
296
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
297
|
+
module.exports = { extractPageElements, extractAllFramesElementMap };
|
|
298
|
+
}
|