@epoint-testtech/ep-stage-skill 0.0.7 → 0.0.9

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.
@@ -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,28 @@ 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
+ 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)} },
436
436
  }`;
437
437
  }
438
438
  function renderChildGridColumns(columns, level) {
@@ -479,13 +479,13 @@ function renderChildGridLocators(locators, level) {
479
479
  export function renderChildGridSlots(slots) {
480
480
  if (slots.length === 0)
481
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()},
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
489
  }`).join(',\n');
490
490
  return `[\n${items}\n]`;
491
491
  }
@@ -501,10 +501,10 @@ function renderChildGridFillSteps(slot, valuePrefix, indent = 10) {
501
501
  const spaces = ' '.repeat(indent);
502
502
  return slot.columns.map((col) => {
503
503
  if (col.component === 'input') {
504
- return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
504
+ return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
505
505
  ${spaces}await childGridPage.fillInputCell(${singleQuoted(`${valuePrefix}${col.label}`)});`;
506
506
  }
507
- return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
507
+ return `${spaces}await childGridPage.editCell(1, ${singleQuoted(col.field)});
508
508
  ${spaces}await childGridPage.selectListboxCell(childGridSlots[0].columns.find((c) => c.field === ${singleQuoted(col.field)})!);`;
509
509
  }).join('\n');
510
510
  }
@@ -528,95 +528,95 @@ function renderChildGridVerifyAssertions(slot, valuePrefix) {
528
528
  /**
529
529
  * 渲染单个子表的新增行测试。
530
530
  */
531
- function renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId) {
531
+ function renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId, caseRef) {
532
532
  const label = slot.label;
533
533
  const fillSteps = renderChildGridFillSteps(slot, '测试');
534
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
- );
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
+ );
567
567
  });`;
568
568
  }
569
569
  /**
570
570
  * 渲染单个子表的编辑行测试。
571
571
  */
572
- function renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId) {
572
+ function renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId, caseRef) {
573
573
  const label = slot.label;
574
574
  const createFillSteps = renderChildGridFillSteps(slot, '测试');
575
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
- );
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
+ );
620
620
  });`;
621
621
  }
622
622
  /**
@@ -625,100 +625,100 @@ ${verifyAssertions}
625
625
  * 注意:本测试会断言删除成功后子表行被真正移除。若系统存在 confirm 确定后仍删除失败的 bug,
626
626
  * 该断言将失败,从而暴露问题,而不是为了绕过 bug 而降低断言标准。
627
627
  */
628
- function renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId) {
628
+ function renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId, caseRef) {
629
629
  const label = slot.label;
630
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
- );
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
+ );
674
674
  });`;
675
675
  }
676
676
  /**
677
677
  * 渲染单个子表的必填校验测试。
678
678
  */
679
- function renderChildGridRequiredTest(slot, moduleLabel, moduleId) {
679
+ function renderChildGridRequiredTest(slot, moduleLabel, moduleId, caseRef) {
680
680
  const label = slot.label;
681
681
  const requiredFields = slot.columns.filter((c) => c.required);
682
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
- );
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
+ );
716
716
  });`;
717
717
  }
718
718
  /**
719
719
  * 渲染所有子表测试块(新增行、编辑行、删除行、必填校验)。
720
720
  */
721
- function renderChildGridTests(slots, crudSlots, moduleLabel, moduleId) {
721
+ function renderChildGridTests(slots, crudSlots, moduleLabel, moduleId, glueCases) {
722
722
  if (slots.length === 0)
723
723
  return '';
724
724
  return slots
@@ -729,15 +729,126 @@ function renderChildGridTests(slots, crudSlots, moduleLabel, moduleId) {
729
729
  if (slot.editMode === 'row_button_dialog') {
730
730
  return [` // TODO: ${slot.label} 子表为行按钮弹窗编辑模式,测试待实现`];
731
731
  }
