@epoint-testtech/ep-stage-skill 0.0.4-alpha.1 → 0.0.5

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 +292 -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 +28 -15
  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 +494 -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
@@ -6,6 +6,15 @@
6
6
  * - 生成调用 LoginPage、MenuPage、CrudPage 的 Playwright 测试文件
7
7
  * - 避免直接拼接 page.fill/page.click,保证测试流程复用骨架层
8
8
  */
9
+ /**
10
+ * 生成 stage-skeleton 测试脚本的输入参数。
11
+ *
12
+ * @property contract CRUD 业务模块契约
13
+ * @property directUrl 待测列表页直达链接,登录后由 LoginPage.gotoDirectUrl 打开
14
+ * @property primaryRole credentials.json5 中的主执行角色
15
+ * @property requirementName 需求目录名,用于拼接 credentialsPath
16
+ * @property testName 生成的测试用例名称
17
+ */
9
18
  /**
10
19
  * 将字符串渲染为 TypeScript 单引号字面量。
11
20
  *
@@ -24,7 +33,11 @@ function singleQuoted(value) {
24
33
  function locatorLiteral(value) {
25
34
  return JSON.stringify(value);
26
35
  }
36
+ // --- 以下为旧 v2 协议(CrudBusinessModuleContract → slots 映射)专用辅助函数 ---
37
+ // 新链路 generateStageSkeletonCrudSpecFromSlots 不再使用这些函数。
38
+ // @deprecated 旧 v2 协议残留,新链路不引用,保留作历史参考,待后续整体清理。
27
39
  /**
40
+ * @deprecated 旧 v2 协议残留,新链路不引用。
28
41
  * 为 XPath locator 补齐 Playwright locator 引擎前缀。
29
42
  *
30
43
  * @param locator 契约中的 locator
@@ -322,13 +335,32 @@ function renderOptionalStringProperty(key, value, level) {
322
335
  return `${' '.repeat(level)}${key}: ${singleQuoted(value)},\n`;
323
336
  }
324
337
  function renderListboxSelections(selections, level) {
325
- if (!selections || selections.length === 0) {
338
+ if (!selections) {
326
339
  return '';
327
340
  }
328
341
  const indent = ' '.repeat(level);
342
+ // 空数组必须显式渲染 `createSelections: [],`,否则 skeleton 走 `condition.createSelections ?? [默认]`
343
+ // fallback 生成默认 selection,反把不在 add 表单里的字段(如列表页搜索条件"状态")当作要在
344
+ // 新增弹框里补选,运行时找不到 trigger 30s timeout。
345
+ if (selections.length === 0) {
346
+ return `${indent}createSelections: [],\n`;
347
+ }
329
348
  const itemIndent = ' '.repeat(level + 4);
330
349
  const items = selections
331
- .map((selection) => `${itemIndent}{ field: ${singleQuoted(selection.field)}, label: ${singleQuoted(selection.label)}, value: ${singleQuoted(selection.value)} }`)
350
+ .map((selection) => {
351
+ const parts = [
352
+ `field: ${singleQuoted(selection.field)}`,
353
+ `label: ${singleQuoted(selection.label)}`,
354
+ ];
355
+ if (selection.value !== undefined)
356
+ parts.push(`value: ${singleQuoted(selection.value)}`);
357
+ if (selection.valueStrategy !== undefined)
358
+ parts.push(`valueStrategy: ${singleQuoted(selection.valueStrategy)}`);
359
+ if (selection.skipValues && selection.skipValues.length > 0) {
360
+ parts.push(`skipValues: [${selection.skipValues.map((v) => singleQuoted(v)).join(', ')}]`);
361
+ }
362
+ return `${itemIndent}{ ${parts.join(', ')} }`;
363
+ })
332
364
  .join(',\n');
333
365
  return `${indent}createSelections: [\n${items},\n${indent}],\n`;
334
366
  }
@@ -336,21 +368,35 @@ function renderSearchCondition(condition, level) {
336
368
  const indent = ' '.repeat(level);
337
369
  const innerIndent = ' '.repeat(level + 4);
338
370
  if (condition.component === 'listbox') {
339
- return `${indent}{
340
- ${innerIndent}component: 'listbox',
341
- ${innerIndent}field: ${singleQuoted(condition.field)},
342
- ${innerIndent}label: ${singleQuoted(condition.label)},
343
- ${innerIndent}value: ${singleQuoted(condition.value)},
371
+ const lines = [
372
+ `${innerIndent}component: 'listbox',`,
373
+ `${innerIndent}field: ${singleQuoted(condition.field)},`,
374
+ `${innerIndent}label: ${singleQuoted(condition.label)},`,
375
+ ];
376
+ if (condition.value !== undefined) {
377
+ lines.push(`${innerIndent}value: ${singleQuoted(condition.value)},`);
378
+ }
379
+ if (condition.valueStrategy !== undefined) {
380
+ lines.push(`${innerIndent}valueStrategy: ${singleQuoted(condition.valueStrategy)},`);
381
+ }
382
+ if (condition.skipValues && condition.skipValues.length > 0) {
383
+ lines.push(`${innerIndent}skipValues: [${condition.skipValues.map((v) => singleQuoted(v)).join(', ')}],`);
384
+ }
385
+ if (condition.searchAssertion !== undefined) {
386
+ lines.push(`${innerIndent}searchAssertion: ${singleQuoted(condition.searchAssertion)},`);
387
+ }
388
+ return `${indent}{
389
+ ${lines.join('\n')}
344
390
  ${renderListboxSelections(condition.createSelections, level + 4)}${indent}}`;
345
391
  }
346
- return `${indent}{
347
- ${innerIndent}component: 'input',
348
- ${innerIndent}field: ${singleQuoted(condition.field)},
349
- ${innerIndent}label: ${singleQuoted(condition.label)},
350
- ${innerIndent}seedValue: ${singleQuoted(condition.seedValue)},
351
- ${renderOptionalStringProperty('strategy', condition.strategy, level + 4)}${renderOptionalStringProperty('fixedValue', condition.fixedValue, level + 4)}${renderOptionalStringProperty('updatePrefix', condition.updatePrefix, level + 4)}${renderOptionalStringProperty('searchSeedValue', condition.searchSeedValue, level + 4)}${innerIndent}addLocator: ${locatorLiteral(condition.addLocator)},
352
- ${innerIndent}editLocator: ${locatorLiteral(condition.editLocator)},
353
- ${innerIndent}listLocator: ${locatorLiteral(condition.listLocator)},
392
+ return `${indent}{
393
+ ${innerIndent}component: 'input',
394
+ ${innerIndent}field: ${singleQuoted(condition.field)},
395
+ ${innerIndent}label: ${singleQuoted(condition.label)},
396
+ ${innerIndent}seedValue: ${singleQuoted(condition.seedValue)},
397
+ ${renderOptionalStringProperty('strategy', condition.strategy, level + 4)}${renderOptionalStringProperty('fixedValue', condition.fixedValue, level + 4)}${renderOptionalStringProperty('updatePrefix', condition.updatePrefix, level + 4)}${renderOptionalStringProperty('searchSeedValue', condition.searchSeedValue, level + 4)}${innerIndent}addLocator: ${locatorLiteral(condition.addLocator)},
398
+ ${innerIndent}editLocator: ${locatorLiteral(condition.editLocator)},
399
+ ${innerIndent}listLocator: ${locatorLiteral(condition.listLocator)},
354
400
  ${indent}}`;
355
401
  }
356
402
  function renderSearchConditions(conditions, level) {
@@ -365,30 +411,333 @@ function renderSearchConditions(conditions, level) {
365
411
  * @returns TypeScript 对象源码
366
412
  */
