@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.
Files changed (213) hide show
  1. package/SKILL.md +27 -27
  2. package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -206
  3. package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -274
  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 -112
  6. package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -72
  7. package/codex-skill/ep-stage/glue-run-test/SKILL.md +295 -289
  8. package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -0
  9. package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -145
  10. package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -43
  11. package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
  12. package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +166 -0
  13. package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +233 -0
  14. package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
  15. package/codex-skill/ep-stage/page-element-scan/SKILL.md +91 -0
  16. package/codex-skill/ep-stage/page-element-scan/evals/evals.json +37 -0
  17. package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +85 -0
  18. package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +153 -0
  19. package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +233 -0
  20. package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +298 -0
  21. package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +749 -0
  22. package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
  23. package/codex-skill/ep-stage/scripts/validate-skill.mjs +98 -95
  24. package/dist/src/cli/generate-testcase.d.ts +17 -51
  25. package/dist/src/cli/generate-testcase.d.ts.map +1 -1
  26. package/dist/src/cli/generate-testcase.js +74 -181
  27. package/dist/src/cli/index.d.ts +3 -2
  28. package/dist/src/cli/index.d.ts.map +1 -1
  29. package/dist/src/cli/index.js +51 -17
  30. package/dist/src/cli/run.d.ts +136 -29
  31. package/dist/src/cli/run.d.ts.map +1 -1
  32. package/dist/src/cli/run.js +507 -65
  33. package/dist/src/cli/spec.d.ts +17 -13
  34. package/dist/src/cli/spec.d.ts.map +1 -1
  35. package/dist/src/cli/spec.js +59 -33
  36. package/dist/src/contracts/crud-business-module.d.ts +6 -0
  37. package/dist/src/contracts/crud-business-module.d.ts.map +1 -1
  38. package/dist/src/contracts/glue-contract-v1.d.ts +98 -0
  39. package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -0
  40. package/dist/src/contracts/glue-contract-v1.js +9 -0
  41. package/dist/src/contracts/observable-chain.d.ts +4 -0
  42. package/dist/src/contracts/observable-chain.d.ts.map +1 -1
  43. package/dist/src/extractors/code-list-json5.d.ts +192 -0
  44. package/dist/src/extractors/code-list-json5.d.ts.map +1 -0
  45. package/dist/src/extractors/code-list-json5.js +91 -0
  46. package/dist/src/extractors/html-page.d.ts +9 -0
  47. package/dist/src/extractors/html-page.d.ts.map +1 -1
  48. package/dist/src/extractors/html-page.js +26 -2
  49. package/dist/src/extractors/prd-extractor.d.ts +70 -0
  50. package/dist/src/extractors/prd-extractor.d.ts.map +1 -0
  51. package/dist/src/extractors/prd-extractor.js +248 -0
  52. package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
  53. package/dist/src/generators/stage-skeleton-script.d.ts +87 -20
  54. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  55. package/dist/src/generators/stage-skeleton-script.js +647 -238
  56. package/dist/src/index.d.ts +2 -2
  57. package/dist/src/index.d.ts.map +1 -1
  58. package/dist/src/index.js +1 -1
  59. package/dist/src/normalizers/crud-contract.d.ts +3 -0
  60. package/dist/src/normalizers/crud-contract.d.ts.map +1 -1
  61. package/dist/src/normalizers/crud-contract.js +3 -0
  62. package/dist/src/runtime/ai-runtime-trace.d.ts +88 -0
  63. package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -0
  64. package/dist/src/runtime/ai-runtime-trace.js +119 -0
  65. package/dist/src/testcase/button-interaction-inference.d.ts +37 -0
  66. package/dist/src/testcase/button-interaction-inference.d.ts.map +1 -0
  67. package/dist/src/testcase/button-interaction-inference.js +385 -0
  68. package/dist/src/testcase/contract-testcase-assembler.d.ts +44 -0
  69. package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -0
  70. package/dist/src/testcase/contract-testcase-assembler.js +806 -0
  71. package/dist/src/testcase/prd-button-mapper.d.ts +72 -0
  72. package/dist/src/testcase/prd-button-mapper.d.ts.map +1 -0
  73. package/dist/src/testcase/prd-button-mapper.js +148 -0
  74. package/dist/src/testcase/runtime-context.d.ts +9 -0
  75. package/dist/src/testcase/runtime-context.d.ts.map +1 -0
  76. package/dist/src/testcase/runtime-context.js +1 -0
  77. package/dist/src/testcase/test-access.d.ts +38 -0
  78. package/dist/src/testcase/test-access.d.ts.map +1 -0
  79. package/dist/src/testcase/test-access.js +106 -0
  80. package/dist/src/testcase/testcase-execution-fields.d.ts +10 -0
  81. package/dist/src/testcase/testcase-execution-fields.d.ts.map +1 -0
  82. package/dist/src/testcase/testcase-execution-fields.js +1 -0
  83. package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
  84. package/dist/src/testcase/testcase-generator.js +6 -0
  85. package/dist/src/testcase/testcase-markdown.d.ts +11 -0
  86. package/dist/src/testcase/testcase-markdown.d.ts.map +1 -0
  87. package/dist/src/testcase/testcase-markdown.js +181 -0
  88. package/dist/src/testcase/testcase-skeleton.d.ts +4 -0
  89. package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -1
  90. package/dist/src/testcase/testcase-spec-assembly.d.ts +4 -0
  91. package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -1
  92. package/dist/src/testcase/testcase-v1-draft.d.ts +58 -0
  93. package/dist/src/testcase/testcase-v1-draft.d.ts.map +1 -0
  94. package/dist/src/testcase/testcase-v1-draft.js +13 -0
  95. package/dist/src/validation/contract.d.ts +14 -0
  96. package/dist/src/validation/contract.d.ts.map +1 -0
  97. package/dist/src/validation/contract.js +132 -0
  98. package/dist/src/validation/credentials.d.ts +7 -2
  99. package/dist/src/validation/credentials.d.ts.map +1 -1
  100. package/dist/src/validation/credentials.js +15 -4
  101. package/dist/src/validation/index.d.ts +1 -0
  102. package/dist/src/validation/index.d.ts.map +1 -1
  103. package/dist/src/validation/index.js +1 -0
  104. package/dist/src/validation/testcase.d.ts +9 -7
  105. package/dist/src/validation/testcase.d.ts.map +1 -1
  106. package/dist/src/validation/testcase.js +181 -34
  107. package/docs/README.md +124 -134
  108. package/docs/mvp-usage-guide.md +298 -298
  109. package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
  110. package/examples/schemeresource.module-hints.json +38 -38
  111. package/examples/schemeresource.observable.code_list.md +37 -37
  112. package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
  113. package/examples/zwplace-placecategory-validation.json +29 -29
  114. package/examples/zwplace.module-hints.json +69 -69
  115. package/examples/zwplace.observable.code_list.md +37 -37
  116. package/package.json +2 -2
  117. package/codex-skill/ep-stage/.omc/state/sessions/d636ad35-5fc9-4d5e-85b0-2bcad7d86ee5/pre-tool-advisory-throttle.json +0 -10
  118. package/dist/test/cli/extract-contract.test.d.ts +0 -2
  119. package/dist/test/cli/extract-contract.test.d.ts.map +0 -1
  120. package/dist/test/cli/extract-contract.test.js +0 -32
  121. package/dist/test/cli/generate-testcase-inference.test.d.ts +0 -2
  122. package/dist/test/cli/generate-testcase-inference.test.d.ts.map +0 -1
  123. package/dist/test/cli/generate-testcase-inference.test.js +0 -63
  124. package/dist/test/cli/generate-testcase-paths.test.d.ts +0 -2
  125. package/dist/test/cli/generate-testcase-paths.test.d.ts.map +0 -1
  126. package/dist/test/cli/generate-testcase-paths.test.js +0 -165
  127. package/dist/test/cli/generate-testcase-requirements.test.d.ts +0 -2
  128. package/dist/test/cli/generate-testcase-requirements.test.d.ts.map +0 -1
  129. package/dist/test/cli/generate-testcase-requirements.test.js +0 -229
  130. package/dist/test/cli/generate-testcase.test.d.ts +0 -2
  131. package/dist/test/cli/generate-testcase.test.d.ts.map +0 -1
  132. package/dist/test/cli/generate-testcase.test.js +0 -130
  133. package/dist/test/cli/index.test.d.ts +0 -2
  134. package/dist/test/cli/index.test.d.ts.map +0 -1
  135. package/dist/test/cli/index.test.js +0 -113
  136. package/dist/test/cli/probe.test.d.ts +0 -2
  137. package/dist/test/cli/probe.test.d.ts.map +0 -1
  138. package/dist/test/cli/probe.test.js +0 -31
  139. package/dist/test/cli/run.test.d.ts +0 -2
  140. package/dist/test/cli/run.test.d.ts.map +0 -1
  141. package/dist/test/cli/run.test.js +0 -149
  142. package/dist/test/cli/spec-boundary.test.d.ts +0 -2
  143. package/dist/test/cli/spec-boundary.test.d.ts.map +0 -1
  144. package/dist/test/cli/spec-boundary.test.js +0 -198
  145. package/dist/test/cli/spec.test.d.ts +0 -2
  146. package/dist/test/cli/spec.test.d.ts.map +0 -1
  147. package/dist/test/cli/spec.test.js +0 -196
  148. package/dist/test/code-list.test.d.ts +0 -2
  149. package/dist/test/code-list.test.d.ts.map +0 -1
  150. package/dist/test/code-list.test.js +0 -39
  151. package/dist/test/crud-contract.test.d.ts +0 -2
  152. package/dist/test/crud-contract.test.d.ts.map +0 -1
  153. package/dist/test/crud-contract.test.js +0 -799
  154. package/dist/test/gap-inference.test.d.ts +0 -2
  155. package/dist/test/gap-inference.test.d.ts.map +0 -1
  156. package/dist/test/gap-inference.test.js +0 -597
  157. package/dist/test/generalization.test.d.ts +0 -2
  158. package/dist/test/generalization.test.d.ts.map +0 -1
  159. package/dist/test/generalization.test.js +0 -73
  160. package/dist/test/inference/module-hints-inference.test.d.ts +0 -2
  161. package/dist/test/inference/module-hints-inference.test.d.ts.map +0 -1
  162. package/dist/test/inference/module-hints-inference.test.js +0 -25
  163. package/dist/test/inference/source-analysis.test.d.ts +0 -2
  164. package/dist/test/inference/source-analysis.test.d.ts.map +0 -1
  165. package/dist/test/inference/source-analysis.test.js +0 -23
  166. package/dist/test/material-inventory.test.d.ts +0 -2
  167. package/dist/test/material-inventory.test.d.ts.map +0 -1
  168. package/dist/test/material-inventory.test.js +0 -154
  169. package/dist/test/observable-chain.test.d.ts +0 -2
  170. package/dist/test/observable-chain.test.d.ts.map +0 -1
  171. package/dist/test/observable-chain.test.js +0 -123
  172. package/dist/test/observable-pipeline.test.d.ts +0 -2
  173. package/dist/test/observable-pipeline.test.d.ts.map +0 -1
  174. package/dist/test/observable-pipeline.test.js +0 -470
  175. package/dist/test/page-structure.test.d.ts +0 -2
  176. package/dist/test/page-structure.test.d.ts.map +0 -1
  177. package/dist/test/page-structure.test.js +0 -45
  178. package/dist/test/scenario-inference.test.d.ts +0 -2
  179. package/dist/test/scenario-inference.test.d.ts.map +0 -1
  180. package/dist/test/scenario-inference.test.js +0 -73
  181. package/dist/test/stage-context.test.d.ts +0 -2
  182. package/dist/test/stage-context.test.d.ts.map +0 -1
  183. package/dist/test/stage-context.test.js +0 -395
  184. package/dist/test/testcase-generator.test.d.ts +0 -2
  185. package/dist/test/testcase-generator.test.d.ts.map +0 -1
  186. package/dist/test/testcase-generator.test.js +0 -276
  187. package/dist/test/testcase-skeleton.test.d.ts +0 -2
  188. package/dist/test/testcase-skeleton.test.d.ts.map +0 -1
  189. package/dist/test/testcase-skeleton.test.js +0 -185
  190. package/dist/test/testcase-spec-assembly.test.d.ts +0 -2
  191. package/dist/test/testcase-spec-assembly.test.d.ts.map +0 -1
  192. package/dist/test/testcase-spec-assembly.test.js +0 -105
  193. package/dist/test/util/credentials.test.d.ts +0 -2
  194. package/dist/test/util/credentials.test.d.ts.map +0 -1
  195. package/dist/test/util/credentials.test.js +0 -64
  196. package/dist/test/util/i18n-testcase.test.d.ts +0 -2
  197. package/dist/test/util/i18n-testcase.test.d.ts.map +0 -1
  198. package/dist/test/util/i18n-testcase.test.js +0 -119
  199. package/dist/test/util/softlink.test.d.ts +0 -2
  200. package/dist/test/util/softlink.test.d.ts.map +0 -1
  201. package/dist/test/util/softlink.test.js +0 -82
  202. package/dist/test/validation/credentials.test.d.ts +0 -2
  203. package/dist/test/validation/credentials.test.d.ts.map +0 -1
  204. package/dist/test/validation/credentials.test.js +0 -72
  205. package/dist/test/validation/projects-index.test.d.ts +0 -2
  206. package/dist/test/validation/projects-index.test.d.ts.map +0 -1
  207. package/dist/test/validation/projects-index.test.js +0 -48
  208. package/dist/test/validation/testcase.test.d.ts +0 -2
  209. package/dist/test/validation/testcase.test.d.ts.map +0 -1
  210. package/dist/test/validation/testcase.test.js +0 -129
  211. package/dist/vitest.config.d.ts +0 -3
  212. package/dist/vitest.config.d.ts.map +0 -1
  213. 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
+ }