@epoint-testtech/ep-stage-skill 0.0.11 → 0.0.13

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 (45) hide show
  1. package/codex-skill/ep-stage/code-list-generate/SKILL.md +168 -0
  2. package/codex-skill/ep-stage/code-list-generate/assets/code_list.template.json5 +325 -0
  3. package/codex-skill/ep-stage/code-list-generate/assets/example-zwplace.json5 +588 -0
  4. package/codex-skill/ep-stage/code-list-generate/evals/evals.json +47 -0
  5. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceAddAction.java +119 -0
  6. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceDetailAction.java +70 -0
  7. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceEditAction.java +107 -0
  8. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/action/SchemeResourceListAction.java +167 -0
  9. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/api/ISchemeResourceService.java +84 -0
  10. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/api/entity/SchemeResource.java +218 -0
  11. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/impl/SchemeResourceService.java +146 -0
  12. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/java/com/epoint/schemeresource/impl/SchemeResourceServiceImpl.java +110 -0
  13. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourceadd.html +158 -0
  14. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourcedetail.html +89 -0
  15. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourceedit.html +125 -0
  16. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/schemeresource-req/src/main/webapp/perpage/schemeresource/schemeresourcelist.html +224 -0
  17. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/docs//345/234/272/346/211/200/347/252/227/345/217/243/344/277/241/346/201/257/347/256/241/347/220/206/345/217/212/345/220/204/347/253/257/347/273/237/350/256/241/346/243/200/347/264/242/346/237/245/347/234/213_prd.md +602 -0
  18. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceAddAction.java +210 -0
  19. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceDetailAction.java +69 -0
  20. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceEditAction.java +235 -0
  21. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/action/GxhZwPlaceListAction.java +145 -0
  22. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/IGxhZwPlaceService.java +87 -0
  23. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/entity/GxhZwPlace.java +306 -0
  24. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/entity/GxhZwPlaceHistory.java +205 -0
  25. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/api/entity/GxhZwWindow.java +184 -0
  26. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/impl/GxhZwPlaceService.java +151 -0
  27. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/java/com/epoint/zwplace/impl/GxhZwPlaceServiceImpl.java +96 -0
  28. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplaceadd.html +253 -0
  29. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplacedetail.html +146 -0
  30. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplaceedit.html +275 -0
  31. package/codex-skill/ep-stage/code-list-generate/evals/fixtures/zwplace-req/src/main/webapp/perpage/zwplace/gxhzwplacelist.html +168 -0
  32. package/codex-skill/ep-stage/code-list-generate/evals/grade-code-list.mjs +184 -0
  33. package/codex-skill/ep-stage/code-list-generate/references/epoint-extraction.md +106 -0
  34. package/codex-skill/ep-stage/code-list-generate/references/field-contract.md +122 -0
  35. package/codex-skill/ep-stage/code-list-generate/scripts/validate-code-list.mjs +202 -0
  36. package/codex-skill/ep-stage/glue-create-project/SKILL.md +227 -223
  37. package/dist/src/extractors/code-list-json5.d.ts +5 -0
  38. package/dist/src/extractors/code-list-json5.d.ts.map +1 -1
  39. package/dist/src/generators/stage-skeleton-script.d.ts +4 -0
  40. package/dist/src/generators/stage-skeleton-script.d.ts.map +1 -1
  41. package/dist/src/generators/stage-skeleton-script.js +399 -362
  42. package/dist/src/testcase/contract-testcase-assembler.d.ts +10 -1
  43. package/dist/src/testcase/contract-testcase-assembler.d.ts.map +1 -1
  44. package/dist/src/testcase/contract-testcase-assembler.js +46 -13
  45. package/package.json +1 -1
@@ -385,18 +385,18 @@ function renderSearchCondition(condition, level) {
385
385
  if (condition.searchAssertion !== undefined) {
386
386
  lines.push(`${innerIndent}searchAssertion: ${singleQuoted(condition.searchAssertion)},`);
387
387
  }
388
- return `${indent}{
389
- ${lines.join('\n')}
388
+ return `${indent}{
389
+ ${lines.join('\n')}
390
390
  ${renderListboxSelections(condition.createSelections, level + 4)}${indent}}`;
391
391
  }
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)},
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)},
400
400
  ${indent}}`;
401
401
  }