732
+ // 子表用例标题(如「窗口信息-新增行」)→ caseId 引用,供测试标题带用例ID。
733
+ const caseRefOf = (suffix) => glueCases?.find((testCase) => testCase.title === `${slot.label}-${suffix}`);
732
734
  return [
733
- renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId),
734
- renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId),
735
- renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId),
736
- renderChildGridRequiredTest(slot, moduleLabel, moduleId),
735
+ renderChildGridCreateTest(slot, crudSlots, moduleLabel, moduleId, caseRefOf('新增行')),
736
+ renderChildGridUpdateTest(slot, crudSlots, moduleLabel, moduleId, caseRefOf('编辑行')),
737
+ renderChildGridDeleteTest(slot, crudSlots, moduleLabel, moduleId, caseRefOf('删除行')),
738
+ renderChildGridRequiredTest(slot, moduleLabel, moduleId, caseRefOf('必填校验')),
737
739
  ];
738
740
  })
739
741
  .join('\n\n');
740
742
  }
743
+ /** 解析 glue CRUD 用例标题:`${moduleName} ${actionKind}(${queryKey})`。 */
744
+ function parseCrudCaseTitle(title) {
745
+ const match = title.match(/\s(create|read|update|delete)((.+))\s*$/);
746
+ if (!match)
747
+ return null;
748
+ return {
749
+ actionKind: match[1],
750
+ queryKeyLabel: match[2],
751
+ };
752
+ }
753
+ /** 把用例 steps 文本("1. …。 2. …。")拆成 elementActions 数组。 */
754
+ function splitCaseSteps(steps, fallback) {
755
+ if (!steps)
756
+ return fallback;
757
+ const parts = steps
758
+ .split(/(?=\d+\.\s)/)
759
+ .map((part) => part.replace(/^\d+\.\s*/, '').trim())
760
+ .filter(Boolean);
761
+ return parts.length > 0 ? parts : fallback;
762
+ }
763
+ /**
764
+ * 按用例逐条渲染主表 CRUD 测试(一用例一脚本)。
765
+ *
766
+ * 串行 describe 内用 workflowDataByKey 按查询主键分组共享记录:
767
+ * create 用例新建记录;同键 read/update/delete 复用;只读键(如系统管理的状态字段)
768
+ * 挂靠最近一个 create 组的记录。用例顺序即装配器产出顺序。
769
+ */
770
+ function renderCrudCaseTests(cases, context) {
771
+ const { moduleId, componentForLabel } = context;
772
+ const createKeys = new Set(cases.filter((c) => c.actionKind === 'create').map((c) => c.queryKeyLabel));
773
+ let lastCreateKey = '';
774
+ const blocks = cases.map((testCase) => {
775
+ const fullTitle = `${testCase.caseId} ${testCase.title}`;
776
+ const key = testCase.queryKeyLabel;
777
+ if (testCase.actionKind === 'create') {
778
+ lastCreateKey = key;
779
+ }
780
+ const dataKey = createKeys.has(key) ? key : lastCreateKey;
781
+ const workflowKind = testCase.actionKind;
782
+ const elementActions = splitCaseSteps(testCase.steps, ['按用例步骤执行']).map((a) => singleQuoted(a)).join(', ');
783
+ const businessIntent = testCase.steps ?? `按用例「${testCase.title}」步骤执行。`;
784
+ const assertionExpectation = testCase.assertion ?? '按用例断言验证执行结果。';
785
+ let body;
786
+ switch (testCase.actionKind) {
787
+ case 'create':
788
+ body = ` await openModule(page);
789
+ const crudPage = new CrudPage(page, crudSlots);
790
+ const data = crudPage.prepareWorkflowData();
791
+ workflowDataByKey[${singleQuoted(key)}] = data;
792
+ await crudPage.runCreateWorkflow(data);`;
793
+ break;
794
+ 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);
801
+ await crudPage.runReadWorkflowByField(data, ${singleQuoted(key)});`;
802
+ break;
803
+ 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);
810
+ ${componentForLabel(key) === 'listbox'
811
+ ? ` await crudPage.runUpdateListboxWorkflow(data, ${singleQuoted(key)});`
812
+ : ` await crudPage.runUpdateWorkflow(data);`}`;
813
+ break;
814
+ 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);
821
+ await crudPage.runDeleteWorkflow(data);`;
822
+ break;
823
+ }
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
+ );
842
+ });`;
843
+ });
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
+
850
+ });`;
851
+ }
741
852
  /**
742
853
  * 渲染导出骨架槽位对象。
743
854
  *
@@ -745,18 +856,18 @@ function renderChildGridTests(slots, crudSlots, moduleLabel, moduleId) {
745
856
  * @returns TypeScript 对象字面量源码
746
857
  */
747
858
  function renderExportSlots(contract) {
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
- },
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
+ },
760
871
  }`;
