@sun-asterisk/sungen 3.2.18 → 3.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +4 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +4 -2
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +10 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +19 -1
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +4 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/dashboard/html-renderer.d.ts.map +1 -1
- package/dist/dashboard/html-renderer.js +7 -2
- package/dist/dashboard/html-renderer.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +3 -0
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/types.d.ts +4 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-environment.d.ts.map +1 -1
- package/dist/exporters/api-environment.js +4 -24
- package/dist/exporters/api-environment.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +6 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +11 -4
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +40 -9
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/env-qa.d.ts +13 -0
- package/dist/exporters/env-qa.d.ts.map +1 -0
- package/dist/exporters/env-qa.js +82 -0
- package/dist/exporters/env-qa.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +16 -33
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +57 -20
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +6 -5
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/gates.d.ts.map +1 -1
- package/dist/exporters/matrix/gates.js +3 -2
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/step-buckets.d.ts +13 -0
- package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
- package/dist/exporters/matrix/step-buckets.js +7 -0
- package/dist/exporters/matrix/step-buckets.js.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.js +143 -0
- package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +20 -13
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +55 -30
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/step-formatter.d.ts +42 -33
- package/dist/exporters/step-formatter.d.ts.map +1 -1
- package/dist/exporters/step-formatter.js +68 -68
- package/dist/exporters/step-formatter.js.map +1 -1
- package/dist/exporters/types.d.ts +9 -8
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +70 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +7 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +6 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
- package/dist/generators/test-generator/recipe-expander.js +1 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +17 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +108 -17
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +8 -4
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +8 -7
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +2 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +1 -55
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +14 -9
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +1 -3
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +9 -7
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +4 -3
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts +12 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +28 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +52 -30
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +3 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +14 -10
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +15 -4
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/trace.js +1 -1
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/unit-paths.d.ts +16 -0
- package/dist/harness/unit-paths.d.ts.map +1 -1
- package/dist/harness/unit-paths.js +39 -2
- package/dist/harness/unit-paths.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +8 -2
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/dist/orchestrator/templates/playwright.config.ts +33 -2
- package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-mock.js +137 -0
- package/dist/orchestrator/templates/specs-mock.js.map +1 -0
- package/dist/orchestrator/templates/specs-mock.ts +179 -0
- package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
- package/dist/orchestrator/test-data-guide.js +4 -0
- package/dist/orchestrator/test-data-guide.js.map +1 -1
- package/package.json +3 -3
- package/src/capabilities/discover.ts +4 -1
- package/src/cli/commands/add.ts +4 -2
- package/src/cli/commands/dashboard.ts +10 -1
- package/src/cli/commands/delivery.ts +19 -1
- package/src/cli/commands/generate.ts +4 -1
- package/src/dashboard/html-renderer.ts +8 -2
- package/src/dashboard/snapshot-builder.ts +3 -0
- package/src/dashboard/types.ts +4 -0
- package/src/exporters/api-environment.ts +3 -20
- package/src/exporters/api-testcase-formatter.ts +8 -2
- package/src/exporters/csv-exporter.ts +47 -8
- package/src/exporters/env-qa.ts +41 -0
- package/src/exporters/feature-parser.ts +17 -38
- package/src/exporters/json-exporter.ts +71 -24
- package/src/exporters/matrix/build.ts +6 -5
- package/src/exporters/matrix/gates.ts +3 -2
- package/src/exporters/matrix/step-buckets.ts +15 -0
- package/src/exporters/qa-env-secret-denylist.ts +118 -0
- package/src/exporters/scenario-merger.ts +80 -42
- package/src/exporters/step-formatter.ts +67 -66
- package/src/exporters/types.ts +9 -8
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/src/generators/test-generator/code-generator.ts +82 -7
- package/src/generators/test-generator/diagnostics.ts +7 -1
- package/src/generators/test-generator/recipe-expander.ts +2 -2
- package/src/generators/test-generator/step-mapper.ts +106 -15
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/generators/test-generator/utils/data-resolver.ts +8 -5
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
- package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
- package/src/harness/audit.ts +10 -9
- package/src/harness/capability-plan.ts +2 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/challenge.ts +2 -3
- package/src/harness/data-driven-lint.ts +13 -9
- package/src/harness/depth-lint.ts +2 -4
- package/src/harness/flow-check.ts +9 -7
- package/src/harness/flow-plan.ts +4 -3
- package/src/harness/manifest.ts +3 -3
- package/src/harness/parse.ts +28 -1
- package/src/harness/script-check.ts +52 -32
- package/src/harness/secret-scan.ts +11 -7
- package/src/harness/sensors.ts +15 -4
- package/src/harness/trace.ts +2 -2
- package/src/harness/unit-paths.ts +39 -2
- package/src/index.ts +1 -0
- package/src/orchestrator/screen-manager.ts +7 -2
- package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/src/orchestrator/templates/playwright.config.ts +33 -2
- package/src/orchestrator/templates/specs-mock.ts +179 -0
- package/src/orchestrator/test-data-guide.ts +4 -0
|
@@ -56,27 +56,34 @@ export interface MergedScenario {
|
|
|
56
56
|
spec: SpecTest | null;
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* without re-parsing compiled spec.ts comments.
|
|
59
|
+
* Feature Background steps, kept separate from `resolvedSteps` so the
|
|
60
|
+
* exporters render them in the Pre-condition column. Inlining them into
|
|
61
|
+
* Steps shifted the step numbering against the .feature source, which made
|
|
62
|
+
* QA's Gherkin-vs-export review a manual re-mapping exercise.
|
|
64
63
|
*/
|
|
65
|
-
|
|
66
|
-
resolvedThenSteps: string[];
|
|
67
|
-
resolvedGivenSteps: string[];
|
|
64
|
+
resolvedBackgroundSteps: OrderedStep[];
|
|
68
65
|
/**
|
|
69
66
|
* Steps section for display, in chronological order:
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
67
|
+
* @extend base (all incl. its Then) → own given/when-bucket steps.
|
|
68
|
+
* Base's `Then` stays here because it's mid-flow setup, not the
|
|
69
|
+
* scenario's final assertion. Background is NOT included — it renders
|
|
70
|
+
* as Pre-condition (see `resolvedBackgroundSteps`).
|
|
73
71
|
*/
|
|
74
72
|
resolvedSteps: OrderedStep[];
|
|
75
73
|
/**
|
|
76
|
-
* Expected results — own scenario's
|
|
77
|
-
*
|
|
74
|
+
* Expected results — own scenario's then-bucket steps (a `When` after a
|
|
75
|
+
* `Then` starts a new action and goes back to Steps; an `And` follows its
|
|
76
|
+
* preceding explicit keyword). Empty when the scenario has no `Then`.
|
|
78
77
|
*/
|
|
79
78
|
resolvedExpected: OrderedStep[];
|
|
79
|
+
/**
|
|
80
|
+
* Count of anchor-eligible action lines (@extend base + own given/when
|
|
81
|
+
* steps). This — NOT the rendered Steps length — is the reference for the
|
|
82
|
+
* "is any assertion mid-flow?" check in groupExpectedByAnchor: manual-comment
|
|
83
|
+
* steps are appended to resolvedSteps but never participate in anchoring,
|
|
84
|
+
* so counting them would falsely mark @manual scenarios as interleaved.
|
|
85
|
+
*/
|
|
86
|
+
resolvedAnchorActionCount: number;
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/**
|
|
@@ -98,20 +105,40 @@ export function mergeFeatureAndSpec(
|
|
|
98
105
|
if (isStepsBaseScenario(scenario)) continue; // skip @steps base scenarios
|
|
99
106
|
if (isSampleScaffoldScenario(scenario)) continue; // skip default sample
|
|
100
107
|
|
|
101
|
-
const specTest = findMatchingSpecTest(scenario, spec.tests);
|
|
108
|
+
const specTest = findMatchingSpecTest(scenario, spec.tests, feature.scenarios);
|
|
102
109
|
const base = scenario.extendsName ? stepsBaseByName.get(scenario.extendsName) : undefined;
|
|
103
110
|
|
|
104
111
|
// Order of execution for a runnable scenario:
|
|
105
|
-
// 1. Feature Background (once) —
|
|
112
|
+
// 1. Feature Background (once) — setup state, rendered as Pre-condition
|
|
113
|
+
// so Steps numbering stays aligned with the .feature scenario.
|
|
106
114
|
// 2. @extend base scenario's own steps (if any) — *including* its Then,
|
|
107
115
|
// because the base's Then is a mid-flow assertion, not the scenario's
|
|
108
116
|
// final verification.
|
|
109
|
-
// 3. This scenario's own steps
|
|
110
|
-
//
|
|
117
|
+
// 3. This scenario's own steps, partitioned by BUCKET (not by position):
|
|
118
|
+
// given/when → Steps, then → Expected results. A positional split at
|
|
119
|
+
// the first `Then` broke interleaved scenarios (When → Then → When →
|
|
120
|
+
// Then …): every later `When` action was exported as an "expected
|
|
121
|
+
// result". classifySteps already resolves `And` to its preceding
|
|
122
|
+
// explicit keyword, so an `And` after `Then` stays an assertion and
|
|
123
|
+
// an `And` after `When` stays an action. Relative order is preserved
|
|
124
|
+
// within each column.
|
|
125
|
+
// Each assertion also records its ANCHOR — the 1-based index of the
|
|
126
|
+
// last action before it in the rendered Steps column (@extend base
|
|
127
|
+
// steps count toward the numbering; Background does not, it renders
|
|
128
|
+
// as Pre-condition). The exporters use anchors to append
|
|
129
|
+
// "— after step N" when a scenario interleaves actions and
|
|
130
|
+
// assertions, restoring the correspondence the 2-column layout loses.
|
|
111
131
|
const ownOrdered = scenario.orderedSteps;
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
const
|
|
132
|
+
const baseLen = base?.orderedSteps.length ?? 0;
|
|
133
|
+
const ownActions: OrderedStep[] = [];
|
|
134
|
+
const ownAssertions: OrderedStep[] = [];
|
|
135
|
+
for (const s of ownOrdered) {
|
|
136
|
+
if (s.bucket === 'then') {
|
|
137
|
+
ownAssertions.push({ ...s, anchorStep: baseLen + ownActions.length });
|
|
138
|
+
} else {
|
|
139
|
+
ownActions.push(s);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
115
142
|
|
|
116
143
|
// A @manual scenario carries no Gherkin steps — its tester procedure lives in the
|
|
117
144
|
// `# Tester verifies:` comment block. Parse it into Steps (Setup/Action) + Expected
|
|
@@ -125,49 +152,60 @@ export function mergeFeatureAndSpec(
|
|
|
125
152
|
result.push({
|
|
126
153
|
feature: scenario,
|
|
127
154
|
spec: specTest,
|
|
128
|
-
|
|
129
|
-
...feature.backgroundWhenSteps,
|
|
130
|
-
...(base?.rawWhenSteps ?? []),
|
|
131
|
-
...scenario.rawWhenSteps,
|
|
132
|
-
],
|
|
133
|
-
resolvedThenSteps: [
|
|
134
|
-
...feature.backgroundThenSteps,
|
|
135
|
-
...(base?.rawThenSteps ?? []),
|
|
136
|
-
...scenario.rawThenSteps,
|
|
137
|
-
],
|
|
138
|
-
resolvedGivenSteps: [
|
|
139
|
-
...feature.backgroundGivenSteps,
|
|
140
|
-
...(base?.rawGivenSteps ?? []),
|
|
141
|
-
...scenario.rawGivenSteps,
|
|
142
|
-
],
|
|
155
|
+
resolvedBackgroundSteps: feature.backgroundOrderedSteps,
|
|
143
156
|
resolvedSteps: [
|
|
144
|
-
...feature.backgroundOrderedSteps,
|
|
145
157
|
...(base?.orderedSteps ?? []),
|
|
146
|
-
...
|
|
158
|
+
...ownActions,
|
|
147
159
|
...manualParsed.steps,
|
|
148
160
|
],
|
|
149
|
-
resolvedExpected: [...
|
|
161
|
+
resolvedExpected: [...ownAssertions, ...manualParsed.expected],
|
|
162
|
+
resolvedAnchorActionCount: baseLen + ownActions.length,
|
|
150
163
|
});
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
return result;
|
|
154
167
|
}
|
|
155
168
|
|
|
169
|
+
/** `@manual` / `@manual:<reason>` — a manual scenario has no automated test by definition. */
|
|
170
|
+
function isManualScenario(scenario: ScenarioMetadata): boolean {
|
|
171
|
+
return scenario.tags.some((t) => t === '@manual' || t.startsWith('@manual:'));
|
|
172
|
+
}
|
|
173
|
+
|
|
156
174
|
/**
|
|
157
175
|
* Find the spec test that corresponds to a given feature scenario.
|
|
158
176
|
* Match by scenario title (exact match on the scenario name).
|
|
177
|
+
*
|
|
178
|
+
* The VP-ID fallback exists only to survive small TITLE drift between compile
|
|
179
|
+
* and export — it must never RE-ASSIGN a result to a different scenario. Field
|
|
180
|
+
* report: a feature carried the same VP id on many scenarios (VP-NAV-001 ×14,
|
|
181
|
+
* mixing @manual and automated); the old first-match fallback attached an
|
|
182
|
+
* automated sibling's executed result to every @manual sibling, so 7 testcases
|
|
183
|
+
* nobody ever ran shipped as "Passed" in the client deliverable. Two guards:
|
|
184
|
+
* 1. a @manual scenario never matches ANY test;
|
|
185
|
+
* 2. the fallback applies only when the vpId is UNIQUE on both sides —
|
|
186
|
+
* exactly one test AND exactly one automatable scenario carry it.
|
|
187
|
+
* Ambiguity → null → the row reports an honest "Pending".
|
|
159
188
|
*/
|
|
160
|
-
function findMatchingSpecTest(
|
|
189
|
+
function findMatchingSpecTest(
|
|
190
|
+
scenario: ScenarioMetadata,
|
|
191
|
+
tests: SpecTest[],
|
|
192
|
+
allScenarios: ScenarioMetadata[],
|
|
193
|
+
): SpecTest | null {
|
|
194
|
+
if (isManualScenario(scenario)) return null;
|
|
195
|
+
|
|
161
196
|
// Exact title match
|
|
162
197
|
const exact = tests.find((t) => t.scenarioName === scenario.name);
|
|
163
198
|
if (exact) return exact;
|
|
164
199
|
|
|
165
|
-
// Fallback: match by VP-ID prefix
|
|
200
|
+
// Fallback: match by VP-ID prefix — only when unique both ways (see above).
|
|
166
201
|
const vpMatch = scenario.name.match(SCENARIO_ID);
|
|
167
202
|
if (vpMatch) {
|
|
168
203
|
const vpId = vpMatch[1];
|
|
169
|
-
const
|
|
170
|
-
|
|
204
|
+
const testsWithVp = tests.filter((t) => t.vpId === vpId);
|
|
205
|
+
const scenariosWithVp = allScenarios.filter(
|
|
206
|
+
(s) => !isManualScenario(s) && s.name.match(SCENARIO_ID)?.[1] === vpId,
|
|
207
|
+
);
|
|
208
|
+
if (testsWithVp.length === 1 && scenariosWithVp.length === 1) return testsWithVp[0];
|
|
171
209
|
}
|
|
172
210
|
|
|
173
211
|
return null;
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Convert raw Gherkin step text
|
|
3
|
-
*
|
|
2
|
+
* Convert raw Gherkin step text into concise natural language for the
|
|
3
|
+
* CSV Steps/Expected columns.
|
|
4
4
|
*
|
|
5
5
|
* Deterministic — no AI calls.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
* Format a list of raw step lines for the Steps / Expected Results columns.
|
|
10
|
-
*
|
|
11
|
-
* - 0 lines → empty string
|
|
12
|
-
* - 1 line → the line verbatim (no "1. " prefix — avoids Google Sheets clutter)
|
|
13
|
-
* - ≥ 2 → each line on its own row, "1. ", "2. ", … prefixes, newline-joined
|
|
14
|
-
* so spreadsheet cells auto-wrap one step per visual line.
|
|
15
|
-
*/
|
|
16
|
-
export function formatNumberedSteps(lines: string[]): string {
|
|
17
|
-
const cleaned = lines.map(cleanStepLine).filter((l) => l.length > 0);
|
|
18
|
-
if (cleaned.length === 0) return '';
|
|
19
|
-
if (cleaned.length === 1) return cleaned[0];
|
|
20
|
-
return cleaned.map((line, idx) => `${idx + 1}. ${line}`).join('\n');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
8
|
/**
|
|
24
9
|
* Strip the leading actor (`User `) from a raw Gherkin step and capitalise
|
|
25
10
|
* the first letter. Brackets `[ref]`, `{{var}}` placeholders and element
|
|
@@ -42,64 +27,80 @@ export function keepActor(raw: string): string {
|
|
|
42
27
|
}
|
|
43
28
|
|
|
44
29
|
/**
|
|
45
|
-
*
|
|
46
|
-
* preserves the original Gherkin punctuation (no bracket-to-quote conversion).
|
|
47
|
-
* The mapper picks between `stripActor` (When/Then) and `keepActor`
|
|
48
|
-
* (Given) so the actor word matches the step's grammatical role.
|
|
49
|
-
* Use this for feature-direct rendering (dashboard + delivery).
|
|
50
|
-
*/
|
|
51
|
-
export function formatFeatureSteps(lines: string[], mapper: (s: string) => string = stripActor): string {
|
|
52
|
-
const cleaned = lines.map(mapper).filter((l) => l.length > 0);
|
|
53
|
-
if (cleaned.length === 0) return '';
|
|
54
|
-
if (cleaned.length === 1) return cleaned[0];
|
|
55
|
-
return cleaned.map((line, idx) => `${idx + 1}. ${line}`).join('\n');
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Clean a raw step line (either Gherkin text or .spec.ts comment).
|
|
30
|
+
* Group formatted Expected-results lines by their anchor step.
|
|
60
31
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
32
|
+
* Single source of truth for the anchor rendering (used by csv-exporter AND
|
|
33
|
+
* json-exporter so the delivery sheet and the dashboard cannot drift). Final
|
|
34
|
+
* format per user review 2026-08-06:
|
|
35
|
+
*
|
|
36
|
+
* - When assertions verify DIFFERENT moments (more than one distinct
|
|
37
|
+
* anchor, or the single anchor is not the final action), the cell is
|
|
38
|
+
* rendered as groups — a "— after step N" header line (anchor 0 →
|
|
39
|
+
* "— before step 1") followed by that group's assertions. Within a
|
|
40
|
+
* group, lines are numbered 1..k only when the group has more than one
|
|
41
|
+
* assertion; a lone assertion stays bare.
|
|
42
|
+
* - When ALL assertions verify the final action (the common shape — every
|
|
43
|
+
* Then after the last When), no headers appear and the caller renders
|
|
44
|
+
* the plain 1..M checklist exactly as before the anchor feature.
|
|
45
|
+
* - Pure-assertion scenarios (actionCount 0) and manual-comment entries
|
|
46
|
+
* (no anchor) never produce headers; unanchored entries trail as their
|
|
47
|
+
* own headerless group.
|
|
48
|
+
*
|
|
49
|
+
* `grouped` tells the caller headers were emitted, so it can force-number
|
|
50
|
+
* the Steps cell even when it has a single line (headers reference step
|
|
51
|
+
* numbers QA must be able to see) and must join the lines verbatim instead
|
|
52
|
+
* of applying checklist numbering.
|
|
53
|
+
*
|
|
54
|
+
* Anchors are computed against the merger's action list; formatted gherkin
|
|
55
|
+
* lines are never empty (keepActor/stripActor on non-empty text), so the
|
|
56
|
+
* rendered numbering cannot drift from the anchor indices.
|
|
67
57
|
*/
|
|
68
|
-
export function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
s = s.replace(/\s+/g, ' ');
|
|
80
|
-
|
|
81
|
-
// For .spec.ts comments that start with "Assert ..." keep as-is (they read naturally)
|
|
82
|
-
// For action comments like "Click X", "Fill X with Y" keep as-is.
|
|
83
|
-
|
|
84
|
-
// Capitalize first letter
|
|
85
|
-
if (s.length > 0) s = s.charAt(0).toUpperCase() + s.slice(1);
|
|
58
|
+
export function groupExpectedByAnchor(
|
|
59
|
+
entries: Array<{ text: string; anchorStep?: number }>,
|
|
60
|
+
actionCount: number
|
|
61
|
+
): { lines: string[]; grouped: boolean } {
|
|
62
|
+
const anchors = entries
|
|
63
|
+
.filter((e) => e.anchorStep !== undefined)
|
|
64
|
+
.map((e) => e.anchorStep as number);
|
|
65
|
+
const distinct = new Set(anchors);
|
|
66
|
+
const grouped =
|
|
67
|
+
distinct.size > 1 || (distinct.size === 1 && anchors[0] !== actionCount);
|
|
68
|
+
if (!grouped) return { lines: entries.map((e) => e.text), grouped };
|
|
86
69
|
|
|
87
|
-
|
|
70
|
+
const lines: string[] = [];
|
|
71
|
+
let i = 0;
|
|
72
|
+
while (i < entries.length) {
|
|
73
|
+
const anchor = entries[i].anchorStep;
|
|
74
|
+
const group: string[] = [];
|
|
75
|
+
while (i < entries.length && entries[i].anchorStep === anchor) {
|
|
76
|
+
group.push(entries[i].text);
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
if (anchor !== undefined) {
|
|
80
|
+
lines.push(anchor === 0 ? '— before step 1' : `— after step ${anchor}`);
|
|
81
|
+
}
|
|
82
|
+
if (group.length === 1) lines.push(group[0]);
|
|
83
|
+
else group.forEach((t, k) => lines.push(`${k + 1}. ${t}`));
|
|
84
|
+
}
|
|
85
|
+
return { lines, grouped };
|
|
88
86
|
}
|
|
89
87
|
|
|
90
88
|
/**
|
|
91
|
-
* Pre-condition — login/auth context
|
|
89
|
+
* Pre-condition — login/auth context + feature Background steps.
|
|
92
90
|
*
|
|
93
91
|
* `@auth:user` → `Logged in as user`
|
|
94
92
|
* `@no-auth` → `Not authenticated`
|
|
95
|
-
* (no auth tag) →
|
|
93
|
+
* (no auth tag) → no auth line
|
|
96
94
|
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
95
|
+
* `backgroundLines` (formatted feature Background steps) follow the auth
|
|
96
|
+
* line, one per line. Background belongs here rather than in Steps: inlining
|
|
97
|
+
* it shifted the Steps numbering against the .feature scenario, forcing QA
|
|
98
|
+
* to re-map step numbers between Gherkin and the exported sheet.
|
|
100
99
|
*/
|
|
101
|
-
export function formatPrecondition(authRole: string | null,
|
|
102
|
-
|
|
103
|
-
if (authRole
|
|
104
|
-
|
|
100
|
+
export function formatPrecondition(authRole: string | null, backgroundLines: string[] = []): string {
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
if (authRole === 'no-auth') lines.push('Not authenticated');
|
|
103
|
+
else if (authRole) lines.push(`Logged in as ${authRole}`);
|
|
104
|
+
lines.push(...backgroundLines.filter((l) => l.length > 0));
|
|
105
|
+
return lines.join('\n');
|
|
105
106
|
}
|
package/src/exporters/types.ts
CHANGED
|
@@ -59,11 +59,8 @@ export interface FeatureMetadata {
|
|
|
59
59
|
featureName: string; // e.g., "Create Kudo Modal"
|
|
60
60
|
featurePath?: string; // e.g., "/kudos"
|
|
61
61
|
featureTags: string[]; // Feature-level tags
|
|
62
|
-
/** Background block
|
|
63
|
-
* scenarios — the merger
|
|
64
|
-
backgroundGivenSteps: string[];
|
|
65
|
-
backgroundWhenSteps: string[];
|
|
66
|
-
backgroundThenSteps: string[];
|
|
62
|
+
/** Background block in chronological order. Shared across all runnable
|
|
63
|
+
* scenarios — the merger surfaces it as `resolvedBackgroundSteps`. */
|
|
67
64
|
backgroundOrderedSteps: OrderedStep[];
|
|
68
65
|
scenarios: ScenarioMetadata[];
|
|
69
66
|
}
|
|
@@ -71,6 +68,13 @@ export interface FeatureMetadata {
|
|
|
71
68
|
export interface OrderedStep {
|
|
72
69
|
text: string;
|
|
73
70
|
bucket: 'given' | 'when' | 'then';
|
|
71
|
+
/**
|
|
72
|
+
* For then-bucket entries in `MergedScenario.resolvedExpected`: 1-based index
|
|
73
|
+
* of the last action line (in the rendered Steps column) this assertion
|
|
74
|
+
* verifies; 0 when the assertion precedes any action. Set by
|
|
75
|
+
* mergeFeatureAndSpec — absent on action steps and manual-comment entries.
|
|
76
|
+
*/
|
|
77
|
+
anchorStep?: number;
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
export interface ScenarioMetadata {
|
|
@@ -79,9 +83,6 @@ export interface ScenarioMetadata {
|
|
|
79
83
|
stepsName?: string; // @steps:<name>
|
|
80
84
|
extendsName?: string; // @extend:<name>
|
|
81
85
|
referencedVars: string[]; // Variables {{var}} used in this scenario
|
|
82
|
-
rawGivenSteps: string[]; // Raw Given/And-after-Given step text
|
|
83
|
-
rawWhenSteps: string[]; // Raw When/And-after-When step text
|
|
84
|
-
rawThenSteps: string[]; // Raw Then/And-after-Then step text
|
|
85
86
|
/**
|
|
86
87
|
* Steps in original chronological order with each step labelled by its
|
|
87
88
|
* effective bucket (And/But inherits from the prior explicit keyword).
|
|
@@ -67,7 +67,7 @@ export interface TestGeneratorAdapter {
|
|
|
67
67
|
// Template rendering methods
|
|
68
68
|
renderTestFile(data: TestFileData): string;
|
|
69
69
|
renderScenario(data: ScenarioData): string;
|
|
70
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string;
|
|
70
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean }): string;
|
|
71
71
|
renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
72
72
|
renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
73
73
|
renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
@@ -31,7 +31,7 @@ export class AppiumAdapter implements TestGeneratorAdapter {
|
|
|
31
31
|
return this.templateEngine.renderScenario(data);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean; needsDb?: boolean; needsApi?: boolean }): string {
|
|
34
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean }): string {
|
|
35
35
|
return this.templateEngine.renderImports(options);
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
{{!-- Global key event (no target). On Android, Escape maps to the system Back button. --}}
|
|
2
|
-
{{#if (
|
|
2
|
+
{{#if repeat}}for (let i = 0; i < {{repeat}}; i++) {
|
|
3
|
+
{{#if (eq key 'Escape')}}await driver.back();{{else}}await driver.keys('{{key}}');{{/if}}
|
|
4
|
+
}{{else}}{{#if (eq key 'Escape')}}await driver.back();{{else}}await driver.keys('{{key}}');{{/if}}{{/if}}
|
|
@@ -26,7 +26,7 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
|
|
|
26
26
|
return this.templateEngine.renderScenario(data);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
|
|
29
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean; needsMock?: boolean }): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -9,6 +9,9 @@ import { db } from '{{basePath}}/db';
|
|
|
9
9
|
{{#if needsApi}}
|
|
10
10
|
import { api } from '{{basePath}}/api';
|
|
11
11
|
{{/if}}
|
|
12
|
+
{{#if needsMock}}
|
|
13
|
+
import { mock } from '{{basePath}}/mock';
|
|
14
|
+
{{/if}}
|
|
12
15
|
|
|
13
16
|
// This file is auto-generated from Gherkin feature files
|
|
14
17
|
// DO NOT EDIT MANUALLY - changes will be overwritten
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
{{> url-assertion}}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
const __stops_{{stepCounter}} = [
|
|
3
|
+
{{#each cells}}
|
|
4
|
+
{ label: '{{escapeQuotes label}}', loc: {{> locator}} },
|
|
5
|
+
{{/each}}
|
|
6
|
+
];
|
|
7
|
+
const __focusReport_{{stepCounter}} = () => page.evaluate(() => {
|
|
8
|
+
let el = document.activeElement;
|
|
9
|
+
while (el && el.shadowRoot && el.shadowRoot.activeElement) el = el.shadowRoot.activeElement;
|
|
10
|
+
if (!el || el === document.body) return 'document.body (focus left the page content)';
|
|
11
|
+
const role = el.getAttribute('role') ? `[role=${el.getAttribute('role')}]` : '';
|
|
12
|
+
const name = el.getAttribute('aria-label') || el.getAttribute('name') || el.id || '';
|
|
13
|
+
return `${el.tagName.toLowerCase()}${role}${name ? ` (${name})` : ''}`;
|
|
14
|
+
});
|
|
15
|
+
const __firstLine_{{stepCounter}} = (e: unknown) => (e instanceof Error ? ` (${e.message.split('\n')[0]})` : '');
|
|
16
|
+
await __stops_{{stepCounter}}[0].loc.focus();
|
|
17
|
+
try {
|
|
18
|
+
await expect(__stops_{{stepCounter}}[0].loc, `tab order origin: expected [${__stops_{{stepCounter}}[0].label}] to hold focus`).toBeFocused({ timeout: 2000 });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
throw new Error(`Tab order origin not focused: [${__stops_{{stepCounter}}[0].label}] did not take focus (is it focusable?), actual focus: ${await __focusReport_{{stepCounter}}()}${__firstLine_{{stepCounter}}(err)}`);
|
|
21
|
+
}
|
|
22
|
+
for (let i = 1; i < __stops_{{stepCounter}}.length; i++) {
|
|
23
|
+
await page.keyboard.press('Tab');
|
|
24
|
+
try {
|
|
25
|
+
await expect(__stops_{{stepCounter}}[i].loc, `tab order position ${i + 1}: expected [${__stops_{{stepCounter}}[i].label}]`).toBeFocused({ timeout: 2000 });
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`Tab order mismatch at position ${i + 1}: expected [${__stops_{{stepCounter}}[i].label}], actual focus: ${await __focusReport_{{stepCounter}}()}${__firstLine_{{stepCounter}}(err)}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{{#if dialogRoleName}}page.getByRole('dialog', { name: '{{escapeQuotes dialogRoleName}}' }){{else if dialogFilterText}}page.getByRole('dialog').filter({ hasText: '{{escapeQuotes dialogFilterText}}' }){{else}}page.getByRole('dialog'){{/if}}
|
|
1
|
+
{{#if dialogRootExpr}}{{{dialogRootExpr}}}{{#if dialogFilterText}}.filter({ hasText: '{{escapeQuotes dialogFilterText}}' }){{/if}}{{else if dialogRoleName}}page.getByRole('dialog', { name: '{{escapeQuotes dialogRoleName}}' }){{else if dialogFilterText}}page.getByRole('dialog').filter({ hasText: '{{escapeQuotes dialogFilterText}}' }){{else}}page.getByRole('dialog'){{/if}}
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{{!-- The ONE shape for every Then-side URL assertion (page-assertion + route-assertion).
|
|
2
|
+
Both used to carry their own copy and drifted apart: `see [X] page` wildcarded a dynamic
|
|
3
|
+
`:id` segment but silently dropped `with {{v}}`, while `is on [X] page` asserted the query
|
|
4
|
+
but escaped `:id` literally into a regex that can never match. One partial, one behaviour.
|
|
5
|
+
`pathRegex` is pre-escaped by pathToRegexSource (`:id` → `[^/]+`); the query is asserted
|
|
6
|
+
separately because its value is runtime data full of regex-special characters. --}}
|
|
7
|
+
await expect(page).toHaveURL(/{{pathRegex}}/);
|
|
8
|
+
{{#if query}}
|
|
9
|
+
expect(page.url()).toContain('{{escapeQuotes query}}');
|
|
10
|
+
{{/if}}
|
|
@@ -311,11 +311,12 @@ export class CodeGenerator {
|
|
|
311
311
|
const activeCapabilityIds = this.activeCapabilityIds(feature);
|
|
312
312
|
const needsDb = activeCapabilityIds.includes('db');
|
|
313
313
|
const needsApi = activeCapabilityIds.includes('api');
|
|
314
|
+
const needsMock = activeCapabilityIds.includes('mock');
|
|
314
315
|
for (const id of activeCapabilityIds) {
|
|
315
316
|
for (const h of capabilityRegistry.get(id)?.runtimeHelpers ?? []) this.syncGeneratedHelper(outputDir, h.file, h.template);
|
|
316
317
|
}
|
|
317
318
|
|
|
318
|
-
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi });
|
|
319
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi, needsMock });
|
|
319
320
|
|
|
320
321
|
// Generate test code (async now to support AI mapping)
|
|
321
322
|
const testCode = await this.generateTestCode(feature);
|
|
@@ -324,7 +325,7 @@ export class CodeGenerator {
|
|
|
324
325
|
const raw = `${imports}\n\n${testCode}`;
|
|
325
326
|
let code = raw.replace(/\n{3,}/g, '\n\n');
|
|
326
327
|
|
|
327
|
-
// Runtime data: replace
|
|
328
|
+
// Runtime data: replace __SUNGEN_TD$…$__ markers with testData.get() calls
|
|
328
329
|
if (this.options.runtimeData) {
|
|
329
330
|
code = transformToRuntimeData(code);
|
|
330
331
|
}
|
|
@@ -382,15 +383,40 @@ export class CodeGenerator {
|
|
|
382
383
|
const steps: ParsedStep[] = [];
|
|
383
384
|
if (feature.background?.steps) steps.push(...feature.background.steps);
|
|
384
385
|
for (const sc of feature.scenarios || []) if (sc.steps) steps.push(...sc.steps);
|
|
385
|
-
|
|
386
|
+
// FEATURE-level tags count too: a feature-wide `@mock` must sync the helper and
|
|
387
|
+
// emit the import exactly like a per-scenario tag — reading scenario tags only
|
|
388
|
+
// left patterns matching while the helper/import never arrived, generating a
|
|
389
|
+
// spec that does not compile.
|
|
390
|
+
const tagPool = [
|
|
391
|
+
...(feature.tags || []),
|
|
392
|
+
...(feature.scenarios || []).flatMap((sc) => sc.tags || []),
|
|
393
|
+
];
|
|
386
394
|
const ids = new Set<string>();
|
|
387
395
|
const def = capabilityRegistry.defaultCapabilityId();
|
|
388
396
|
if (def) ids.add(def);
|
|
389
397
|
for (const cap of capabilityRegistry.all()) {
|
|
390
|
-
const annoMatch = (cap.annotations ?? []).some((a) =>
|
|
398
|
+
const annoMatch = (cap.annotations ?? []).some((a) => tagPool.some((t) => t === a || t.startsWith(a + ':')));
|
|
391
399
|
const stepMatch = cap.detectsStep ? steps.some((s) => s && typeof s.text === 'string' && cap.detectsStep!(s.text)) : false;
|
|
392
400
|
if (annoMatch || stepMatch) ids.add(cap.id);
|
|
393
401
|
}
|
|
402
|
+
|
|
403
|
+
// Opt-in driver ABSENT while the feature clearly wants it = hard error. Discovery
|
|
404
|
+
// swallows a missing driver silently and unmatched steps become warn-only TODOs —
|
|
405
|
+
// a "mocked" scenario would then run green against the REAL backend.
|
|
406
|
+
const wantsMock =
|
|
407
|
+
tagPool.some((t) => t === '@mock' || t.startsWith('@mock:') || t === '@network') ||
|
|
408
|
+
steps.some((s) => s && typeof s.text === 'string' && /\[[^\]]+\]\s+mock\s+(?:is\s+active|called)\b/i.test(s.text));
|
|
409
|
+
if (wantsMock && !capabilityRegistry.get('mock')) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
`feature "${feature.name}" uses network mocking (@mock / mock steps) but the mock capability is not installed — run: sungen capability add mock`);
|
|
412
|
+
}
|
|
413
|
+
// Web-only: the runtime rides Playwright's page.route. Failing here beats a
|
|
414
|
+
// confusing "Cannot find name 'mock'" tsc error in the generated appium spec.
|
|
415
|
+
if (wantsMock && this.adapter.frameworkName === 'appium') {
|
|
416
|
+
throw new Error(
|
|
417
|
+
`feature "${feature.name}" uses network mocking (@mock / mock steps), which is web-only — mobile drivers have no request-interception seam. Keep these scenarios @manual on mobile.`);
|
|
418
|
+
}
|
|
419
|
+
|
|
394
420
|
return [...ids];
|
|
395
421
|
}
|
|
396
422
|
|
|
@@ -525,10 +551,33 @@ export class CodeGenerator {
|
|
|
525
551
|
// tab`) is silently dropped and every scenario runs on the launcher. So branch on isParallel only
|
|
526
552
|
// for web; for appium always use generateBackgroundSteps().
|
|
527
553
|
const isMobile = this.adapter.frameworkName === 'appium';
|
|
554
|
+
const mockActive = !isMobile && this.activeCapabilityIds(feature).includes('mock');
|
|
528
555
|
if (isParallel && !isMobile) {
|
|
529
|
-
background = await this.generateBeforeEach(
|
|
556
|
+
background = await this.generateBeforeEach(
|
|
557
|
+
feature.background,
|
|
558
|
+
// The passthrough flag must be armed BEFORE the Background installs + goto —
|
|
559
|
+
// a scenario-body statement runs too late: boot-time requests of an opted-out
|
|
560
|
+
// scenario would be aborted and recorded as violations. beforeEach reads the
|
|
561
|
+
// scenario's own tags via test.info().
|
|
562
|
+
mockActive
|
|
563
|
+
? [{
|
|
564
|
+
comment: '@mock:passthrough scenarios opt out before any mock installs',
|
|
565
|
+
code: this.indentCode("if (test.info().tags.includes('@mock:passthrough')) mock.setPassthrough(page);", 4),
|
|
566
|
+
}]
|
|
567
|
+
: [],
|
|
568
|
+
);
|
|
530
569
|
} else {
|
|
531
570
|
backgroundSteps = await this.generateBackgroundSteps(feature.background);
|
|
571
|
+
// @serial: Background (beforeAll) mocks + the guard must survive each scenario's
|
|
572
|
+
// scoped reset — snapshot them so reset() removes only scenario-added routes.
|
|
573
|
+
// Without this, scenario 1's reset kills the Background mocks and scenarios 2+
|
|
574
|
+
// run against the REAL backend with no guard.
|
|
575
|
+
if (mockActive && !isMobile) {
|
|
576
|
+
backgroundSteps.push({
|
|
577
|
+
comment: 'Background mocks persist across the @serial scenarios',
|
|
578
|
+
code: this.indentCode('await mock.snapshotBackground(page);', 4),
|
|
579
|
+
});
|
|
580
|
+
}
|
|
532
581
|
}
|
|
533
582
|
}
|
|
534
583
|
|
|
@@ -736,9 +785,12 @@ export class CodeGenerator {
|
|
|
736
785
|
return steps;
|
|
737
786
|
}
|
|
738
787
|
|
|
739
|
-
private async generateBeforeEach(
|
|
788
|
+
private async generateBeforeEach(
|
|
789
|
+
background: ParsedScenario,
|
|
790
|
+
preambleSteps: Array<{ comment?: string; code: string }> = [],
|
|
791
|
+
): Promise<string> {
|
|
740
792
|
// Map all steps
|
|
741
|
-
const steps: Array<{ comment?: string; code: string }> = [];
|
|
793
|
+
const steps: Array<{ comment?: string; code: string }> = [...preambleSteps];
|
|
742
794
|
for (const step of background.steps) {
|
|
743
795
|
const mapped = await Promise.resolve(this.stepMapper.mapStep(step));
|
|
744
796
|
steps.push({
|
|
@@ -885,6 +937,29 @@ export class CodeGenerator {
|
|
|
885
937
|
steps.unshift(...preconditions.map((p) => ({ comment: p.comment, code: this.indentCode(p.code, 4) })));
|
|
886
938
|
}
|
|
887
939
|
|
|
940
|
+
// Mock scenario epilogue. (a) EVERY path: the fail-on-unmocked verdict — the guard
|
|
941
|
+
// records+aborts unmatched requests (it must not throw inside a route handler:
|
|
942
|
+
// that surfaces as an unhandled rejection), so the scenario ends by asserting the
|
|
943
|
+
// violation list is empty. (b) @serial shared page only: a scoped reset so this
|
|
944
|
+
// scenario's routes/counters don't leak into the next (the router unroutes only
|
|
945
|
+
// what IT installed — never page.unrouteAll(), which would also kill Background
|
|
946
|
+
// mocks and third-party routes; the per-test path gets that free at page close).
|
|
947
|
+
const usesMock =
|
|
948
|
+
[...(featureTags || []), ...(scenario.tags || [])].some((t) => t === '@mock' || t.startsWith('@mock:') || t === '@network') ||
|
|
949
|
+
stepsToMap.some((s) => /\[[^\]]+\]\s+mock\s+is\s+active\b/i.test(s.text || ''));
|
|
950
|
+
if (usesMock && this.adapter.frameworkName !== 'appium' && capabilityRegistry.get('mock')) {
|
|
951
|
+
steps.push({
|
|
952
|
+
comment: 'Fail-on-unmocked verdict: any unmatched request aborted by the guard fails here',
|
|
953
|
+
code: this.indentCode('await mock.assertNoUnmocked(page);', 4),
|
|
954
|
+
});
|
|
955
|
+
if (!isParallel) {
|
|
956
|
+
steps.push({
|
|
957
|
+
comment: 'Reset mock routes and counters (shared @serial page)',
|
|
958
|
+
code: this.indentCode('await mock.reset(page);', 4),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
888
963
|
// Extract pass-through tags (feature + scenario, excluding functional tags)
|
|
889
964
|
const tags = extractPassThroughTags(scenario.tags, featureTags);
|
|
890
965
|
|