402
402
  function renderSearchConditions(conditions, level) {
@@ -411,28 +411,30 @@ function renderSearchConditions(conditions, level) {
411
411
  * @returns TypeScript 对象源码
412
412
  */
413
413
  export function renderStageSkeletonCrudSlots(slots) {
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)} },
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
+ ${renderOptionalStringProperty('addDraftSaveButton', slots.locators.addDraftSaveButton, 12)}
427
+ ${renderOptionalStringProperty('addDialogCloseButton', slots.locators.addDialogCloseButton, 12)}
428
+ listSearchSubmit: ${locatorLiteral(slots.locators.listSearchSubmit)},
429
+ listResultValueLocatorTemplate: ${locatorLiteral(slots.locators.listResultValueLocatorTemplate)},
430
+ listEditActionLocatorTemplate: ${locatorLiteral(slots.locators.listEditActionLocatorTemplate)},
431
+ editSaveButton: ${locatorLiteral(slots.locators.editSaveButton)},
432
+ listRowSelectLocatorTemplate: ${locatorLiteral(slots.locators.listRowSelectLocatorTemplate)},
433
+ listDeleteButton: ${locatorLiteral(slots.locators.listDeleteButton)},
434
+ ${renderOptionalStringProperty('deleteConfirmButton', slots.locators.deleteConfirmButton, 12)} },
435
+ assertions: {
436
+ deletePolicy: ${singleQuoted(slots.assertions.deletePolicy)},
437
+ ${renderOptionalStringProperty('blockedDeleteMessage', slots.assertions.blockedDeleteMessage, 12)} },
436
438
  }`;
437
439
  }
438
440
  function renderChildGridColumns(columns, level) {
@@ -479,13 +481,13 @@ function renderChildGridLocators(locators, level) {
479
481
  export function renderChildGridSlots(slots) {
480
482
  if (slots.length === 0)
481
483
  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()},
484
+ const items = slots.map((slot) => `{
485
+ gridId: ${singleQuoted(slot.gridId)},
486
+ label: ${singleQuoted(slot.label)},
487
+ pageRole: ${singleQuoted(slot.pageRole)},
488
+ editMode: ${singleQuoted(slot.editMode)},
489
+ columns: ${renderChildGridColumns(slot.columns, 8).trimStart()},
490
+ locators: ${renderChildGridLocators(slot.locators, 8).trimStart()},
489
491
  }`).join(',\n');
490
492
  return `[\n${items}\n]`;
491
493
  }
@@ -501,10 +503,10 @@ function renderChildGridFillSteps(slot, valuePrefix, indent = 10) {
501
503
  const spaces = ' '.repeat(indent);
502
504
  return slot.columns.map((col) => {
503
505
  if (col.component === 'input') {
504
- return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
506
+ return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
505
507
  ${spaces}await childGridPage.fillInputCell(${singleQuoted(`${valuePrefix}${col.label}`)});`;
506
508
  }