761
872
  }
762
873
  /**
@@ -772,18 +883,18 @@ function renderExportSlots(contract) {
772
883
  * @returns TypeScript 对象字面量源码
773
884
  */
774
885
  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
- },
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
+ },
787
898
  }`;
788
899
  }
789
900
  /**
@@ -802,219 +913,129 @@ function renderExportSlotsFromSlot(exportSlot, listIframeSrcKeyword) {
802
913
  * @returns 完整的 `.spec.ts` 源码
803
914
  */
804
915
  export function generateStageSkeletonCrudSpecFromSlots(options) {
805
- const { moduleId, moduleLabel, directUrl, primaryRole, requirementName, crudSlots, exportSlot, childGridSlots } = options;
916
+ const { moduleId, moduleLabel, directUrl, primaryRole, requirementName, crudSlots, exportSlot, childGridSlots, glueCases } = options;
806
917
  const suiteTitle = `${moduleLabel} CRUD + Export`;
807
- const deletePolicy = crudSlots.assertions.deletePolicy;
808
- const blockedDeleteMessage = crudSlots.assertions.blockedDeleteMessage;
809
- // 删除工作流的业务意图和断言取决于 deletePolicy
810
- const deleteIntent = deletePolicy === 'blocked_by_business_rule'
811
- ? `在列表页选择${moduleLabel}记录后点击删除,验证业务规则拦截删除操作${blockedDeleteMessage ? `(${blockedDeleteMessage})` : ''}。`
812
- : `在列表页选择${moduleLabel}记录后点击删除并确认,验证列表中不再存在该记录。`;
813
- const deleteAssertion = deletePolicy === 'blocked_by_business_rule'
814
- ? `删除操作应被业务规则拦截${blockedDeleteMessage ? `,提示「${blockedDeleteMessage}」` : ''},列表保留该记录。`
815
- : '删除成功后,列表中不再存在该记录,搜索结果为空。';
816
918
  const childGridImport = childGridSlots?.length
817
919
  ? ', ChildGridPage, type ChildGridFlowSlots'
818
920
  : '';
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
- });
921
+ // 一用例一脚本:按 glue 用例逐条渲染 CRUD 测试,保证报告结果数 == 用例数。
922
+ const componentForLabel = (label) => crudSlots.searchConditions.find((c) => c.label === label)?.component ?? 'input';
923
+ const crudCases = (glueCases ?? []).flatMap((testCase) => {
924
+ const parsed = parseCrudCaseTitle(testCase.title);
925
+ return parsed ? [{ ...testCase, ...parsed }] : [];
926
+ });
927
+ // 子表用例标题形如「窗口信息-新增行」,export 用例以「 export」结尾,均不参与主表 CRUD 映射。
928
+ const childGridTitles = new Set((childGridSlots ?? []).flatMap((slot) => [
929
+ `${slot.label}-新增行`,
930
+ `${slot.label}-编辑行`,
931
+ `${slot.label}-删除行`,
932
+ `${slot.label}-必填校验`,
933
+ ]));
934
+ const unmappable = (glueCases ?? []).filter((testCase) => !parseCrudCaseTitle(testCase.title) &&
935
+ !testCase.title.endsWith(' export') &&
936
+ !childGridTitles.has(testCase.title));
937
+ if (unmappable.length > 0) {
938
+ throw new Error(`glue 用例标题无法映射到 CRUD 阶段:${unmappable.map((c) => `${c.caseId}(${c.title})`).join('、')}。` +
939
+ '期望格式「<模块名> create|read|update|delete(<查询主键>)」。');
940
+ }
941
+ const crudCaseTests = renderCrudCaseTests(crudCases, { moduleId, moduleLabel, componentForLabel });
942
+ 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
+ });
1018
1039
  `;
1019
1040
  }
1020
1041
  /**