@epoint-testtech/ep-stage-skill 0.0.4-alpha.0 → 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.
- package/SKILL.md +27 -27
- package/codex-skill/ep-stage/glue-create-project/SKILL.md +223 -186
- package/codex-skill/ep-stage/glue-generate-testcase/SKILL.md +275 -199
- package/codex-skill/ep-stage/glue-generate-testcase/examples/observable-testcase.json +40 -40
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-schema.md +95 -112
- package/codex-skill/ep-stage/glue-generate-testcase/references/testcase-tpl.md +72 -0
- package/codex-skill/ep-stage/glue-run-test/SKILL.md +292 -249
- package/codex-skill/ep-stage/glue-run-test/references/ai-runtime-loop.md +30 -0
- package/codex-skill/ep-stage/glue-run-test/references/crud-pipeline.md +132 -145
- package/codex-skill/ep-stage/glue-run-test/references/gap-review-protocol.md +44 -43
- package/codex-skill/ep-stage/glue-run-test/references/harness-principles.md +46 -46
- package/codex-skill/ep-stage/glue-run-test/references/playwright-cli-odav.md +166 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/glue-run-test/scripts/generate-crud-spec.mjs +149 -149
- package/codex-skill/ep-stage/page-element-scan/SKILL.md +91 -0
- package/codex-skill/ep-stage/page-element-scan/evals/evals.json +37 -0
- package/codex-skill/ep-stage/page-element-scan/evals/fixtures/sample-list-page.html +85 -0
- package/codex-skill/ep-stage/page-element-scan/references/pagination-controls.md +153 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements-eval.mjs +233 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/extract-page-elements.mjs +298 -0
- package/codex-skill/ep-stage/page-element-scan/scripts/scan-page.mjs +749 -0
- package/codex-skill/ep-stage/recording-to-glue/SKILL.md +28 -28
- package/codex-skill/ep-stage/scripts/validate-skill.mjs +98 -95
- package/dist/src/cli/generate-testcase.d.ts +18 -51
- package/dist/src/cli/generate-testcase.d.ts.map +1 -1
- package/dist/src/cli/generate-testcase.js +127 -131
- package/dist/src/cli/index.d.ts +11 -2
- package/dist/src/cli/index.d.ts.map +1 -1
- package/dist/src/cli/index.js +49 -16
- package/dist/src/cli/probe.d.ts +2 -0
- package/dist/src/cli/probe.d.ts.map +1 -1
- package/dist/src/cli/probe.js +239 -13
- package/dist/src/cli/run.d.ts +136 -29
- package/dist/src/cli/run.d.ts.map +1 -1
- package/dist/src/cli/run.js +494 -65
- package/dist/src/cli/spec.d.ts +18 -11
- package/dist/src/cli/spec.d.ts.map +1 -1
- package/dist/src/cli/spec.js +74 -20
- package/dist/src/contracts/crud-business-module.d.ts +6 -0
- package/dist/src/contracts/crud-business-module.d.ts.map +1 -1
- package/dist/src/contracts/glue-contract-v1.d.ts +98 -0
- package/dist/src/contracts/glue-contract-v1.d.ts.map +1 -0
- package/dist/src/contracts/glue-contract-v1.js +9 -0
- package/dist/src/contracts/inference-trace.d.ts +223 -0
- package/dist/src/contracts/inference-trace.d.ts.map +1 -0
- package/dist/src/contracts/inference-trace.js +12 -0
- package/dist/src/contracts/observable-chain.d.ts +4 -0
- package/dist/src/contracts/observable-chain.d.ts.map +1 -1
- package/dist/src/extractors/code-list-json5.d.ts +192 -0
- package/dist/src/extractors/code-list-json5.d.ts.map +1 -0
- package/dist/src/extractors/code-list-json5.js +91 -0
- package/dist/src/extractors/code-list.d.ts +3 -3
- package/dist/src/extractors/code-list.d.ts.map +1 -1
- package/dist/src/extractors/code-list.js +46 -11
- package/dist/src/extractors/html-page.d.ts +9 -0
- package/dist/src/extractors/html-page.d.ts.map +1 -1
- package/dist/src/extractors/html-page.js +26 -2
- package/dist/src/extractors/prd-extractor.d.ts +70 -0
- package/dist/src/extractors/prd-extractor.d.ts.map +1 -0
- package/dist/src/extractors/prd-extractor.js +248 -0
- package/dist/src/generators/agent-inferred-workflow-script.js +66 -66
- package/dist/src/generators/stage-skeleton-script.d.ts +87 -20
- package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
- package/dist/src/generators/stage-skeleton-script.js +656 -238
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/inference/module-hints-inference.d.ts +30 -0
- package/dist/src/inference/module-hints-inference.d.ts.map +1 -0
- package/dist/src/inference/module-hints-inference.js +363 -0
- package/dist/src/inference/source-analysis.d.ts +26 -0
- package/dist/src/inference/source-analysis.d.ts.map +1 -0
- package/dist/src/inference/source-analysis.js +258 -0
- package/dist/src/normalizers/crud-contract.d.ts +3 -0
- package/dist/src/normalizers/crud-contract.d.ts.map +1 -1
- package/dist/src/normalizers/crud-contract.js +3 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts +88 -0
- package/dist/src/runtime/ai-runtime-trace.d.ts.map +1 -0
- package/dist/src/runtime/ai-runtime-trace.js +119 -0
- package/dist/src/testcase/button-interaction-inference.d.ts +37 -0
- package/dist/src/testcase/button-interaction-inference.d.ts.map +1 -0
- package/dist/src/testcase/button-interaction-inference.js +385 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts +44 -0
- package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -0
- package/dist/src/testcase/contract-testcase-assembler.js +806 -0
- package/dist/src/testcase/prd-button-mapper.d.ts +72 -0
- package/dist/src/testcase/prd-button-mapper.d.ts.map +1 -0
- package/dist/src/testcase/prd-button-mapper.js +148 -0
- package/dist/src/testcase/runtime-context.d.ts +9 -0
- package/dist/src/testcase/runtime-context.d.ts.map +1 -0
- package/dist/src/testcase/runtime-context.js +1 -0
- package/dist/src/testcase/test-access.d.ts +38 -0
- package/dist/src/testcase/test-access.d.ts.map +1 -0
- package/dist/src/testcase/test-access.js +106 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts +10 -0
- package/dist/src/testcase/testcase-execution-fields.d.ts.map +1 -0
- package/dist/src/testcase/testcase-execution-fields.js +1 -0
- package/dist/src/testcase/testcase-generator.d.ts.map +1 -1
- package/dist/src/testcase/testcase-generator.js +6 -0
- package/dist/src/testcase/testcase-markdown.d.ts +11 -0
- package/dist/src/testcase/testcase-markdown.d.ts.map +1 -0
- package/dist/src/testcase/testcase-markdown.js +181 -0
- package/dist/src/testcase/testcase-skeleton.d.ts +4 -0
- package/dist/src/testcase/testcase-skeleton.d.ts.map +1 -1
- package/dist/src/testcase/testcase-spec-assembly.d.ts +4 -0
- package/dist/src/testcase/testcase-spec-assembly.d.ts.map +1 -1
- package/dist/src/testcase/testcase-v1-draft.d.ts +58 -0
- package/dist/src/testcase/testcase-v1-draft.d.ts.map +1 -0
- package/dist/src/testcase/testcase-v1-draft.js +13 -0
- package/dist/src/validation/contract.d.ts +14 -0
- package/dist/src/validation/contract.d.ts.map +1 -0
- package/dist/src/validation/contract.js +132 -0
- package/dist/src/validation/credentials.d.ts +7 -2
- package/dist/src/validation/credentials.d.ts.map +1 -1
- package/dist/src/validation/credentials.js +15 -4
- package/dist/src/validation/index.d.ts +2 -0
- package/dist/src/validation/index.d.ts.map +1 -1
- package/dist/src/validation/index.js +2 -0
- package/dist/src/validation/inference-trace.d.ts +13 -0
- package/dist/src/validation/inference-trace.d.ts.map +1 -0
- package/dist/src/validation/inference-trace.js +31 -0
- package/dist/src/validation/testcase.d.ts +9 -7
- package/dist/src/validation/testcase.d.ts.map +1 -1
- package/dist/src/validation/testcase.js +181 -34
- package/docs/README.md +124 -134
- package/docs/mvp-usage-guide.md +298 -298
- package/examples/schemeresource-observable-docs/schemeresource.context.md +20 -20
- package/examples/schemeresource.module-hints.json +38 -38
- package/examples/schemeresource.observable.code_list.md +37 -37
- package/examples/zwplace-observable-docs/zwplace.context.md +16 -16
- package/examples/zwplace-placecategory-validation.json +29 -29
- package/examples/zwplace.module-hints.json +69 -69
- package/examples/zwplace.observable.code_list.md +37 -37
- package/package.json +2 -2
- package/dist/test/cli/extract-contract.test.d.ts +0 -2
- package/dist/test/cli/extract-contract.test.d.ts.map +0 -1
- package/dist/test/cli/extract-contract.test.js +0 -32
- package/dist/test/cli/generate-testcase.test.d.ts +0 -2
- package/dist/test/cli/generate-testcase.test.d.ts.map +0 -1
- package/dist/test/cli/generate-testcase.test.js +0 -130
- package/dist/test/cli/index.test.d.ts +0 -2
- package/dist/test/cli/index.test.d.ts.map +0 -1
- package/dist/test/cli/index.test.js +0 -93
- package/dist/test/cli/run.test.d.ts +0 -2
- package/dist/test/cli/run.test.d.ts.map +0 -1
- package/dist/test/cli/run.test.js +0 -149
- package/dist/test/cli/spec.test.d.ts +0 -2
- package/dist/test/cli/spec.test.d.ts.map +0 -1
- package/dist/test/cli/spec.test.js +0 -196
- package/dist/test/crud-contract.test.d.ts +0 -2
- package/dist/test/crud-contract.test.d.ts.map +0 -1
- package/dist/test/crud-contract.test.js +0 -819
- package/dist/test/gap-inference.test.d.ts +0 -2
- package/dist/test/gap-inference.test.d.ts.map +0 -1
- package/dist/test/gap-inference.test.js +0 -597
- package/dist/test/generalization.test.d.ts +0 -2
- package/dist/test/generalization.test.d.ts.map +0 -1
- package/dist/test/generalization.test.js +0 -73
- package/dist/test/material-inventory.test.d.ts +0 -2
- package/dist/test/material-inventory.test.d.ts.map +0 -1
- package/dist/test/material-inventory.test.js +0 -141
- package/dist/test/observable-chain.test.d.ts +0 -2
- package/dist/test/observable-chain.test.d.ts.map +0 -1
- package/dist/test/observable-chain.test.js +0 -123
- package/dist/test/observable-pipeline.test.d.ts +0 -2
- package/dist/test/observable-pipeline.test.d.ts.map +0 -1
- package/dist/test/observable-pipeline.test.js +0 -461
- package/dist/test/page-structure.test.d.ts +0 -2
- package/dist/test/page-structure.test.d.ts.map +0 -1
- package/dist/test/page-structure.test.js +0 -45
- package/dist/test/scenario-inference.test.d.ts +0 -2
- package/dist/test/scenario-inference.test.d.ts.map +0 -1
- package/dist/test/scenario-inference.test.js +0 -73
- package/dist/test/stage-context.test.d.ts +0 -2
- package/dist/test/stage-context.test.d.ts.map +0 -1
- package/dist/test/stage-context.test.js +0 -395
- package/dist/test/testcase-generator.test.d.ts +0 -2
- package/dist/test/testcase-generator.test.d.ts.map +0 -1
- package/dist/test/testcase-generator.test.js +0 -276
- package/dist/test/testcase-skeleton.test.d.ts +0 -2
- package/dist/test/testcase-skeleton.test.d.ts.map +0 -1
- package/dist/test/testcase-skeleton.test.js +0 -185
- package/dist/test/testcase-spec-assembly.test.d.ts +0 -2
- package/dist/test/testcase-spec-assembly.test.d.ts.map +0 -1
- package/dist/test/testcase-spec-assembly.test.js +0 -105
- package/dist/test/util/credentials.test.d.ts +0 -2
- package/dist/test/util/credentials.test.d.ts.map +0 -1
- package/dist/test/util/credentials.test.js +0 -64
- package/dist/test/util/i18n-testcase.test.d.ts +0 -2
- package/dist/test/util/i18n-testcase.test.d.ts.map +0 -1
- package/dist/test/util/i18n-testcase.test.js +0 -119
- package/dist/test/util/softlink.test.d.ts +0 -2
- package/dist/test/util/softlink.test.d.ts.map +0 -1
- package/dist/test/util/softlink.test.js +0 -82
- package/dist/test/validation/credentials.test.d.ts +0 -2
- package/dist/test/validation/credentials.test.d.ts.map +0 -1
- package/dist/test/validation/credentials.test.js +0 -72
- package/dist/test/validation/projects-index.test.d.ts +0 -2
- package/dist/test/validation/projects-index.test.d.ts.map +0 -1
- package/dist/test/validation/projects-index.test.js +0 -48
- package/dist/test/validation/testcase.test.d.ts +0 -2
- package/dist/test/validation/testcase.test.d.ts.map +0 -1
- package/dist/test/validation/testcase.test.js +0 -129
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -7
|
@@ -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
|
|
@@ -184,6 +197,13 @@ function findBlockedDeleteMessage(contract) {
|
|
|
184
197
|
/**
|
|
185
198
|
* 将契约中的数据生成策略映射为骨架支持的策略。
|
|
186
199
|
*
|
|
200
|
+
* 设计取舍:契约把"测试人员定"的策略标成 `human_required`(属于 unresolvedSlots,由 tester 决策)。
|
|
201
|
+
* 但 spec 组装阶段缺少喂策略的顺滑入口(spec CLI 不收 hints、contract 禁手编),
|
|
202
|
+
* 若硬抛错会导致测试人员审过用例后仍卡死。
|
|
203
|
+
* 故 `human_required` / 缺省时统一**默认到 `timestamp_prefix`**——这正是 unresolvedSlots
|
|
204
|
+
* `suggestedFormat` 自身给出的建议值,也是 CRUD 冒烟造唯一数据的标配;
|
|
205
|
+
* unresolvedSlots 仍保留作审计标记,测试人员后续可通过 ModuleHints 显式覆盖。
|
|
206
|
+
*
|
|
187
207
|
* @param strategy 契约数据生成策略
|
|
188
208
|
* @returns 骨架数据生成策略
|
|
189
209
|
*/
|
|
@@ -192,6 +212,8 @@ function mapDataStrategy(strategy) {
|
|
|
192
212
|
return 'fixed_value';
|
|
193
213
|
if (strategy === 'timestamp_prefix')
|
|
194
214
|
return 'timestamp_prefix';
|
|
215
|
+
if (strategy === 'human_required' || strategy === undefined)
|
|
216
|
+
return 'timestamp_prefix';
|
|
195
217
|
throw new Error(`当前数据生成策略无法自动映射到 CrudFlowSlots: ${strategy}`);
|
|
196
218
|
}
|
|
197
219
|
/**
|
|
@@ -313,13 +335,32 @@ function renderOptionalStringProperty(key, value, level) {
|
|
|
313
335
|
return `${' '.repeat(level)}${key}: ${singleQuoted(value)},\n`;
|
|
314
336
|
}
|
|
315
337
|
function renderListboxSelections(selections, level) {
|
|
316
|
-
if (!selections
|
|
338
|
+
if (!selections) {
|
|
317
339
|
return '';
|
|
318
340
|
}
|
|
319
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
|
+
}
|
|
320
348
|
const itemIndent = ' '.repeat(level + 4);
|
|
321
349
|
const items = selections
|
|
322
|
-
.map((selection) =>
|
|
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
|
+
})
|
|
323
364
|
.join(',\n');
|
|
324
365
|
return `${indent}createSelections: [\n${items},\n${indent}],\n`;
|
|
325
366
|
}
|
|
@@ -327,21 +368,35 @@ function renderSearchCondition(condition, level) {
|
|
|
327
368
|
const indent = ' '.repeat(level);
|
|
328
369
|
const innerIndent = ' '.repeat(level + 4);
|
|
329
370
|
if (condition.component === 'listbox') {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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')}
|
|
335
390
|
${renderListboxSelections(condition.createSelections, level + 4)}${indent}}`;
|
|
336
391
|
}
|
|
337
|
-
return `${indent}{
|
|
338
|
-
${innerIndent}component: 'input',
|
|
339
|
-
${innerIndent}field: ${singleQuoted(condition.field)},
|
|
340
|
-
${innerIndent}label: ${singleQuoted(condition.label)},
|
|
341
|
-
${innerIndent}seedValue: ${singleQuoted(condition.seedValue)},
|
|
342
|
-
${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)},
|
|
343
|
-
${innerIndent}editLocator: ${locatorLiteral(condition.editLocator)},
|
|
344
|
-
${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)},
|
|
345
400
|
${indent}}`;
|
|
346
401
|
}
|
|
347
402
|
function renderSearchConditions(conditions, level) {
|
|
@@ -356,30 +411,333 @@ function renderSearchConditions(conditions, level) {
|
|
|
356
411
|
* @returns TypeScript 对象源码
|
|
357
412
|
*/
|
|
358
413
|
export function renderStageSkeletonCrudSlots(slots) {
|
|
359
|
-
return `{
|
|
360
|
-
moduleId: ${singleQuoted(slots.moduleId)},
|
|
361
|
-
${renderSearchConditions(slots.searchConditions, 8)}
|
|
362
|
-
frames: {
|
|
363
|
-
listIframeSrcKeyword: ${singleQuoted(slots.frames.listIframeSrcKeyword)},
|
|
364
|
-
addIframeSrcKeyword: ${singleQuoted(slots.frames.addIframeSrcKeyword)},
|
|
365
|
-
editIframeSrcKeyword: ${singleQuoted(slots.frames.editIframeSrcKeyword)},
|
|
366
|
-
},
|
|
367
|
-
locators: {
|
|
368
|
-
createButton: ${locatorLiteral(slots.locators.createButton)},
|
|
369
|
-
${renderOptionalStringProperty('addAutofillTrigger', slots.locators.addAutofillTrigger, 12)}
|
|
370
|
-
addSaveButton: ${locatorLiteral(slots.locators.addSaveButton)},
|
|
371
|
-
listSearchSubmit: ${locatorLiteral(slots.locators.listSearchSubmit)},
|
|
372
|
-
listResultValueLocatorTemplate: ${locatorLiteral(slots.locators.listResultValueLocatorTemplate)},
|
|
373
|
-
listEditActionLocatorTemplate: ${locatorLiteral(slots.locators.listEditActionLocatorTemplate)},
|
|
374
|
-
editSaveButton: ${locatorLiteral(slots.locators.editSaveButton)},
|
|
375
|
-
listRowSelectLocatorTemplate: ${locatorLiteral(slots.locators.listRowSelectLocatorTemplate)},
|
|
376
|
-
listDeleteButton: ${locatorLiteral(slots.locators.listDeleteButton)},
|
|
377
|
-
${renderOptionalStringProperty('deleteConfirmButton', slots.locators.deleteConfirmButton, 12)} },
|
|
378
|
-
assertions: {
|
|
379
|
-
deletePolicy: ${singleQuoted(slots.assertions.deletePolicy)},
|
|
380
|
-
${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)} },
|
|
381
436
|
}`;
|
|
382
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
|
+
}
|
|
383
741
|
/**
|
|
384
742
|
* 渲染导出骨架槽位对象。
|
|
385
743
|
*
|
|
@@ -387,38 +745,67 @@ ${renderOptionalStringProperty('blockedDeleteMessage', slots.assertions.blockedD
|
|
|
387
745
|
* @returns TypeScript 对象字面量源码
|
|
388
746
|
*/
|
|
389
747
|
function renderExportSlots(contract) {
|
|
390
|
-
return `{
|
|
391
|
-
moduleId: ${singleQuoted(contract.module.id)},
|
|
392
|
-
frames: {
|
|
393
|
-
listIframeSrcKeyword: ${singleQuoted(iframeKeyword(contract.pages.list.iframeSrcKeyword, true))},
|
|
394
|
-
},
|
|
395
|
-
locators: {
|
|
396
|
-
exportButton: '.mini-button.mini-dataexport',
|
|
397
|
-
},
|
|
398
|
-
export: {
|
|
399
|
-
label: '导出',
|
|
400
|
-
mode: 'all' as const,
|
|
401
|
-
},
|
|
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
|
+
},
|
|
760
|
+
}`;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
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
|
+
},
|
|
402
787
|
}`;
|
|
403
788
|
}
|
|
404
789
|
/**
|
|
405
|
-
*
|
|
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**。
|
|
406
796
|
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
797
|
+
* - crudSlots 直接调 {@link renderStageSkeletonCrudSlots}(复用现有 render)。
|
|
798
|
+
* - exportSlot 调 {@link renderExportSlotsFromSlot}(listIframeSrcKeyword 取自 crudSlots.frames)。
|
|
799
|
+
* - deletePolicy 从 crudSlots.assertions.deletePolicy 取(不再从 contract.flows.delete 推)。
|
|
410
800
|
*
|
|
411
|
-
* @param options
|
|
801
|
+
* @param options 生成参数(全部来自 contract.skeletonSlots + testcase 导航/模块信息)
|
|
412
802
|
* @returns 完整的 `.spec.ts` 源码
|
|
413
803
|
*/
|
|
414
|
-
export function
|
|
415
|
-
const {
|
|
416
|
-
const slots = mapContractToStageSkeletonCrudSlots(contract);
|
|
417
|
-
const moduleId = contract.module.id;
|
|
418
|
-
const moduleLabel = contract.module.label;
|
|
804
|
+
export function generateStageSkeletonCrudSpecFromSlots(options) {
|
|
805
|
+
const { moduleId, moduleLabel, directUrl, primaryRole, requirementName, crudSlots, exportSlot, childGridSlots } = options;
|
|
419
806
|
const suiteTitle = `${moduleLabel} CRUD + Export`;
|
|
420
|
-
const deletePolicy =
|
|
421
|
-
const blockedDeleteMessage =
|
|
807
|
+
const deletePolicy = crudSlots.assertions.deletePolicy;
|
|
808
|
+
const blockedDeleteMessage = crudSlots.assertions.blockedDeleteMessage;
|
|
422
809
|
// 删除工作流的业务意图和断言取决于 deletePolicy
|
|
423
810
|
const deleteIntent = deletePolicy === 'blocked_by_business_rule'
|
|
424
811
|
? `在列表页选择${moduleLabel}记录后点击删除,验证业务规则拦截删除操作${blockedDeleteMessage ? `(${blockedDeleteMessage})` : ''}。`
|
|
@@ -426,182 +813,213 @@ export function generateStageSkeletonCrudSpec(options) {
|
|
|
426
813
|
const deleteAssertion = deletePolicy === 'blocked_by_business_rule'
|
|
427
814
|
? `删除操作应被业务规则拦截${blockedDeleteMessage ? `,提示「${blockedDeleteMessage}」` : ''},列表保留该记录。`
|
|
428
815
|
: '删除成功后,列表中不再存在该记录,搜索结果为空。';
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
import {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
);
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
})
|
|
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
|
+
});
|
|
606
1018
|
`;
|
|
607
1019
|
}
|
|
1020
|
+
/**
|
|
1021
|
+
* @deprecated 旧 v2 协议入口已删除(新链路改用 {@link generateStageSkeletonCrudSpecFromSlots})。
|
|
1022
|
+
* 原实现接收 CrudBusinessModuleContract 并经 mapContractToStageSkeletonCrudSlots 映射,
|
|
1023
|
+
* 新协议不再依赖 contract 直传,故整段移除。下方保留的 contract→slots 辅助函数
|
|
1024
|
+
* 与 renderExportSlots(contract) 仅为历史参考,新链路不引用。
|
|
1025
|
+
*/
|