367
413
  export function renderStageSkeletonCrudSlots(slots) {
368
- return `{
369
- moduleId: ${singleQuoted(slots.moduleId)},
370
- ${renderSearchConditions(slots.searchConditions, 8)}
371
- frames: {
372
- listIframeSrcKeyword: ${singleQuoted(slots.frames.listIframeSrcKeyword)},
373
- addIframeSrcKeyword: ${singleQuoted(slots.frames.addIframeSrcKeyword)},
374
- editIframeSrcKeyword: ${singleQuoted(slots.frames.editIframeSrcKeyword)},
375
- },
376
- locators: {
377
- createButton: ${locatorLiteral(slots.locators.createButton)},
378
- ${renderOptionalStringProperty('addAutofillTrigger', slots.locators.addAutofillTrigger, 12)}
379
- addSaveButton: ${locatorLiteral(slots.locators.addSaveButton)},
380
- listSearchSubmit: ${locatorLiteral(slots.locators.listSearchSubmit)},
381
- listResultValueLocatorTemplate: ${locatorLiteral(slots.locators.listResultValueLocatorTemplate)},
382
- listEditActionLocatorTemplate: ${locatorLiteral(slots.locators.listEditActionLocatorTemplate)},
383
- editSaveButton: ${locatorLiteral(slots.locators.editSaveButton)},
384
- listRowSelectLocatorTemplate: ${locatorLiteral(slots.locators.listRowSelectLocatorTemplate)},
385
- listDeleteButton: ${locatorLiteral(slots.locators.listDeleteButton)},
386
- ${renderOptionalStringProperty('deleteConfirmButton', slots.locators.deleteConfirmButton, 12)} },
387
- assertions: {
388
- deletePolicy: ${singleQuoted(slots.assertions.deletePolicy)},
389
- ${renderOptionalStringProperty('blockedDeleteMessage', slots.assertions.blockedDeleteMessage, 12)} },
414
+ return `{
415
+ moduleId: ${singleQuoted(slots.moduleId)},
416
+ ${renderSearchConditions(slots.searchConditions, 8)}
417
+ frames: {
418
+ listIframeSrcKeyword: ${singleQuoted(slots.frames.listIframeSrcKeyword)},
419
+ addIframeSrcKeyword: ${singleQuoted(slots.frames.addIframeSrcKeyword)},
420
+ editIframeSrcKeyword: ${singleQuoted(slots.frames.editIframeSrcKeyword)},
421
+ },
422
+ locators: {
423
+ createButton: ${locatorLiteral(slots.locators.createButton)},
424
+ ${renderOptionalStringProperty('addAutofillTrigger', slots.locators.addAutofillTrigger, 12)}
425
+ addSaveButton: ${locatorLiteral(slots.locators.addSaveButton)},
426
+ listSearchSubmit: ${locatorLiteral(slots.locators.listSearchSubmit)},
427
+ listResultValueLocatorTemplate: ${locatorLiteral(slots.locators.listResultValueLocatorTemplate)},
428
+ listEditActionLocatorTemplate: ${locatorLiteral(slots.locators.listEditActionLocatorTemplate)},
429
+ editSaveButton: ${locatorLiteral(slots.locators.editSaveButton)},
430
+ listRowSelectLocatorTemplate: ${locatorLiteral(slots.locators.listRowSelectLocatorTemplate)},
431
+ listDeleteButton: ${locatorLiteral(slots.locators.listDeleteButton)},
432
+ ${renderOptionalStringProperty('deleteConfirmButton', slots.locators.deleteConfirmButton, 12)} },
433
+ assertions: {
434
+ deletePolicy: ${singleQuoted(slots.assertions.deletePolicy)},
435
+ ${renderOptionalStringProperty('blockedDeleteMessage', slots.assertions.blockedDeleteMessage, 12)} },
390
436
  }`;
391
437
  }
438
+ function renderChildGridColumns(columns, level) {
439
+ const indent = ' '.repeat(level);
440
+ const innerIndent = ' '.repeat(level + 4);
441
+ const items = columns.map((col) => {
442
+ const parts = [
443
+ `field: ${singleQuoted(col.field)}`,
444
+ `label: ${singleQuoted(col.label)}`,
445
+ `component: ${singleQuoted(col.component)}`,
446
+ `required: ${String(col.required)}`,
447
+ ];
448
+ if (col.modelRef !== undefined) {
449
+ parts.push(`modelRef: ${singleQuoted(col.modelRef)}`);
450
+ }
451
+ return `${innerIndent}{ ${parts.join(', ')} }`;
452
+ }).join(',\n');
453
+ return `${indent}[\n${items},\n${indent}]`;
454
+ }
455
+ function renderChildGridLocators(locators, level) {
456
+ const indent = ' '.repeat(level);
457
+ const innerIndent = ' '.repeat(level + 4);
458
+ const fields = [
459
+ ['container', locators.container],
460
+ ['addRowButton', locators.addRowButton],
461
+ ['deleteSelectedButton', locators.deleteSelectedButton],
462
+ ['grid', locators.grid],
463
+ ['editWrapInput', locators.editWrapInput],
464
+ ['editWrapListboxTrigger', locators.editWrapListboxTrigger],
465
+ ['editWrapListboxInput', locators.editWrapListboxInput],
466
+ ['rowCheckboxByIndex', locators.rowCheckboxByIndex],
467
+ ['cellByIndexAndField', locators.cellByIndexAndField],
468
+ ['cellValueByIndexAndField', locators.cellValueByIndexAndField],
469
+ ];
470
+ const lines = fields.map(([key, value]) => `${innerIndent}${key}: ${locatorLiteral(value)},`).join('\n');
471
+ return `${indent}{\n${lines}\n${indent}}`;
472
+ }
473
+ /**
474
+ * 将子表槽位数组渲染为 TypeScript 对象字面量。
475
+ *
476
+ * @param slots - 子表槽位数组
477
+ * @returns TypeScript 源码
478
+ */
479
+ export function renderChildGridSlots(slots) {
480
+ if (slots.length === 0)
481
+ return '[]';
482
+ const items = slots.map((slot) => `{
483
+ gridId: ${singleQuoted(slot.gridId)},
484
+ label: ${singleQuoted(slot.label)},
485
+ pageRole: ${singleQuoted(slot.pageRole)},
486
+ editMode: ${singleQuoted(slot.editMode)},
487
+ columns: ${renderChildGridColumns(slot.columns, 8).trimStart()},
488
+ locators: ${renderChildGridLocators(slot.locators, 8).trimStart()},
489
+ }`).join(',\n');
490
+ return `[\n${items}\n]`;
491
+ }
492
+ /**
493
+ * 渲染子表行内编辑的填写步骤。
494
+ *
495
+ * @param slot 子表槽位
496
+ * @param valuePrefix 填充值前缀,例如 "测试" / "修改后"
497
+ * @param indent 语句块缩进空格数,默认 10
498
+ * @returns 可直接嵌入生成测试源码的语句块
499
+ */
500
+ function renderChildGridFillSteps(slot, valuePrefix, indent = 10) {
501
+ const spaces = ' '.repeat(indent);
502
+ return slot.columns.map((col) => {
503
+ if (col.component === 'input') {
504
+ return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
505
+ ${spaces}await childGridPage.fillInputCell(${singleQuoted(`${valuePrefix}${col.label}`)});`;
506
+ }
507
+ return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
508
+ ${spaces}await childGridPage.selectListboxCell(childGridSlots[0].columns.find((c) => c.field === ${singleQuoted(col.field)})!);`;
509
+ }).join('\n');
510
+ }
511
+ /**
512
+ * 渲染子表持久化后的单元格断言步骤。
513
+ *
514
+ * @param slot 子表槽位
515
+ * @param valuePrefix 填充值前缀,用于 input 类型精确匹配
516
+ * @returns 可直接嵌入生成测试源码的语句块(已含 8 空格缩进)
517
+ */
518
+ function renderChildGridVerifyAssertions(slot, valuePrefix) {
519
+ return slot.columns
520
+ .map((col) => {
521
+ if (col.component === 'input') {
522
+ return ` await verifyChildGridPage.assertCellValue(1, ${singleQuoted(col.field)}, ${singleQuoted(`${valuePrefix}${col.label}`)});`;
523
+ }
524
+ return ` await verifyChildGridPage.assertCellNotEmpty(1, ${singleQuoted(col.field)});`;
525
+ })
526
+ .join('\n');
527
+ }
528
+ /**
529
+ * 渲染单个子表的新增行测试。
530
+ */
531
+ function renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId) {
532
+ const label = slot.label;
533
+ const fillSteps = renderChildGridFillSteps(slot, '测试');
534
+ const verifyAssertions = renderChildGridVerifyAssertions(slot, '测试');
535
+ return ` test(${singleQuoted(`${label}-新增行并随主表保存`)}, async ({ page }) => {
536
+ test.slow();
537
+
538
+ await runRecordedWorkflow(
539
+ recorder,
540
+ workflowMeta({
541
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.create`)},
542
+ workflowKind: 'create',
543
+ workflowName: ${singleQuoted(`${label}-新增行`)},
544
+ businessIntent: ${singleQuoted(`在新增${moduleLabel}时维护${label}子表,验证子表数据随主表保存并持久化。`)},
545
+ elementActions: ['进入新增页', '点击子表新增行', '填写子表单元格', '保存主表', '返回列表搜索并校验子表数据'],
546
+ assertionExpectation: ${singleQuoted(`保存后重新打开编辑页,${label}子表应显示新增的行数据。`)},
547
+ playwrightTestTitle: ${singleQuoted(`${label}-新增行并随主表保存`)},
548
+ }),
549
+ async () => {
550
+ await openModule(page);
551
+ const crudPage = new CrudPage(page, crudSlots);
552
+ const childWorkflowData = crudPage.prepareWorkflowData();
553
+ await crudPage.runCreateWorkflowWithChildGrid(childWorkflowData, async (addContext) => {
554
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
555
+ await childGridPage.addRow();
556
+ ${fillSteps}
557
+ });
558
+
559
+ // 验证子表数据持久化:搜索主记录并重新打开编辑页
560
+ const editActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
561
+ await page.locator(editActionLocator).click();
562
+ const editFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
563
+ const verifyChildGridPage = new ChildGridPage(page, editFrame, childGridSlots[0]);
564
+ ${verifyAssertions}
565
+ },
566
+ );
567
+ });`;
568
+ }
569
+ /**
570
+ * 渲染单个子表的编辑行测试。
571
+ */
572
+ function renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId) {
573
+ const label = slot.label;
574
+ const createFillSteps = renderChildGridFillSteps(slot, '测试');
575
+ const verifyAssertions = renderChildGridVerifyAssertions(slot, '修改后');
576
+ return ` test(${singleQuoted(`${label}-编辑行并随主表保存`)}, async ({ page }) => {
577
+ test.slow();
578
+
579
+ await runRecordedWorkflow(
580
+ recorder,
581
+ workflowMeta({
582
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.update`)},
583
+ workflowKind: 'update',
584
+ workflowName: ${singleQuoted(`${label}-编辑行`)},
585
+ businessIntent: ${singleQuoted(`在修改${moduleLabel}时维护${label}子表,验证子表数据随主表保存并持久化。`)},
586
+ elementActions: ['进入列表页', '创建带窗口信息子表的主记录', '打开编辑页', '修改子表单元格', '保存主表', '重新搜索并校验子表数据'],
587
+ assertionExpectation: ${singleQuoted(`保存后重新打开编辑页,${label}子表应显示更新后的行数据。`)},
588
+ playwrightTestTitle: ${singleQuoted(`${label}-编辑行并随主表保存`)},
589
+ }),
590
+ async () => {
591
+ await openModule(page);
592
+ const crudPage = new CrudPage(page, crudSlots);
593
+ const childWorkflowData = crudPage.prepareWorkflowData();
594
+
595
+ // 前置:创建带${label}子表的主记录
596
+ await crudPage.runCreateWorkflowWithChildGrid(childWorkflowData, async (addContext) => {
597
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
598
+ await childGridPage.addRow();
599
+ ${createFillSteps}
600
+ });
601
+
602
+ // 打开编辑页并修改子表行
603
+ const editActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
604
+ await page.locator(editActionLocator).click();
605
+ const editFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
606
+ const childGridPage = new ChildGridPage(page, editFrame, childGridSlots[0]);
607
+ ${renderChildGridFillSteps(slot, '修改后', 8)}
608
+
609
+ // 保存主表
610
+ await editFrame.locator(crudSlots.locators.editSaveButton).click();
611
+
612
+ // 验证子表数据持久化:重新搜索主记录并打开编辑页
613
+ const verifyEditActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
614
+ await page.locator(verifyEditActionLocator).click();
615
+ const verifyEditFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
616
+ const verifyChildGridPage = new ChildGridPage(page, verifyEditFrame, childGridSlots[0]);
617
+ ${verifyAssertions}
618
+ },
619
+ );
620
+ });`;
621
+ }
622
+ /**
623
+ * 渲染单个子表的删除行测试。
624
+ *
625
+ * 注意:本测试会断言删除成功后子表行被真正移除。若系统存在 confirm 确定后仍删除失败的 bug,
626
+ * 该断言将失败,从而暴露问题,而不是为了绕过 bug 而降低断言标准。
627
+ */
628
+ function renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId) {
629
+ const label = slot.label;
630
+ const createFillSteps = renderChildGridFillSteps(slot, '测试');
631
+ return ` test(${singleQuoted(`${label}-删除行`)}, async ({ page }) => {
632
+ test.slow();
633
+
634
+ await runRecordedWorkflow(
635
+ recorder,
636
+ workflowMeta({
637
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.delete`)},
638
+ workflowKind: 'delete',
639
+ workflowName: ${singleQuoted(`${label}-删除行`)},
640
+ businessIntent: ${singleQuoted(`验证${moduleLabel}的${label}子表删除选中行后,数据应被真正移除。`)},
641
+ elementActions: ['进入列表页', '创建带窗口信息子表的主记录', '打开编辑页', '勾选子表行', '点击删除选中', '确认删除', '校验子表行已移除'],
642
+ assertionExpectation: ${singleQuoted(`确认删除后,${label}子表的数据行数应为 0,被删除的数据不再存在。`)},
643
+ playwrightTestTitle: ${singleQuoted(`${label}-删除行`)},
644
+ }),
645
+ async () => {
646
+ await openModule(page);
647
+ const crudPage = new CrudPage(page, crudSlots);
648
+ const childWorkflowData = crudPage.prepareWorkflowData();
649
+
650
+ // 前置:创建带${label}子表的主记录
651
+ await crudPage.runCreateWorkflowWithChildGrid(childWorkflowData, async (addContext) => {
652
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
653
+ await childGridPage.addRow();
654
+ ${createFillSteps}
655
+ });
656
+
657
+ // 打开编辑页
658
+ const editActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
659
+ await page.locator(editActionLocator).click();
660
+ const editFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
661
+
662
+ const childGridPage = new ChildGridPage(page, editFrame, childGridSlots[0]);
663
+ const confirmPromise = childGridPage.acceptCustomConfirm('确定要删除');
664
+ await childGridPage.selectRow(1);
665
+ await childGridPage.deleteSelected();
666
+
667
+ await confirmPromise;
668
+
669
+ // 断言删除成功:子表数据行数应为 0
670
+ await page.waitForTimeout(500);
671
+ await childGridPage.assertRowCount(0);
672
+ },
673
+ );
674
+ });`;
675
+ }
676
+ /**
677
+ * 渲染单个子表的必填校验测试。
678
+ */
679
+ function renderChildGridRequiredTest(slot, moduleLabel, moduleId) {
680
+ const label = slot.label;
681
+ const requiredFields = slot.columns.filter((c) => c.required);
682
+ const requiredLabels = requiredFields.map((c) => c.label).join('、');
683
+ return ` test(${singleQuoted(`${label}-必填校验`)}, async ({ page }) => {
684
+ test.slow();
685
+
686
+ await runRecordedWorkflow(
687
+ recorder,
688
+ workflowMeta({
689
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.required`)},
690
+ workflowKind: 'create',
691
+ workflowName: ${singleQuoted(`${label}-必填校验`)},
692
+ businessIntent: ${singleQuoted(`验证${label}子表必填字段未填写时,主表保存应被阻断并给出校验提示。`)},
693
+ elementActions: ['进入新增页', '点击子表新增行', '不填必填字段', '保存主表', '观察校验提示'],
694
+ assertionExpectation: ${singleQuoted(`系统应阻断保存并提示「${requiredLabels}不能为空」。`)},
695
+ playwrightTestTitle: ${singleQuoted(`${label}-必填校验`)},
696
+ }),
697
+ async () => {
698
+ await openModule(page);
699
+ const crudPage = new CrudPage(page, crudSlots);
700
+ const childWorkflowData = crudPage.prepareWorkflowData();
701
+
702
+ let alertPromise: Promise<void> | undefined;
703
+
704
+ await crudPage.runCreateWorkflowWithChildGridWithoutSearch(childWorkflowData, async (addContext) => {
705
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
706
+ alertPromise = childGridPage.assertAlert('不能为空');
707
+ await childGridPage.addRow();
708
+ });
709
+
710
+ if (!alertPromise) {
711
+ throw new Error('alertPromise 未初始化');
712
+ }
713
+ await alertPromise;
714
+ },
715
+ );
716
+ });`;
717
+ }
718
+ /**
719
+ * 渲染所有子表测试块(新增行、编辑行、删除行、必填校验)。
720
+ */
721
+ function renderChildGridTests(slots, crudSlots, moduleLabel, moduleId) {
722
+ if (slots.length === 0)
723
+ return '';
724
+ return slots
725
+ .flatMap((slot) => {
726
+ if (slot.editMode === 'none') {
727
+ return [];
728
+ }
729
+ if (slot.editMode === 'row_button_dialog') {
730
+ return [` // TODO: ${slot.label} 子表为行按钮弹窗编辑模式,测试待实现`];
731
+ }
732
+ return [
733
+ renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId),
734
+ renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId),
735
+ renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId),
736
+ renderChildGridRequiredTest(slot, moduleLabel, moduleId),
737
+ ];
738
+ })
739
+ .join('\n\n');
740
+ }
392
741
  /**
393
742
  * 渲染导出骨架槽位对象。
394
743
  *
@@ -396,38 +745,67 @@ ${renderOptionalStringProperty('blockedDeleteMessage', slots.assertions.blockedD
396
745
  * @returns TypeScript 对象字面量源码
397
746
  */