507
- return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
509
+ return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
508
510
  ${spaces}await childGridPage.selectListboxCell(childGridSlots[0].columns.find((c) => c.field === ${singleQuoted(col.field)})!);`;
509
511
  }).join('\n');
510
512
  }
@@ -532,38 +534,38 @@ function renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId, caseR
532
534
  const label = slot.label;
533
535
  const fillSteps = renderChildGridFillSteps(slot, '测试');
534
536
  const verifyAssertions = renderChildGridVerifyAssertions(slot, '测试');
535
- return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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(caseRef?.title ?? `${label}-新增行`)},
544
- businessIntent: ${singleQuoted(`在新增${moduleLabel}时维护${label}子表,验证子表数据随主表保存并持久化。`)},
545
- elementActions: ['进入新增页', '点击子表新增行', '填写子表单元格', '保存主表', '返回列表搜索并校验子表数据'],
546
- assertionExpectation: ${singleQuoted(`保存后重新打开编辑页,${label}子表应显示新增的行数据。`)},
547
- playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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
- );
537
+ return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-新增行并随主表保存`)}, async ({ page }) => {
538
+ test.slow();
539
+
540
+ await runRecordedWorkflow(
541
+ recorder,
542
+ workflowMeta({
543
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.create`)},
544
+ workflowKind: 'create',
545
+ workflowName: ${singleQuoted(caseRef?.title ?? `${label}-新增行`)},
546
+ businessIntent: ${singleQuoted(`在新增${moduleLabel}时维护${label}子表,验证子表数据随主表保存并持久化。`)},
547
+ elementActions: ['进入新增页', '点击子表新增行', '填写子表单元格', '保存主表', '返回列表搜索并校验子表数据'],
548
+ assertionExpectation: ${singleQuoted(`保存后重新打开编辑页,${label}子表应显示新增的行数据。`)},
549
+ playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-新增行并随主表保存`)},
550
+ }),
551
+ async () => {
552
+ await openModule(page);
553
+ const crudPage = new CrudPage(page, crudSlots);
554
+ const childWorkflowData = crudPage.prepareWorkflowData();
555
+ await crudPage.runCreateWorkflowWithChildGrid(childWorkflowData, async (addContext) => {
556
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
557
+ await childGridPage.addRow();
558
+ ${fillSteps}
559
+ });
560
+
561
+ // 验证子表数据持久化:搜索主记录并重新打开编辑页
562
+ const editActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
563
+ await page.locator(editActionLocator).click();
564
+ const editFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
565
+ const verifyChildGridPage = new ChildGridPage(page, editFrame, childGridSlots[0]);
566
+ ${verifyAssertions}
567
+ },
568
+ );
567
569
  });`;
568
570
  }
569
571
  /**
@@ -573,50 +575,50 @@ function renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId, caseR
573
575
  const label = slot.label;
574
576
  const createFillSteps = renderChildGridFillSteps(slot, '测试');
575
577
  const verifyAssertions = renderChildGridVerifyAssertions(slot, '修改后');
576
- return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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(caseRef?.title ?? `${label}-编辑行`)},
585
- businessIntent: ${singleQuoted(`在修改${moduleLabel}时维护${label}子表,验证子表数据随主表保存并持久化。`)},
586
- elementActions: ['进入列表页', '创建带窗口信息子表的主记录', '打开编辑页', '修改子表单元格', '保存主表', '重新搜索并校验子表数据'],
587
- assertionExpectation: ${singleQuoted(`保存后重新打开编辑页,${label}子表应显示更新后的行数据。`)},
588
- playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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
- );
578
+ return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-编辑行并随主表保存`)}, async ({ page }) => {
579
+ test.slow();
580
+
581
+ await runRecordedWorkflow(
582
+ recorder,
583
+ workflowMeta({
584
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.update`)},
585
+ workflowKind: 'update',
586
+ workflowName: ${singleQuoted(caseRef?.title ?? `${label}-编辑行`)},
587
+ businessIntent: ${singleQuoted(`在修改${moduleLabel}时维护${label}子表,验证子表数据随主表保存并持久化。`)},
588
+ elementActions: ['进入列表页', '创建带窗口信息子表的主记录', '打开编辑页', '修改子表单元格', '保存主表', '重新搜索并校验子表数据'],
589
+ assertionExpectation: ${singleQuoted(`保存后重新打开编辑页,${label}子表应显示更新后的行数据。`)},
590
+ playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-编辑行并随主表保存`)},
591
+ }),
592
+ async () => {
593
+ await openModule(page);
594
+ const crudPage = new CrudPage(page, crudSlots);
595
+ const childWorkflowData = crudPage.prepareWorkflowData();
596
+
597
+ // 前置:创建带${label}子表的主记录
598
+ await crudPage.runCreateWorkflowWithChildGrid(childWorkflowData, async (addContext) => {
599
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
600
+ await childGridPage.addRow();
601
+ ${createFillSteps}
602
+ });
603
+
604
+ // 打开编辑页并修改子表行
605
+ const editActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
606
+ await page.locator(editActionLocator).click();
607
+ const editFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
608
+ const childGridPage = new ChildGridPage(page, editFrame, childGridSlots[0]);
609
+ ${renderChildGridFillSteps(slot, '修改后', 8)}
610
+
611
+ // 保存主表
612
+ await editFrame.locator(crudSlots.locators.editSaveButton).click();
613
+
614
+ // 验证子表数据持久化:重新搜索主记录并打开编辑页
615
+ const verifyEditActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
616
+ await page.locator(verifyEditActionLocator).click();
617
+ const verifyEditFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
618
+ const verifyChildGridPage = new ChildGridPage(page, verifyEditFrame, childGridSlots[0]);
619
+ ${verifyAssertions}
620
+ },
621
+ );
620
622
  });`;
621
623
  }
622
624
  /**
@@ -628,49 +630,49 @@ ${verifyAssertions}
628
630
  function renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId, caseRef) {
629
631
  const label = slot.label;
630
632
  const createFillSteps = renderChildGridFillSteps(slot, '测试');
631
- return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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(caseRef?.title ?? `${label}-删除行`)},
640
- businessIntent: ${singleQuoted(`验证${moduleLabel}的${label}子表删除选中行后,数据应被真正移除。`)},
641
- elementActions: ['进入列表页', '创建带窗口信息子表的主记录', '打开编辑页', '勾选子表行', '点击删除选中', '确认删除', '校验子表行已移除'],
642
- assertionExpectation: ${singleQuoted(`确认删除后,${label}子表的数据行数应为 0,被删除的数据不再存在。`)},
643
- playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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
- );
633
+ return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-删除行`)}, async ({ page }) => {
634
+ test.slow();
635
+
636
+ await runRecordedWorkflow(
637
+ recorder,
638
+ workflowMeta({
639
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.delete`)},
640
+ workflowKind: 'delete',
641
+ workflowName: ${singleQuoted(caseRef?.title ?? `${label}-删除行`)},
642
+ businessIntent: ${singleQuoted(`验证${moduleLabel}的${label}子表删除选中行后,数据应被真正移除。`)},
643
+ elementActions: ['进入列表页', '创建带窗口信息子表的主记录', '打开编辑页', '勾选子表行', '点击删除选中', '确认删除', '校验子表行已移除'],
644
+ assertionExpectation: ${singleQuoted(`确认删除后,${label}子表的数据行数应为 0,被删除的数据不再存在。`)},
645
+ playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-删除行`)},
646
+ }),
647
+ async () => {
648
+ await openModule(page);
649
+ const crudPage = new CrudPage(page, crudSlots);
650
+ const childWorkflowData = crudPage.prepareWorkflowData();
651
+
652
+ // 前置:创建带${label}子表的主记录
653
+ await crudPage.runCreateWorkflowWithChildGrid(childWorkflowData, async (addContext) => {
654
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
655
+ await childGridPage.addRow();
656
+ ${createFillSteps}
657
+ });
658
+
659
+ // 打开编辑页
660
+ const editActionLocator = ${locatorLiteral(crudSlots.locators.listEditActionLocatorTemplate)}.replace('{{value}}', childWorkflowData.searchValue);
661
+ await page.locator(editActionLocator).click();
662
+ const editFrame = page.locator(\`iframe[src*="${crudSlots.frames.editIframeSrcKeyword}"]\`).contentFrame();
663
+
664
+ const childGridPage = new ChildGridPage(page, editFrame, childGridSlots[0]);
665
+ const confirmPromise = childGridPage.acceptCustomConfirm('确定要删除');
666
+ await childGridPage.selectRow(1);
667
+ await childGridPage.deleteSelected();
668
+
669
+ await confirmPromise;
670
+
671
+ // 断言删除成功:子表数据行数应为 0
672
+ await page.waitForTimeout(500);
673
+ await childGridPage.assertRowCount(0);
674
+ },
675
+ );
674
676
  });`;
675
677
  }
676
678
  /**
@@ -680,39 +682,39 @@ function renderChildGridRequiredTest(slot, moduleLabel, moduleId, caseRef) {
680
682
  const label = slot.label;
681
683
  const requiredFields = slot.columns.filter((c) => c.required);
682
684
  const requiredLabels = requiredFields.map((c) => c.label).join('、');
683
- return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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(caseRef?.title ?? `${label}-必填校验`)},
692
- businessIntent: ${singleQuoted(`验证${label}子表必填字段未填写时,主表保存应被阻断并给出校验提示。`)},
693
- elementActions: ['进入新增页', '点击子表新增行', '不填必填字段', '保存主表', '观察校验提示'],
694
- assertionExpectation: ${singleQuoted(`系统应阻断保存并提示「${requiredLabels}不能为空」。`)},
695
- playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${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
- );
685
+ return ` test(${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-必填校验`)}, async ({ page }) => {
686
+ test.slow();
687
+
688
+ await runRecordedWorkflow(
689
+ recorder,
690
+ workflowMeta({
691
+ workflowId: ${singleQuoted(`${moduleId}.childGrid.${slot.gridId}.required`)},
692
+ workflowKind: 'create',
693
+ workflowName: ${singleQuoted(caseRef?.title ?? `${label}-必填校验`)},
694
+ businessIntent: ${singleQuoted(`验证${label}子表必填字段未填写时,主表保存应被阻断并给出校验提示。`)},
695
+ elementActions: ['进入新增页', '点击子表新增行', '不填必填字段', '保存主表', '观察校验提示'],
696
+ assertionExpectation: ${singleQuoted(`系统应阻断保存并提示「${requiredLabels}不能为空」。`)},
697
+ playwrightTestTitle: ${singleQuoted(caseRef ? `${caseRef.caseId} ${caseRef.title}` : `${label}-必填校验`)},
698
+ }),
699
+ async () => {
700
+ await openModule(page);
701
+ const crudPage = new CrudPage(page, crudSlots);
702
+ const childWorkflowData = crudPage.prepareWorkflowData();
703
+
704
+ let alertPromise: Promise<void> | undefined;
705
+
706
+ await crudPage.runCreateWorkflowWithChildGridWithoutSearch(childWorkflowData, async (addContext) => {
707
+ const childGridPage = new ChildGridPage(page, addContext, childGridSlots[0]);
708
+ alertPromise = childGridPage.assertAlert('不能为空');
709
+ await childGridPage.addRow();
710
+ });
711
+
712
+ if (!alertPromise) {
713
+ throw new Error('alertPromise 未初始化');
714
+ }
715
+ await alertPromise;
716
+ },
717
+ );
716
718
  });`;
717
719
  }
718
720
  /**
@@ -740,14 +742,28 @@ function renderChildGridTests(slots, crudSlots, moduleLabel, moduleId, glueCases
740
742
  })
741
743
  .join('\n\n');
742
744
  }
743
- /** 解析 glue CRUD 用例标题:`${moduleName} ${actionKind}(${queryKey})`。 */
745
+ /** 解析 glue CRUD 用例标题:`${moduleName} ${actionKind}(${queryKey})`。
746
+ * 条件性删除用例标题带变体后缀:`delete(${queryKey}-草稿)` / `delete(${queryKey}-在用)`,
747
+ * 解析时剥离后缀还原 base queryKey(保证数据分组不受后缀影响),并返回 deleteVariant。 */
744
748
  function parseCrudCaseTitle(title) {
745
749
  const match = title.match(/\s(create|read|update|delete)((.+))\s*$/);
746
750
  if (!match)
747
751
  return null;
752
+ const actionKind = match[1];
753
+ const rawKey = match[2];
754
+ if (actionKind === 'delete') {
755
+ const variantMatch = rawKey.match(/^(.*)-(?:草稿|在用)$/);
756
+ if (variantMatch) {
757
+ return {
758
+ actionKind,
759
+ queryKeyLabel: variantMatch[1],
760
+ deleteVariant: rawKey.endsWith('-草稿') ? 'draft_success' : 'blocked',
761
+ };
762
+ }
763
+ }
748
764
  return {
749
- actionKind: match[1],
750
- queryKeyLabel: match[2],
765
+ actionKind,
766
+ queryKeyLabel: rawKey,
751
767
  };
752
768
  }
753
769
  /** 把用例 steps 文本("1. …。 2. …。")拆成 elementActions 数组。 */
@@ -785,68 +801,89 @@ function renderCrudCaseTests(cases, context) {
785
801
  let body;
786
802
  switch (testCase.actionKind) {
787
803
  case 'create':
788
- body = ` await openModule(page);
789
- const crudPage = new CrudPage(page, crudSlots);
790
- const data = crudPage.prepareWorkflowData();
791
- workflowDataByKey[${singleQuoted(key)}] = data;
804
+ body = ` await openModule(page);
805
+ const crudPage = new CrudPage(page, crudSlots);
806
+ const data = crudPage.prepareWorkflowData();
807
+ workflowDataByKey[${singleQuoted(key)}] = data;
792
808
  await crudPage.runCreateWorkflow(data);`;
793
809
  break;
794
810
  case 'read':
795
- body = ` await openModule(page);
796
- const data = workflowDataByKey[${singleQuoted(dataKey)}];
797
- if (!data) {
798
- throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
799
- }
800
- const crudPage = new CrudPage(page, crudSlots);
811
+ body = ` await openModule(page);
812
+ const data = workflowDataByKey[${singleQuoted(dataKey)}];
813
+ if (!data) {
814
+ throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
815
+ }
816
+ const crudPage = new CrudPage(page, crudSlots);
801
817
  await crudPage.runReadWorkflowByField(data, ${singleQuoted(key)});`;
802
818
  break;
803
819
  case 'update':
804
- body = ` await openModule(page);
805
- const data = workflowDataByKey[${singleQuoted(dataKey)}];
806
- if (!data) {
807
- throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
808
- }
809
- const crudPage = new CrudPage(page, crudSlots);
820
+ body = ` await openModule(page);
821
+ const data = workflowDataByKey[${singleQuoted(dataKey)}];
822
+ if (!data) {
823
+ throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
824
+ }
825
+ const crudPage = new CrudPage(page, crudSlots);
810
826
  ${componentForLabel(key) === 'listbox'
811
827
  ? ` await crudPage.runUpdateListboxWorkflow(data, ${singleQuoted(key)});`
812
828
  : ` await crudPage.runUpdateWorkflow(data);`}`;
813
829
  break;
814
830
  case 'delete':
815
- body = ` await openModule(page);
816
- const data = workflowDataByKey[${singleQuoted(dataKey)}];
817
- if (!data) {
818
- throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
819
- }
820
- const crudPage = new CrudPage(page, crudSlots);
831
+ if (testCase.deleteVariant === 'draft_success') {
832
+ // 条件性删除-可删用例:自建草稿数据(saveMode='draft')再删除,预期成功。
833
+ // 不复用 create 组的在用数据(deleteVariant='failed' 的用例依赖它保持未被删除)。
834
+ body = ` await openModule(page);
835
+ const crudPage = new CrudPage(page, crudSlots);
836
+ const data = crudPage.prepareWorkflowData();
837
+ await crudPage.runCreateWorkflow(data, { saveMode: 'draft' });
838
+ await crudPage.runDeleteWorkflow(data, { expectation: 'success' });`;
839
+ }
840
+ else if (testCase.deleteVariant === 'blocked') {
841
+ // 条件性删除-阻断用例:复用 create 组的在用数据,预期删除失败。
842
+ body = ` await openModule(page);
843
+ const data = workflowDataByKey[${singleQuoted(dataKey)}];
844
+ if (!data) {
845
+ throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
846
+ }
847
+ const crudPage = new CrudPage(page, crudSlots);
848
+ await crudPage.runDeleteWorkflow(data, { expectation: 'failed' });`;
849
+ }
850
+ else {
851
+ body = ` await openModule(page);
852
+ const data = workflowDataByKey[${singleQuoted(dataKey)}];
853
+ if (!data) {
854
+ throw new Error(${singleQuoted(`${testCase.title} 依赖 create(${dataKey})用例先行生成 workflowData`)});
855
+ }
856
+ const crudPage = new CrudPage(page, crudSlots);
821
857
  await crudPage.runDeleteWorkflow(data);`;
858
+ }
822
859
  break;
823
860
  }
824
- return ` test(${singleQuoted(fullTitle)}, async ({ page }) => {
825
- test.slow();
826
-
827
- await runRecordedWorkflow(
828
- recorder,
829
- workflowMeta({
830
- workflowId: ${singleQuoted(`${moduleId}.${testCase.caseId}`)},
831
- workflowKind: '${workflowKind}',
832
- workflowName: ${singleQuoted(testCase.title)},
833
- businessIntent: ${singleQuoted(businessIntent)},
834
- elementActions: [${elementActions}],
835
- assertionExpectation: ${singleQuoted(assertionExpectation)},
836
- playwrightTestTitle: ${singleQuoted(fullTitle)},
837
- }),
838
- async () => {
839
- ${body}
840
- },
841
- );
861
+ return ` test(${singleQuoted(fullTitle)}, async ({ page }) => {
862
+ test.slow();
863
+
864
+ await runRecordedWorkflow(
865
+ recorder,
866
+ workflowMeta({
867
+ workflowId: ${singleQuoted(`${moduleId}.${testCase.caseId}`)},
868
+ workflowKind: '${workflowKind}',
869
+ workflowName: ${singleQuoted(testCase.title)},
870
+ businessIntent: ${singleQuoted(businessIntent)},
871
+ elementActions: [${elementActions}],
872
+ assertionExpectation: ${singleQuoted(assertionExpectation)},
873
+ playwrightTestTitle: ${singleQuoted(fullTitle)},
874
+ }),
875
+ async () => {
876
+ ${body}
877
+ },
878
+ );
842
879
  });`;
843
880
  });
844
- return ` test.describe('主表 CRUD 流程', () => {
845
- test.describe.configure({ mode: 'serial' });
846
- const workflowDataByKey: Record<string, CrudWorkflowData | undefined> = {};
847
-
848
- ${blocks.join('\n\n')}
849
-
881
+ return ` test.describe('主表 CRUD 流程', () => {
882
+ test.describe.configure({ mode: 'serial' });
883
+ const workflowDataByKey: Record<string, CrudWorkflowData | undefined> = {};
884
+
885
+ ${blocks.join('\n\n')}
886
+
850
887
  });`;
851
888
  }
852
889
  /**
@@ -856,18 +893,18 @@ ${blocks.join('\n\n')}
856
893
  * @returns TypeScript 对象字面量源码
857
894
  */
858
895
  function renderExportSlots(contract) {
859
- return `{
860
- moduleId: ${singleQuoted(contract.module.id)},
861
- frames: {
862
- listIframeSrcKeyword: ${singleQuoted(iframeKeyword(contract.pages.list.iframeSrcKeyword, true))},
863
- },
864
- locators: {
865
- exportButton: '.mini-button.mini-dataexport',
866
- },
867
- export: {
868
- label: '导出',
869
- mode: 'all' as const,
870
- },
896
+ return `{
897
+ moduleId: ${singleQuoted(contract.module.id)},
898
+ frames: {
899
+ listIframeSrcKeyword: ${singleQuoted(iframeKeyword(contract.pages.list.iframeSrcKeyword, true))},
900
+ },
901
+ locators: {
902
+ exportButton: '.mini-button.mini-dataexport',
903
+ },
904
+ export: {
905
+ label: '导出',
906
+ mode: 'all' as const,
907
+ },
871
908
  }`;
872
909
  }
873
910
  /**
@@ -883,18 +920,18 @@ function renderExportSlots(contract) {
883
920
  * @returns TypeScript 对象字面量源码
884
921
  */
885
922
  function renderExportSlotsFromSlot(exportSlot, listIframeSrcKeyword) {
886
- return `{
887
- moduleId: ${singleQuoted(exportSlot.moduleId)},
888
- frames: {
889
- listIframeSrcKeyword: ${singleQuoted(listIframeSrcKeyword)},
890
- },
891
- locators: {
892
- exportButton: '.mini-button.mini-dataexport',
893
- },
894
- export: {
895
- label: ${singleQuoted(exportSlot.label)},
896
- mode: ${exportSlot.mode === 'all' ? "'all' as const" : singleQuoted(exportSlot.mode)},
897
- },
923
+ return `{
924
+ moduleId: ${singleQuoted(exportSlot.moduleId)},
925
+ frames: {
926
+ listIframeSrcKeyword: ${singleQuoted(listIframeSrcKeyword)},
927
+ },
928
+ locators: {
929
+ exportButton: '.mini-button.mini-dataexport',
930
+ },
931
+ export: {
932
+ label: ${singleQuoted(exportSlot.label)},
933
+ mode: ${exportSlot.mode === 'all' ? "'all' as const" : singleQuoted(exportSlot.mode)},
934
+ },
898
935
  }`;
899
936
  }
900
937
  /**
@@ -940,102 +977,102 @@ export function generateStageSkeletonCrudSpecFromSlots(options) {
940
977
  }
941
978
  const crudCaseTests = renderCrudCaseTests(crudCases, { moduleId, moduleLabel, componentForLabel });
942
979
  const exportCaseRef = (glueCases ?? []).find((testCase) => testCase.title.endsWith(' export'));
943
- return `import { test, expect, type Page } from '@playwright/test';
944
- import { loadCredentials, StagePath, type Credential } from '@epoint-testtech/stage-core';
945
- import { CrudPage, ExportPage, LoginPage${childGridImport}, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../../skeletons';
946
- import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../../report/glue-report';
947
-
948
- test.describe(${singleQuoted(suiteTitle)}, () => {
949
- const directListUrl = ${singleQuoted(directUrl)};
950
- const credentialsPath = \`\${StagePath.projectRootDirPath}/src/tests/${requirementName}/credentials.json5\`;
951
- const primaryRole = ${singleQuoted(primaryRole)};
952
- const crudSlots: CrudFlowSlots = ${renderStageSkeletonCrudSlots(crudSlots)};
953
- const exportSlots = ${renderExportSlotsFromSlot(exportSlot, crudSlots.frames.listIframeSrcKeyword)};
954
- ${childGridSlots?.length ? ` const childGridSlots: ChildGridFlowSlots[] = ${renderChildGridSlots(childGridSlots)};` : ''}
955
-
956
- const recorder = new GlueWorkflowRecorder({
957
- outputRoot: StagePath.projectRootDirPath,
958
- moduleId: ${singleQuoted(moduleId)},
959
- moduleName: ${singleQuoted(moduleLabel)},
960
- suiteTitle: ${singleQuoted(suiteTitle)},
961
- generatedSpecFile: import.meta.url,
962
- });
963
-
964
- test.beforeAll(async ({ browser }) => {
965
- recorder.updateEnvironment({
966
- browserName: browser.browserType().name(),
967
- browserVersion: browser.version(),
968
- });
969
- });
970
-
971
- test.afterAll(() => {
972
- recorder.writeReport();
973
- });
974
-
975
- function resolveCredential(role: string): Credential {
976
- const [credential] = loadCredentials(credentialsPath, role);
977
- if (!credential) {
978
- throw new Error(\`credentials.json5 中未找到角色: \${role}\`);
979
- }
980
- return credential;
981
- }
982
-
983
- function applyLoginCredential(credential: Credential): void {
984
- process.env.LOGIN_SYSTEM_URL = credential.url;
985
- process.env.LOGIN_USERNAME = credential.username;
986
- process.env.LOGIN_PASSWORD = credential.password;
987
- }
988
-
989
- async function openModule(page: Page): Promise<void> {
990
- applyLoginCredential(resolveCredential(primaryRole));
991
- const loginPage = new LoginPage(page);
992
- await loginPage.login();
993
- await loginPage.gotoDirectUrl(directListUrl);
994
- }
995
-
996
- function workflowMeta(
997
- input: Omit<
998
- GlueWorkflowRecordInput,
999
- 'moduleId' | 'executionSource' | 'evidenceSources' | 'evidenceRefs' | 'playwrightSuiteTitle'
1000
- >,
1001
- ): GlueWorkflowRecordInput {
1002
- return {
1003
- moduleId: ${singleQuoted(moduleId)},
1004
- executionSource: 'glue_code' as const,
1005
- evidenceSources: ['crud_flow_slots'],
1006
- evidenceRefs: [],
1007
- playwrightSuiteTitle: ${singleQuoted(suiteTitle)},
1008
- ...input,
1009
- };
1010
- }
1011
- ${crudCaseTests}
1012
-
1013
- test(${singleQuoted(exportCaseRef ? `${exportCaseRef.caseId} ${exportCaseRef.title}` : `Export:导出${moduleLabel}列表并校验下载文件`)}, async ({ page }, testInfo) => {
1014
- test.slow();
1015
-
1016
- await runRecordedWorkflow(
1017
- recorder,
1018
- workflowMeta({
1019
- workflowId: ${singleQuoted(exportCaseRef ? `${moduleId}.${exportCaseRef.caseId}` : `${moduleId}.export`)},
1020
- workflowKind: 'export',
1021
- workflowName: ${singleQuoted(exportCaseRef ? exportCaseRef.title : `导出${moduleLabel}列表`)},
1022
- businessIntent: '在列表页触发全部导出,验证导出弹框可驱动并成功下载数据文件。',
1023
- elementActions: ['点击列表页数据导出按钮打开导出弹框', '在导出弹框选择「全部」并确认导出', '等待 download 事件并断言下载文件存在且非空'],
1024
- assertionExpectation: '导出操作应触发下载,落盘文件存在且大小大于 0。',
1025
- playwrightTestTitle: ${singleQuoted(exportCaseRef ? `${exportCaseRef.caseId} ${exportCaseRef.title}` : `Export:导出${moduleLabel}列表并校验下载文件`)},
1026
- }),
1027
- async () => {
1028
- await openModule(page);
1029
- const exportPage = new ExportPage(page, exportSlots);
1030
- await exportPage.runExportWorkflow(testInfo);
1031
- },
1032
- );
1033
- });
1034
-
1035
- ${childGridSlots?.length ? renderChildGridTests(childGridSlots, crudSlots, moduleLabel, moduleId, glueCases) : ''}
1036
-
1037
- // @stage-gap-append
1038
- });
980
+ return `import { test, expect, type Page } from '@playwright/test';
981
+ import { loadCredentials, StagePath, type Credential } from '@epoint-testtech/stage-core';
982
+ import { CrudPage, ExportPage, LoginPage${childGridImport}, type CrudFlowSlots, type ExportFlowSlots, type CrudWorkflowData } from '../../skeletons';
983
+ import { GlueWorkflowRecorder, runRecordedWorkflow, type GlueWorkflowRecordInput } from '../../report/glue-report';
984
+
985
+ test.describe(${singleQuoted(suiteTitle)}, () => {
986
+ const directListUrl = ${singleQuoted(directUrl)};
987
+ const credentialsPath = \`\${StagePath.projectRootDirPath}/src/tests/${requirementName}/credentials.json5\`;
988
+ const primaryRole = ${singleQuoted(primaryRole)};
989
+ const crudSlots: CrudFlowSlots = ${renderStageSkeletonCrudSlots(crudSlots)};
990
+ const exportSlots = ${renderExportSlotsFromSlot(exportSlot, crudSlots.frames.listIframeSrcKeyword)};
991
+ ${childGridSlots?.length ? ` const childGridSlots: ChildGridFlowSlots[] = ${renderChildGridSlots(childGridSlots)};` : ''}
992
+
993
+ const recorder = new GlueWorkflowRecorder({
994
+ outputRoot: StagePath.projectRootDirPath,
995
+ moduleId: ${singleQuoted(moduleId)},
996
+ moduleName: ${singleQuoted(moduleLabel)},
997
+ suiteTitle: ${singleQuoted(suiteTitle)},
998
+ generatedSpecFile: import.meta.url,
999
+ });
1000
+
1001
+ test.beforeAll(async ({ browser }) => {
1002
+ recorder.updateEnvironment({
1003
+ browserName: browser.browserType().name(),
1004
+ browserVersion: browser.version(),
1005
+ });
1006
+ });
1007
+
1008
+ test.afterAll(() => {
1009
+ recorder.writeReport();
1010
+ });
1011
+
1012
+ function resolveCredential(role: string): Credential {
1013
+ const [credential] = loadCredentials(credentialsPath, role);
1014
+ if (!credential) {
1015
+ throw new Error(\`credentials.json5 中未找到角色: \${role}\`);
1016
+ }
1017
+ return credential;
1018
+ }
1019
+
1020
+ function applyLoginCredential(credential: Credential): void {
1021
+ process.env.LOGIN_SYSTEM_URL = credential.url;
1022
+ process.env.LOGIN_USERNAME = credential.username;
1023
+ process.env.LOGIN_PASSWORD = credential.password;
1024
+ }
1025
+
1026
+ async function openModule(page: Page): Promise<void> {
1027
+ applyLoginCredential(resolveCredential(primaryRole));
1028
+ const loginPage = new LoginPage(page);
1029
+ await loginPage.login();
1030
+ await loginPage.gotoDirectUrl(directListUrl);
1031
+ }
1032
+
1033
+ function workflowMeta(
1034
+ input: Omit<
1035
+ GlueWorkflowRecordInput,
1036
+ 'moduleId' | 'executionSource' | 'evidenceSources' | 'evidenceRefs' | 'playwrightSuiteTitle'
1037
+ >,
1038
+ ): GlueWorkflowRecordInput {
1039
+ return {
1040
+ moduleId: ${singleQuoted(moduleId)},
1041
+ executionSource: 'glue_code' as const,
1042
+ evidenceSources: ['crud_flow_slots'],
1043
+ evidenceRefs: [],
1044
+ playwrightSuiteTitle: ${singleQuoted(suiteTitle)},
1045
+ ...input,
1046
+ };
1047
+ }
1048
+ ${crudCaseTests}
1049
+
1050
+ test(${singleQuoted(exportCaseRef ? `${exportCaseRef.caseId} ${exportCaseRef.title}` : `Export:导出${moduleLabel}列表并校验下载文件`)}, async ({ page }, testInfo) => {
1051
+ test.slow();
1052
+
1053
+ await runRecordedWorkflow(
1054
+ recorder,
1055
+ workflowMeta({
1056
+ workflowId: ${singleQuoted(exportCaseRef ? `${moduleId}.${exportCaseRef.caseId}` : `${moduleId}.export`)},
1057
+ workflowKind: 'export',
1058
+ workflowName: ${singleQuoted(exportCaseRef ? exportCaseRef.title : `导出${moduleLabel}列表`)},
1059
+ businessIntent: '在列表页触发全部导出,验证导出弹框可驱动并成功下载数据文件。',
1060
+ elementActions: ['点击列表页数据导出按钮打开导出弹框', '在导出弹框选择「全部」并确认导出', '等待 download 事件并断言下载文件存在且非空'],
1061
+ assertionExpectation: '导出操作应触发下载,落盘文件存在且大小大于 0。',
1062
+ playwrightTestTitle: ${singleQuoted(exportCaseRef ? `${exportCaseRef.caseId} ${exportCaseRef.title}` : `Export:导出${moduleLabel}列表并校验下载文件`)},
1063
+ }),
1064
+ async () => {
1065
+ await openModule(page);
1066
+ const exportPage = new ExportPage(page, exportSlots);
1067
+ await exportPage.runExportWorkflow(testInfo);
1068
+ },
1069
+ );
1070
+ });
1071
+
1072
+ ${childGridSlots?.length ? renderChildGridTests(childGridSlots, crudSlots, moduleLabel, moduleId, glueCases) : ''}
1073
+
1074
+ // @stage-gap-append
1075
+ });
1039
1076
  `;
1040
1077
  }
1041
1078
  /**