398
747
  function renderExportSlots(contract) {
399
- return `{
400
- moduleId: ${singleQuoted(contract.module.id)},
401
- frames: {
402
- listIframeSrcKeyword: ${singleQuoted(iframeKeyword(contract.pages.list.iframeSrcKeyword, true))},
403
- },
404
- locators: {
405
- exportButton: '.mini-button.mini-dataexport',
406
- },
407
- export: {
408
- label: '导出',
409
- mode: 'all' as const,
410
- },
748
+ return `{
749
+ moduleId: ${singleQuoted(contract.module.id)},
750
+ frames: {
751
+ listIframeSrcKeyword: ${singleQuoted(iframeKeyword(contract.pages.list.iframeSrcKeyword, true))},
752
+ },
753
+ locators: {
754
+ exportButton: '.mini-button.mini-dataexport',
755
+ },
756
+ export: {
757
+ label: '导出',
758
+ mode: 'all' as const,
759
+ },
411
760
  }`;
412
761
  }
413
762
  /**
414
- * 生成动作级 CRUD + Export Playwright 测试脚本。
763
+ * 基于 skeletonSlots.export 槽位渲染导出骨架对象(新协议 fromSlots 入口专用)。
764
+ *
765
+ * 与 {@link renderExportSlots} 输出等价的 TypeScript 对象字面量,但消费
766
+ * `{ moduleId, label, mode }` 三元槽位而非完整 contract。
767
+ * `frames.listIframeSrcKeyword` 与 crud 槽位共享同一列表页 iframe keyword
768
+ * (导出动作发生在列表页),由调用方从 crudSlots.frames.listIframeSrcKeyword 传入。
769
+ *
770
+ * @param exportSlot contract.scenario.skeletonSlots.export 槽位
771
+ * @param listIframeSrcKeyword 列表页 iframe keyword(来自 crudSlots.frames.listIframeSrcKeyword)
772
+ * @returns TypeScript 对象字面量源码
773
+ */
774
+ function renderExportSlotsFromSlot(exportSlot, listIframeSrcKeyword) {
775
+ return `{
776
+ moduleId: ${singleQuoted(exportSlot.moduleId)},
777
+ frames: {
778
+ listIframeSrcKeyword: ${singleQuoted(listIframeSrcKeyword)},
779
+ },
780
+ locators: {
781
+ exportButton: '.mini-button.mini-dataexport',
782
+ },
783
+ export: {
784
+ label: ${singleQuoted(exportSlot.label)},
785
+ mode: ${exportSlot.mode === 'all' ? "'all' as const" : singleQuoted(exportSlot.mode)},
786
+ },
787
+ }`;
788
+ }
789
+ /**
790
+ * 基于 skeletonSlots 槽位生成动作级 CRUD + Export Playwright 测试脚本(新协议 fromSlots 入口)。
791
+ *
792
+ * 与 {@link generateStageSkeletonCrudSpec} 输出同构的 spec 模板
793
+ * (test.describe / recorder / openModule / Create-Read-Update-Delete-Export),
794
+ * 但所有数据来自入参槽位,**不接收 CrudBusinessModuleContract、不 import 它、
795
+ * 不调 mapContractToStageSkeletonCrudSlots**。
415
796
  *
416
- * 每个标准工作流(Create / Read / Update / Delete / Export)独立输出为
417
- * 一个 test case,使用 GlueWorkflowRecorder 记录执行报告。
418
- * 末尾保留 `// @stage-gap-append` 标记供 gap Custom test 追加。
797
+ * - crudSlots 直接调 {@link renderStageSkeletonCrudSlots}(复用现有 render)。
798
+ * - exportSlot {@link renderExportSlotsFromSlot}(listIframeSrcKeyword 取自 crudSlots.frames)。
799
+ * - deletePolicy crudSlots.assertions.deletePolicy 取(不再从 contract.flows.delete 推)。
419
800
  *
420
- * @param options 生成参数
801
+ * @param options 生成参数(全部来自 contract.skeletonSlots + testcase 导航/模块信息)
421
802
  * @returns 完整的 `.spec.ts` 源码
422
803
  */
423
- export function generateStageSkeletonCrudSpec(options) {
424
- const { contract, menuNavigation } = options;
425
- const slots = mapContractToStageSkeletonCrudSlots(contract);
426
- const moduleId = contract.module.id;
427
- const moduleLabel = contract.module.label;
804
+ export function generateStageSkeletonCrudSpecFromSlots(options) {
805
+ const { moduleId, moduleLabel, directUrl, primaryRole, requirementName, crudSlots, exportSlot, childGridSlots } = options;
428
806
  const suiteTitle = `${moduleLabel} CRUD + Export`;
429
- const deletePolicy = mapDeletePolicy(contract.flows.delete?.expectedOutcome);
430
- const blockedDeleteMessage = deletePolicy === 'blocked_by_business_rule' ? findBlockedDeleteMessage(contract) : undefined;
807
+ const deletePolicy = crudSlots.assertions.deletePolicy;
808
+ const blockedDeleteMessage = crudSlots.assertions.blockedDeleteMessage;
431
809
  // 删除工作流的业务意图和断言取决于 deletePolicy
432
810
  const deleteIntent = deletePolicy === 'blocked_by_business_rule'
433
811
  ? `在列表页选择${moduleLabel}记录后点击删除,验证业务规则拦截删除操作${blockedDeleteMessage ? `(${blockedDeleteMessage})` : ''}。`
@@ -435,182 +813,213 @@ export function generateStageSkeletonCrudSpec(options) {
435
813
  const deleteAssertion = deletePolicy === 'blocked_by_business_rule'
436
814
  ? `删除操作应被业务规则拦截${blockedDeleteMessage ? `,提示「${blockedDeleteMessage}」` : ''},列表保留该记录。`
437
815
  : '删除成功后,列表中不再存在该记录,搜索结果为空。';
438
- return `import { test, expect, type Page } from '@playwright/test';
439
- import { StagePath } from '@epoint-testtech/stage-core';
440
- import { CrudPage, ExportPage, LoginPage, MenuPage, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../skeletons';
441
- import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../report/glue-report';
442
-
443
- test.describe(${singleQuoted(suiteTitle)}, () => {
444
- const menuRoute = ${singleQuoted(menuNavigation)};
445
- const crudSlots: CrudFlowSlots = ${renderStageSkeletonCrudSlots(slots)};
446
- const exportSlots = ${renderExportSlots(contract)};
447
-
448
- test.describe.configure({ mode: 'serial' });
449
- let workflowData: CrudWorkflowData | undefined;
450
-
451
- const recorder = new GlueWorkflowRecorder({
452
- outputRoot: StagePath.projectRootDirPath,
453
- moduleId: ${singleQuoted(moduleId)},
454
- moduleName: ${singleQuoted(moduleLabel)},
455
- suiteTitle: ${singleQuoted(suiteTitle)},
456
- generatedSpecFile: import.meta.url,
457
- });
458
-
459
- test.beforeAll(async ({ browser }) => {
460
- recorder.updateEnvironment({
461
- browserName: browser.browserType().name(),
462
- browserVersion: browser.version(),
463
- });
464
- });
465
-
466
- test.afterAll(() => {
467
- recorder.writeReport();
468
- });
469
-
470
- async function openModule(page: Page): Promise<void> {
471
- const loginPage = new LoginPage(page);
472
- const menuPage = new MenuPage(page);
473
- await loginPage.login();
474
- await menuPage.navigateToMenu(menuRoute);
475
- }
476
-
477
- function workflowMeta(
478
- input: Omit<
479
- GlueWorkflowRecordInput,
480
- 'moduleId' | 'executionSource' | 'evidenceSources' | 'evidenceRefs' | 'playwrightSuiteTitle'
481
- >,
482
- ): GlueWorkflowRecordInput {
483
- return {
484
- moduleId: ${singleQuoted(moduleId)},
485
- executionSource: 'glue_code' as const,
486
- evidenceSources: ['crud_flow_slots'],
487
- evidenceRefs: [],
488
- playwrightSuiteTitle: ${singleQuoted(suiteTitle)},
489
- ...input,
490
- };
491
- }
492
-
493
- test('Create:新增${moduleLabel}并校验列表存在对应记录', async ({ page }) => {
494
- test.slow();
495
-
496
- await runRecordedWorkflow(
497
- recorder,
498
- workflowMeta({
499
- workflowId: ${singleQuoted(`${moduleId}.create`)},
500
- workflowKind: 'create',
501
- workflowName: '新增${moduleLabel}',
502
- businessIntent: '在列表页点击新增按钮,填写表单并保存,验证列表中存在新增的记录。',
503
- elementActions: ['点击列表页新增按钮打开新增弹框', '在新增弹框中填写表单字段', '点击保存并关闭提交表单', '在列表页搜索并验证记录存在'],
504
- assertionExpectation: '新增成功后,列表页应能通过搜索找到刚新增的记录。',
505
- playwrightTestTitle: 'Create:新增${moduleLabel}并校验列表存在对应记录',
506
- }),
507
- async () => {
508
- await openModule(page);
509
- const crudPage = new CrudPage(page, crudSlots);
510
- workflowData = crudPage.prepareWorkflowData();
511
- await crudPage.runCreateWorkflow(workflowData);
512
- },
513
- );
514
- });
515
-
516
- test('Read:搜索${moduleLabel}并校验搜索结果', async ({ page }) => {
517
- test.slow();
518
-
519
- await runRecordedWorkflow(
520
- recorder,
521
- workflowMeta({
522
- workflowId: ${singleQuoted(`${moduleId}.read`)},
523
- workflowKind: 'read',
524
- workflowName: '搜索${moduleLabel}',
525
- businessIntent: '使用搜索条件查询记录,验证搜索结果与搜索条件匹配。',
526
- elementActions: ['在搜索区域填写搜索条件', '点击搜索按钮提交查询', '在结果列表中校验搜索结果'],
527
- assertionExpectation: '搜索结果列表中应包含符合搜索条件的记录。',
528
- playwrightTestTitle: 'Read:搜索${moduleLabel}并校验搜索结果',
529
- }),
530
- async () => {
531
- await openModule(page);
532
- if (!workflowData) {
533
- throw new Error('Read 工作流依赖 Create 工作流先行生成 workflowData');
534
- }
535
- const crudPage = new CrudPage(page, crudSlots);
536
- await crudPage.runReadWorkflow(workflowData);
537
- },
538
- );
539
- });
540
-
541
- test('Update:修改${moduleLabel}并校验修改结果', async ({ page }) => {
542
- test.slow();
543
-
544
- await runRecordedWorkflow(
545
- recorder,
546
- workflowMeta({
547
- workflowId: ${singleQuoted(`${moduleId}.update`)},
548
- workflowKind: 'update',
549
- workflowName: '修改${moduleLabel}',
550
- businessIntent: '定位目标记录行,点击修改按钮进入编辑弹框,修改字段后保存,验证修改生效。',
551
- elementActions: ['在列表中定位目标记录行', '点击行操作中的修改按钮打开编辑弹框', '修改表单字段', '点击保存并关闭提交修改'],
552
- assertionExpectation: '修改成功后,列表页应显示修改后的字段值。',
553
- playwrightTestTitle: 'Update:修改${moduleLabel}并校验修改结果',
554
- }),
555
- async () => {
556
- await openModule(page);
557
- if (!workflowData) {
558
- throw new Error('Update 工作流依赖 Create 工作流先行生成 workflowData');
559
- }
560
- const crudPage = new CrudPage(page, crudSlots);
561
- await crudPage.runUpdateWorkflow(workflowData);
562
- },
563
- );
564
- });
565
-
566
- test('Delete:删除${moduleLabel}并校验${deletePolicy === 'blocked_by_business_rule' ? '业务规则拦截' : '删除结果'}', async ({ page }) => {
567
- test.slow();
568
-
569
- await runRecordedWorkflow(
570
- recorder,
571
- workflowMeta({
572
- workflowId: ${singleQuoted(`${moduleId}.delete`)},
573
- workflowKind: 'delete',
574
- workflowName: '删除${moduleLabel}',
575
- businessIntent: ${singleQuoted(deleteIntent)},
576
- elementActions: ['在列表中选择目标记录', '点击删除按钮触发删除确认', ${deletePolicy === 'blocked_by_business_rule' ? "'确认删除操作并验证拦截提示'" : "'确认删除操作'"}],
577
- assertionExpectation: ${singleQuoted(deleteAssertion)},
578
- playwrightTestTitle: 'Delete:删除${moduleLabel}并校验${deletePolicy === 'blocked_by_business_rule' ? '业务规则拦截' : '删除结果'}',
579
- }),
580
- async () => {
581
- await openModule(page);
582
- if (!workflowData) {
583
- throw new Error('Delete 工作流依赖 Create 工作流先行生成 workflowData');
584
- }
585
- const crudPage = new CrudPage(page, crudSlots);
586
- await crudPage.runDeleteWorkflow(workflowData);
587
- },
588
- );
589
- });
590
-
591
- test('Export:导出${moduleLabel}列表并校验下载文件', async ({ page }, testInfo) => {
592
- test.slow();
593
-
594
- await runRecordedWorkflow(
595
- recorder,
596
- workflowMeta({
597
- workflowId: ${singleQuoted(`${moduleId}.export`)},
598
- workflowKind: 'export',
599
- workflowName: '导出${moduleLabel}列表',
600
- businessIntent: '在列表页触发全部导出,验证导出弹框可驱动并成功下载数据文件。',
601
- elementActions: ['点击列表页数据导出按钮打开导出弹框', '在导出弹框选择「全部」并确认导出', '等待 download 事件并断言下载文件存在且非空'],
602
- assertionExpectation: '导出操作应触发下载,落盘文件存在且大小大于 0。',
603
- playwrightTestTitle: 'Export:导出${moduleLabel}列表并校验下载文件',
604
- }),
605
- async () => {
606
- await openModule(page);
607
- const exportPage = new ExportPage(page, exportSlots);
608
- await exportPage.runExportWorkflow(testInfo);
609
- },
610
- );
611
- });
612
-
613
- // @stage-gap-append
614
- });
816
+ const childGridImport = childGridSlots?.length
817
+ ? ', ChildGridPage, type ChildGridFlowSlots'
818
+ : '';
819
+ return `import { test, expect, type Page } from '@playwright/test';
820
+ import { loadCredentials, StagePath, type Credential } from '@epoint-testtech/stage-core';
821
+ import { CrudPage, ExportPage, LoginPage${childGridImport}, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../../skeletons';
822
+ import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../../report/glue-report';
823
+
824
+ test.describe(${singleQuoted(suiteTitle)}, () => {
825
+ const directListUrl = ${singleQuoted(directUrl)};
826
+ const credentialsPath = \`\${StagePath.projectRootDirPath}/src/tests/${requirementName}/credentials.json5\`;
827
+ const primaryRole = ${singleQuoted(primaryRole)};
828
+ const crudSlots: CrudFlowSlots = ${renderStageSkeletonCrudSlots(crudSlots)};
829
+ const exportSlots = ${renderExportSlotsFromSlot(exportSlot, crudSlots.frames.listIframeSrcKeyword)};
830
+ ${childGridSlots?.length ? ` const childGridSlots: ChildGridFlowSlots[] = ${renderChildGridSlots(childGridSlots)};` : ''}
831
+
832
+ const recorder = new GlueWorkflowRecorder({
833
+ outputRoot: StagePath.projectRootDirPath,
834
+ moduleId: ${singleQuoted(moduleId)},
835
+ moduleName: ${singleQuoted(moduleLabel)},
836
+ suiteTitle: ${singleQuoted(suiteTitle)},
837
+ generatedSpecFile: import.meta.url,
838
+ });
839
+
840
+ test.beforeAll(async ({ browser }) => {
841
+ recorder.updateEnvironment({
842
+ browserName: browser.browserType().name(),
843
+ browserVersion: browser.version(),
844
+ });
845
+ });
846
+
847
+ test.afterAll(() => {
848
+ recorder.writeReport();
849
+ });
850
+
851
+ function resolveCredential(role: string): Credential {
852
+ const [credential] = loadCredentials(credentialsPath, role);
853
+ if (!credential) {
854
+ throw new Error(\`credentials.json5 中未找到角色: \${role}\`);
855
+ }
856
+ return credential;
857
+ }
858
+
859
+ function applyLoginCredential(credential: Credential): void {
860
+ process.env.LOGIN_SYSTEM_URL = credential.url;
861
+ process.env.LOGIN_USERNAME = credential.username;
862
+ process.env.LOGIN_PASSWORD = credential.password;
863
+ }
864
+
865
+ async function openModule(page: Page): Promise<void> {
866
+ applyLoginCredential(resolveCredential(primaryRole));
867
+ const loginPage = new LoginPage(page);
868
+ await loginPage.login();
869
+ await loginPage.gotoDirectUrl(directListUrl);
870
+ }
871
+
872
+ function workflowMeta(
873
+ input: Omit<
874
+ GlueWorkflowRecordInput,
875
+ 'moduleId' | 'executionSource' | 'evidenceSources' | 'evidenceRefs' | 'playwrightSuiteTitle'
876
+ >,
877
+ ): GlueWorkflowRecordInput {
878
+ return {
879
+ moduleId: ${singleQuoted(moduleId)},
880
+ executionSource: 'glue_code' as const,
881
+ evidenceSources: ['crud_flow_slots'],
882
+ evidenceRefs: [],
883
+ playwrightSuiteTitle: ${singleQuoted(suiteTitle)},
884
+ ...input,
885
+ };
886
+ }
887
+
888
+ test.describe('主表 CRUD 流程', () => {
889
+ test.describe.configure({ mode: 'serial' });
890
+ let workflowData: CrudWorkflowData | undefined;
891
+
892
+ test('Create:新增${moduleLabel}并校验列表存在对应记录', async ({ page }) => {
893
+ test.slow();
894
+
895
+ await runRecordedWorkflow(
896
+ recorder,
897
+ workflowMeta({
898
+ workflowId: ${singleQuoted(`${moduleId}.create`)},
899
+ workflowKind: 'create',
900
+ workflowName: '新增${moduleLabel}',
901
+ businessIntent: '在列表页点击新增按钮,填写表单并保存,验证列表中存在新增的记录。',
902
+ elementActions: ['点击列表页新增按钮打开新增弹框', '在新增弹框中填写表单字段', '点击保存并关闭提交表单', '在列表页搜索并验证记录存在'],
903
+ assertionExpectation: '新增成功后,列表页应能通过搜索找到刚新增的记录。',
904
+ playwrightTestTitle: 'Create:新增${moduleLabel}并校验列表存在对应记录',
905
+ }),
906
+ async () => {
907
+ await openModule(page);
908
+ const crudPage = new CrudPage(page, crudSlots);
909
+ workflowData = crudPage.prepareWorkflowData();
910
+ await crudPage.runCreateWorkflow(workflowData);
911
+ },
912
+ );
913
+ });
914
+
915
+ test('Read:搜索${moduleLabel}并校验搜索结果', async ({ page }) => {
916
+ test.slow();
917
+
918
+ await runRecordedWorkflow(
919
+ recorder,
920
+ workflowMeta({
921
+ workflowId: ${singleQuoted(`${moduleId}.read`)},
922
+ workflowKind: 'read',
923
+ workflowName: '搜索${moduleLabel}',
924
+ businessIntent: '使用搜索条件查询记录,验证搜索结果与搜索条件匹配。',
925
+ elementActions: ['在搜索区域填写搜索条件', '点击搜索按钮提交查询', '在结果列表中校验搜索结果'],
926
+ assertionExpectation: '搜索结果列表中应包含符合搜索条件的记录。',
927
+ playwrightTestTitle: 'Read:搜索${moduleLabel}并校验搜索结果',
928
+ }),
929
+ async () => {
930
+ await openModule(page);
931
+ if (!workflowData) {
932
+ throw new Error('Read 工作流依赖 Create 工作流先行生成 workflowData');
933
+ }
934
+ const crudPage = new CrudPage(page, crudSlots);
935
+ await crudPage.runReadWorkflow(workflowData);
936
+ },
937
+ );
938
+ });
939
+
940
+ test('Update:修改${moduleLabel}并校验修改结果', async ({ page }) => {
941
+ test.slow();
942
+
943
+ await runRecordedWorkflow(
944
+ recorder,
945
+ workflowMeta({
946
+ workflowId: ${singleQuoted(`${moduleId}.update`)},
947
+ workflowKind: 'update',
948
+ workflowName: '修改${moduleLabel}',
949
+ businessIntent: '定位目标记录行,点击修改按钮进入编辑弹框,修改字段后保存,验证修改生效。',
950
+ elementActions: ['在列表中定位目标记录行', '点击行操作中的修改按钮打开编辑弹框', '修改表单字段', '点击保存并关闭提交修改'],
951
+ assertionExpectation: '修改成功后,列表页应显示修改后的字段值。',
952
+ playwrightTestTitle: 'Update:修改${moduleLabel}并校验修改结果',
953
+ }),
954
+ async () => {
955
+ await openModule(page);
956
+ if (!workflowData) {
957
+ throw new Error('Update 工作流依赖 Create 工作流先行生成 workflowData');
958
+ }
959
+ const crudPage = new CrudPage(page, crudSlots);
960
+ await crudPage.runUpdateWorkflow(workflowData);
961
+ },
962
+ );
963
+ });
964
+
965
+ test('Delete:删除${moduleLabel}并校验${deletePolicy === 'blocked_by_business_rule' ? '业务规则拦截' : '删除结果'}', async ({ page }) => {
966
+ test.slow();
967
+
968
+ await runRecordedWorkflow(
969
+ recorder,
970
+ workflowMeta({
971
+ workflowId: ${singleQuoted(`${moduleId}.delete`)},
972
+ workflowKind: 'delete',
973
+ workflowName: '删除${moduleLabel}',
974
+ businessIntent: ${singleQuoted(deleteIntent)},
975
+ elementActions: ['在列表中选择目标记录', '点击删除按钮触发删除确认', ${deletePolicy === 'blocked_by_business_rule' ? "'确认删除操作并验证拦截提示'" : "'确认删除操作'"}],
976
+ assertionExpectation: ${singleQuoted(deleteAssertion)},
977
+ playwrightTestTitle: 'Delete:删除${moduleLabel}并校验${deletePolicy === 'blocked_by_business_rule' ? '业务规则拦截' : '删除结果'}',
978
+ }),
979
+ async () => {
980
+ await openModule(page);
981
+ if (!workflowData) {
982
+ throw new Error('Delete 工作流依赖 Create 工作流先行生成 workflowData');
983
+ }
984
+ const crudPage = new CrudPage(page, crudSlots);
985
+ await crudPage.runDeleteWorkflow(workflowData);
986
+ },
987
+ );
988
+ });
989
+
990
+ });
991
+
992
+ test('Export:导出${moduleLabel}列表并校验下载文件', async ({ page }, testInfo) => {
993
+ test.slow();
994
+
995
+ await runRecordedWorkflow(
996
+ recorder,
997
+ workflowMeta({
998
+ workflowId: ${singleQuoted(`${moduleId}.export`)},
999
+ workflowKind: 'export',
1000
+ workflowName: '导出${moduleLabel}列表',
1001
+ businessIntent: '在列表页触发全部导出,验证导出弹框可驱动并成功下载数据文件。',
1002
+ elementActions: ['点击列表页数据导出按钮打开导出弹框', '在导出弹框选择「全部」并确认导出', '等待 download 事件并断言下载文件存在且非空'],
1003
+ assertionExpectation: '导出操作应触发下载,落盘文件存在且大小大于 0。',
1004
+ playwrightTestTitle: 'Export:导出${moduleLabel}列表并校验下载文件',
1005
+ }),
1006
+ async () => {
1007
+ await openModule(page);
1008
+ const exportPage = new ExportPage(page, exportSlots);
1009
+ await exportPage.runExportWorkflow(testInfo);
1010
+ },
1011
+ );
1012
+ });
1013
+
1014
+ ${childGridSlots?.length ? renderChildGridTests(childGridSlots, crudSlots, moduleLabel, moduleId) : ''}
1015
+
1016
+ // @stage-gap-append
1017
+ });
615
1018
  `;
616
1019
  }
1020
+ /**
1021
+ * @deprecated 旧 v2 协议入口已删除(新链路改用 {@link generateStageSkeletonCrudSpecFromSlots})。
1022
+ * 原实现接收 CrudBusinessModuleContract 并经 mapContractToStageSkeletonCrudSlots 映射,
1023
+ * 新协议不再依赖 contract 直传,故整段移除。下方保留的 contract→slots 辅助函数
1024
+ * 与 renderExportSlots(contract) 仅为历史参考,新链路不引用。
1025
+ */