@sun-asterisk/sungen 2.5.2 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/dist/cli/commands/add-flow.d.ts +3 -0
  2. package/dist/cli/commands/add-flow.d.ts.map +1 -0
  3. package/dist/cli/commands/add-flow.js +27 -0
  4. package/dist/cli/commands/add-flow.js.map +1 -0
  5. package/dist/cli/commands/delivery.d.ts.map +1 -1
  6. package/dist/cli/commands/delivery.js +95 -60
  7. package/dist/cli/commands/delivery.js.map +1 -1
  8. package/dist/cli/commands/generate.d.ts.map +1 -1
  9. package/dist/cli/commands/generate.js +38 -6
  10. package/dist/cli/commands/generate.js.map +1 -1
  11. package/dist/cli/index.js +3 -1
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +16 -0
  14. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  15. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +3 -0
  16. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  17. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  18. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -2
  19. package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +20 -1
  20. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  21. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  22. package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  23. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +2 -2
  24. package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +1 -0
  25. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +2 -1
  26. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -2
  27. package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  28. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +84 -4
  29. package/dist/generators/test-generator/code-generator.d.ts +2 -0
  30. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  31. package/dist/generators/test-generator/code-generator.js +105 -17
  32. package/dist/generators/test-generator/code-generator.js.map +1 -1
  33. package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +1 -1
  34. package/dist/generators/test-generator/patterns/interaction-patterns.js +22 -3
  35. package/dist/generators/test-generator/patterns/interaction-patterns.js.map +1 -1
  36. package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +1 -1
  37. package/dist/generators/test-generator/patterns/navigation-patterns.js +8 -3
  38. package/dist/generators/test-generator/patterns/navigation-patterns.js.map +1 -1
  39. package/dist/generators/test-generator/step-mapper.d.ts +4 -0
  40. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  41. package/dist/generators/test-generator/step-mapper.js +7 -0
  42. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  43. package/dist/generators/test-generator/template-engine.d.ts +14 -0
  44. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  45. package/dist/generators/test-generator/template-engine.js +1 -1
  46. package/dist/generators/test-generator/template-engine.js.map +1 -1
  47. package/dist/generators/test-generator/utils/data-resolver.d.ts +3 -20
  48. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  49. package/dist/generators/test-generator/utils/data-resolver.js +23 -66
  50. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  51. package/dist/generators/test-generator/utils/selector-resolver.d.ts +2 -6
  52. package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
  53. package/dist/generators/test-generator/utils/selector-resolver.js +18 -80
  54. package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
  55. package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
  56. package/dist/orchestrator/ai-rules-updater.js +4 -0
  57. package/dist/orchestrator/ai-rules-updater.js.map +1 -1
  58. package/dist/orchestrator/flow-manager.d.ts +22 -0
  59. package/dist/orchestrator/flow-manager.d.ts.map +1 -0
  60. package/dist/orchestrator/flow-manager.js +251 -0
  61. package/dist/orchestrator/flow-manager.js.map +1 -0
  62. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  63. package/dist/orchestrator/project-initializer.js +1 -0
  64. package/dist/orchestrator/project-initializer.js.map +1 -1
  65. package/dist/orchestrator/screen-manager.d.ts.map +1 -1
  66. package/dist/orchestrator/screen-manager.js +3 -1
  67. package/dist/orchestrator/screen-manager.js.map +1 -1
  68. package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +88 -0
  69. package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -8
  70. package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -6
  71. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +15 -11
  72. package/dist/orchestrator/templates/ai-instructions/claude-config.md +41 -10
  73. package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +12 -0
  74. package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +19 -18
  75. package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +12 -0
  76. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +122 -10
  77. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +31 -3
  78. package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +45 -0
  79. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +92 -0
  80. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +30 -0
  81. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +86 -0
  82. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +13 -10
  83. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +16 -15
  84. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
  85. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +21 -17
  86. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +40 -9
  87. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +12 -0
  88. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +19 -18
  89. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +12 -0
  90. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +122 -10
  91. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +31 -3
  92. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +45 -0
  93. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +93 -0
  94. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +30 -0
  95. package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
  96. package/dist/orchestrator/templates/playwright.config.js +3 -1
  97. package/dist/orchestrator/templates/playwright.config.js.map +1 -1
  98. package/dist/orchestrator/templates/playwright.config.ts +4 -1
  99. package/dist/orchestrator/templates/specs-base.d.ts +3 -4
  100. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  101. package/dist/orchestrator/templates/specs-base.js +60 -91
  102. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  103. package/dist/orchestrator/templates/specs-base.ts +61 -101
  104. package/dist/orchestrator/templates/specs-test-data.d.ts +3 -1
  105. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -1
  106. package/dist/orchestrator/templates/specs-test-data.js +53 -2
  107. package/dist/orchestrator/templates/specs-test-data.js.map +1 -1
  108. package/dist/orchestrator/templates/specs-test-data.ts +56 -2
  109. package/package.json +1 -1
  110. package/src/cli/commands/add-flow.ts +25 -0
  111. package/src/cli/commands/delivery.ts +109 -58
  112. package/src/cli/commands/generate.ts +43 -6
  113. package/src/cli/index.ts +3 -1
  114. package/src/generators/test-generator/adapters/adapter-interface.ts +6 -1
  115. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
  116. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -2
  117. package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +20 -1
  118. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
  119. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
  120. package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
  121. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +2 -2
  122. package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +1 -0
  123. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +2 -1
  124. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -2
  125. package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
  126. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +84 -4
  127. package/src/generators/test-generator/code-generator.ts +119 -20
  128. package/src/generators/test-generator/patterns/interaction-patterns.ts +25 -3
  129. package/src/generators/test-generator/patterns/navigation-patterns.ts +8 -3
  130. package/src/generators/test-generator/step-mapper.ts +8 -0
  131. package/src/generators/test-generator/template-engine.ts +5 -2
  132. package/src/generators/test-generator/utils/data-resolver.ts +25 -77
  133. package/src/generators/test-generator/utils/selector-resolver.ts +23 -109
  134. package/src/orchestrator/ai-rules-updater.ts +5 -0
  135. package/src/orchestrator/flow-manager.ts +243 -0
  136. package/src/orchestrator/project-initializer.ts +1 -0
  137. package/src/orchestrator/screen-manager.ts +3 -1
  138. package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +88 -0
  139. package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -8
  140. package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -6
  141. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +15 -11
  142. package/src/orchestrator/templates/ai-instructions/claude-config.md +41 -10
  143. package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +12 -0
  144. package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +19 -18
  145. package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +12 -0
  146. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +122 -10
  147. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +31 -3
  148. package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +45 -0
  149. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +92 -0
  150. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +30 -0
  151. package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +86 -0
  152. package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +13 -10
  153. package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +16 -15
  154. package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
  155. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +21 -17
  156. package/src/orchestrator/templates/ai-instructions/copilot-config.md +40 -9
  157. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +12 -0
  158. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +19 -18
  159. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +12 -0
  160. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +122 -10
  161. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +31 -3
  162. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +45 -0
  163. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +93 -0
  164. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +30 -0
  165. package/src/orchestrator/templates/playwright.config.ts +4 -1
  166. package/src/orchestrator/templates/specs-base.ts +61 -101
  167. package/src/orchestrator/templates/specs-test-data.ts +56 -2
  168. package/dist/utils/feature-finder.d.ts +0 -9
  169. package/dist/utils/feature-finder.d.ts.map +0 -1
  170. package/dist/utils/feature-finder.js +0 -67
  171. package/dist/utils/feature-finder.js.map +0 -1
  172. package/dist/utils/screen-paths.d.ts +0 -10
  173. package/dist/utils/screen-paths.d.ts.map +0 -1
  174. package/dist/utils/screen-paths.js +0 -73
  175. package/dist/utils/screen-paths.js.map +0 -1
  176. package/dist/utils/selector-loader.d.ts +0 -6
  177. package/dist/utils/selector-loader.d.ts.map +0 -1
  178. package/dist/utils/selector-loader.js +0 -20
  179. package/dist/utils/selector-loader.js.map +0 -1
  180. package/dist/utils/test-data-loader.d.ts +0 -6
  181. package/dist/utils/test-data-loader.d.ts.map +0 -1
  182. package/dist/utils/test-data-loader.js +0 -20
  183. package/dist/utils/test-data-loader.js.map +0 -1
  184. package/src/utils/feature-finder.ts +0 -33
  185. package/src/utils/screen-paths.ts +0 -37
  186. package/src/utils/selector-loader.ts +0 -23
  187. package/src/utils/test-data-loader.ts +0 -23
@@ -21,6 +21,9 @@ export interface TestFileData {
21
21
  runtimeData?: boolean; // --runtime-data flag: testData.get() instead of hardcoded values
22
22
  screenName?: string; // Screen name for TestDataLoader.load()
23
23
  featureFileName?: string; // Feature file name for TestDataLoader.load()
24
+ isParallel?: boolean; // @parallel tag: fresh page per test (opt-out from serial default)
25
+ cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
26
+ backgroundSteps?: Array<{ comment?: string; code: string }>; // Raw background steps for serial beforeAll
24
27
  scenarios: string[];
25
28
  authGroups?: AuthGroup[]; // Grouped by auth role for nested describes
26
29
  singleAuthRole?: string; // Auth role when all scenarios share the same role
@@ -30,6 +33,8 @@ export interface ScenarioData {
30
33
  scenarioName: string;
31
34
  steps: Array<{ comment?: string; code: string }>;
32
35
  authRole?: string; // Auth role for storage state
36
+ isParallel?: boolean; // @parallel: use fresh page from fixture
37
+ tags?: string; // Pass-through tags for Playwright { tag: [...] }, e.g. "'@smoke', '@critical'"
33
38
  }
34
39
 
35
40
  export interface StepTemplateData {
@@ -57,7 +62,7 @@ export interface TestGeneratorAdapter {
57
62
  // Template rendering methods
58
63
  renderTestFile(data: TestFileData): string;
59
64
  renderScenario(data: ScenarioData): string;
60
- renderImports(options?: { runtimeData?: boolean }): string;
65
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string;
61
66
  renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
62
67
  renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
63
68
  renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
@@ -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 }): string {
29
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string {
30
30
  return this.templateEngine.renderImports(options);
31
31
  }
32
32
 
@@ -1,6 +1,7 @@
1
- import { test, expect } from '../base';
1
+ import { test, expect{{#if needsCleanupImport}}, cleanupPage{{/if}} } from '{{basePath}}/base';
2
+ import { type Page, type BrowserContext } from '@playwright/test';
2
3
  {{#if runtimeData}}
3
- import { TestDataLoader } from '../test-data';
4
+ import { TestDataLoader } from '{{basePath}}/test-data';
4
5
  {{/if}}
5
6
 
6
7
  // This file is auto-generated from Gherkin feature files
@@ -1,8 +1,27 @@
1
+ {{#if isParallel}}
2
+ {{#if tags}}
3
+ test('{{scenarioName}}', { tag: [{{{tags}}}] }, async ({ page }) => {
4
+ {{else}}
1
5
  test('{{scenarioName}}', async ({ page }) => {
6
+ {{/if}}
7
+ {{#each steps}}
8
+ {{#if comment}}
9
+ // {{comment}}
10
+ {{/if}}
11
+ {{code}}
12
+ {{/each}}
13
+ });
14
+ {{else}}
15
+ {{#if tags}}
16
+ test('{{scenarioName}}', { tag: [{{{tags}}}] }, async () => {
17
+ {{else}}
18
+ test('{{scenarioName}}', async () => {
19
+ {{/if}}
2
20
  {{#each steps}}
3
21
  {{#if comment}}
4
22
  // {{comment}}
5
23
  {{/if}}
6
24
  {{code}}
7
25
  {{/each}}
8
- });
26
+ });
27
+ {{/if}}
@@ -1 +1 @@
1
- page.once('dialog', dialog => dialog.accept());
1
+ page.on('dialog', dialog => dialog.accept());
@@ -1 +1 @@
1
- page.once('dialog', dialog => dialog.dismiss());
1
+ page.on('dialog', dialog => dialog.dismiss());
@@ -1 +1 @@
1
- page.once('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
1
+ page.on('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
@@ -1,8 +1,8 @@
1
1
  const dialogPromise{{stepCounter}} = new Promise<string>(resolve => {
2
- page.once('dialog', dialog => {
2
+ page.on('dialog', dialog => {
3
3
  resolve(dialog.message());
4
4
  dialog.accept();
5
5
  });
6
6
  });
7
7
  // Trigger the dialog action, then:
8
- // expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
8
+ // expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
@@ -1,2 +1,3 @@
1
+ await expect({{> locator}}.getByRole('columnheader', { name: '{{columnName}}' })).toBeVisible();
1
2
  const {{columnIndexVar}} = (await page.getByRole('columnheader').allTextContents()).findIndex(h => h.includes('{{columnName}}'));
2
3
  await expect(page.getByRole('row').nth({{rowNth}}).getByRole('cell').nth({{columnIndexVar}})).toHaveText('{{dataValue}}');
@@ -1 +1,2 @@
1
- await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
1
+ await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
2
+ await page.waitForLoadState('domcontentloaded');
@@ -1,2 +1 @@
1
- await page.waitForURL('**{{path}}', { timeout: 5000 });
2
- expect(page.url()).toContain('{{path}}');
1
+ await expect(page).toHaveURL(/{{escapeRegex path}}/);
@@ -1 +1 @@
1
- await page.waitForTimeout({{duration}});
1
+ await page.waitForLoadState('networkidle');
@@ -11,14 +11,11 @@ const testData = TestDataLoader.load('{{screenName}}', '{{featureFileName}}');
11
11
  */
12
12
  {{/if}}
13
13
 
14
+ {{#if isParallel}}
14
15
  test.describe('{{featureName}}', () => {
15
16
  {{#if singleAuthRole}}
16
17
  test.use({ storageState: 'specs/.auth/{{singleAuthRole}}.json' });
17
18
 
18
- {{/if}}
19
- {{#if cleanupConfig}}
20
- test.use({ autoCleanup: { {{cleanupConfig}} } });
21
-
22
19
  {{/if}}
23
20
  {{#if screenshotOnFailure}}
24
21
  test.use({ screenshotOnFailure: true });
@@ -66,3 +63,86 @@ test.describe('{{featureName}}', () => {
66
63
  {{/each}}
67
64
  {{/if}}
68
65
  });
66
+ {{else}}
67
+ test.describe.serial('{{featureName}}', () => {
68
+ let page: Page;
69
+ let context: BrowserContext;
70
+
71
+ {{#if singleAuthRole}}
72
+ test.beforeAll(async ({ browser }) => {
73
+ context = await browser.newContext({ storageState: 'specs/.auth/{{singleAuthRole}}.json' });
74
+ page = await context.newPage();
75
+ {{#each backgroundSteps}}
76
+ {{#if comment}}
77
+ // {{comment}}
78
+ {{/if}}
79
+ {{code}}
80
+ {{/each}}
81
+ });
82
+ {{else}}
83
+ test.beforeAll(async ({ browser }) => {
84
+ context = await browser.newContext();
85
+ page = await context.newPage();
86
+ {{#each backgroundSteps}}
87
+ {{#if comment}}
88
+ // {{comment}}
89
+ {{/if}}
90
+ {{code}}
91
+ {{/each}}
92
+ });
93
+ {{/if}}
94
+
95
+ test.afterAll(async () => {
96
+ await page.close();
97
+ await context.close();
98
+ });
99
+
100
+ {{#if cleanup}}
101
+ test.afterEach(async () => {
102
+ await cleanupPage(page, { {{cleanupConfig}} });
103
+ });
104
+
105
+ {{/if}}
106
+ {{#if screenshotOnFailure}}
107
+ test.use({ screenshotOnFailure: true });
108
+
109
+ {{/if}}
110
+ {{#if beforeAll}}
111
+ {{beforeAll}}
112
+
113
+ {{/if}}
114
+ {{#if afterEach}}
115
+ {{afterEach}}
116
+
117
+ {{/if}}
118
+ {{#if afterAll}}
119
+ {{afterAll}}
120
+
121
+ {{/if}}
122
+ {{#if authGroups}}
123
+ {{#each authGroups}}
124
+ {{#if authRole}}
125
+ test.describe('{{authRole}}', () => {
126
+ test.use({ storageState: 'specs/.auth/{{authRole}}.json' });
127
+
128
+ {{#each scenarios}}
129
+ {{indent this 2}}
130
+
131
+ {{/each}}
132
+ });
133
+
134
+ {{else}}
135
+ {{#each scenarios}}
136
+ {{this}}
137
+
138
+ {{/each}}
139
+ {{/if}}
140
+ {{/each}}
141
+ {{else}}
142
+ {{#each scenarios}}
143
+ {{this}}
144
+
145
+ {{/each}}
146
+ {{/if}}
147
+ });
148
+ {{/if}}
@@ -48,6 +48,44 @@ function extractCleanupConfig(tags: string[]): string | undefined {
48
48
  return entries.map(key => `${key}: true`).join(', ');
49
49
  }
50
50
 
51
+ /**
52
+ * Extract @cleanup:* tags into structured flags for serial mode afterEach
53
+ */
54
+ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean } | undefined {
55
+ const validKeys = ['overlay', 'forms', 'scroll', 'storage'] as const;
56
+ const flags: Record<string, boolean> = {};
57
+ let found = false;
58
+ for (const tag of tags) {
59
+ if (!tag.startsWith('@cleanup:')) continue;
60
+ const key = tag.replace('@cleanup:', '');
61
+ if (validKeys.includes(key as any)) {
62
+ flags[key] = true;
63
+ found = true;
64
+ }
65
+ }
66
+ return found ? flags : undefined;
67
+ }
68
+
69
+ /**
70
+ * Extract pass-through tags (non-functional) for Playwright { tag: [...] }.
71
+ * Any tag not recognized by sungen as functional → pass through.
72
+ */
73
+ const FUNCTIONAL_TAG_PREFIXES = [
74
+ '@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
75
+ '@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
76
+ '@flow',
77
+ ];
78
+
79
+ function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
80
+ const allTags = [...featureTags, ...scenarioTags];
81
+ const passThrough = allTags.filter(tag =>
82
+ !FUNCTIONAL_TAG_PREFIXES.some(prefix => tag.startsWith(prefix))
83
+ );
84
+ const unique = [...new Set(passThrough)];
85
+ if (unique.length === 0) return undefined;
86
+ return unique.map(t => `'${t}'`).join(', ');
87
+ }
88
+
51
89
  /**
52
90
  * Check for @screenshot:on-failure tag
53
91
  */
@@ -134,7 +172,7 @@ export class CodeGenerator {
134
172
  // Steps registry built per feature during generateTestCode(); used by countSteps()
135
173
  private stepsRegistry = new Map<string, ParsedScenario>();
136
174
 
137
- constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean } = {}) {
175
+ constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean; flowMode?: boolean } = {}) {
138
176
  this.options = options;
139
177
  this.screenName = options.screenName;
140
178
  this.stepMapper = new StepMapper(options);
@@ -165,25 +203,37 @@ export class CodeGenerator {
165
203
  fileName = this.featureNameToFileName(feature.name);
166
204
  }
167
205
 
168
- // Extract screen name from source file path if available
206
+ // Extract screen/flow name from source file path for output subdirectory
169
207
  // qa/screens/{screenName}/features/{featureName}.feature -> screenName
170
- let screenSubdir = '';
208
+ // qa/flows/{flowName}/features/{featureName}.feature -> flows/flowName
209
+ let outputSubdir = '';
171
210
  if (feature.sourceFile) {
172
211
  const sourceDir = path.dirname(feature.sourceFile);
173
212
  const parts = sourceDir.split(path.sep);
213
+ const flowsIndex = parts.indexOf('flows');
174
214
  const screensIndex = parts.indexOf('screens');
175
- if (screensIndex >= 0 && screensIndex < parts.length - 2) {
176
- screenSubdir = parts[screensIndex + 1]; // screen name after 'screens'
215
+ if (flowsIndex >= 0 && flowsIndex < parts.length - 2) {
216
+ outputSubdir = path.join('flows', parts[flowsIndex + 1]);
217
+ } else if (screensIndex >= 0 && screensIndex < parts.length - 2) {
218
+ outputSubdir = parts[screensIndex + 1];
177
219
  }
178
220
  }
179
-
180
- // Build output path with screen subdirectory
181
- const filePath = screenSubdir
182
- ? path.join(outputDir, screenSubdir, fileName)
221
+
222
+ // Build output path with subdirectory
223
+ const filePath = outputSubdir
224
+ ? path.join(outputDir, outputSubdir, fileName)
183
225
  : path.join(outputDir, fileName);
184
226
 
185
- // Generate imports using adapter
186
- const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData });
227
+ // Compute relative path from output file back to specs/generated/
228
+ const depth = outputSubdir ? outputSubdir.split(path.sep).length : 0;
229
+ const basePath = depth > 0 ? Array(depth).fill('..').join('/') : '..';
230
+
231
+ // Serial + @cleanup tags → need cleanupPage import from base
232
+ const isParallelFeature = (feature.tags || []).includes('@parallel');
233
+ const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
234
+ const needsCleanupImport = !isParallelFeature && hasCleanupTags;
235
+
236
+ const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport });
187
237
 
188
238
  // Generate test code (async now to support AI mapping)
189
239
  const testCode = await this.generateTestCode(feature);
@@ -278,19 +328,29 @@ export class CodeGenerator {
278
328
  featureName = this.featureNameToFileName(feature.name).replace('.spec.ts', '');
279
329
  }
280
330
 
281
- // Derive screen name from source file path when not explicitly set
331
+ // Derive screen/flow name from source file path when not explicitly set
282
332
  // qa/screens/{screenName}/features/{featureName}.feature -> screenName
333
+ // qa/flows/{flowName}/features/{featureName}.feature -> flowName
283
334
  let effectiveScreenName = this.screenName;
335
+ let isFlowFeature = !!this.options.flowMode;
284
336
  if (!this.screenName && feature.sourceFile) {
285
337
  const sourceDir = path.dirname(feature.sourceFile);
286
338
  const parts = sourceDir.split(path.sep);
339
+ const flowsIndex = parts.indexOf('flows');
287
340
  const screensIndex = parts.indexOf('screens');
288
- if (screensIndex >= 0 && screensIndex < parts.length - 2) {
341
+ if (flowsIndex >= 0 && flowsIndex < parts.length - 2) {
342
+ effectiveScreenName = parts[flowsIndex + 1];
343
+ isFlowFeature = true;
344
+ } else if (screensIndex >= 0 && screensIndex < parts.length - 2) {
289
345
  effectiveScreenName = parts[screensIndex + 1];
290
- this.stepMapper.setScreenContext(effectiveScreenName);
346
+ isFlowFeature = false;
291
347
  }
348
+ this.stepMapper.setScreenContext(effectiveScreenName);
292
349
  }
293
350
 
351
+ // Reset flow mode per feature to prevent state leak in --all mode
352
+ this.stepMapper.setFlowMode(isFlowFeature);
353
+
294
354
  // Set feature context for data resolution and navigation
295
355
  this.stepMapper.setFeatureContext(featureName, feature.path);
296
356
 
@@ -322,10 +382,18 @@ export class CodeGenerator {
322
382
  }
323
383
  }
324
384
 
385
+ // Detect @parallel opt-out (default is serial)
386
+ const isParallel = (feature.tags || []).includes('@parallel');
387
+
325
388
  // Generate background if exists
326
389
  let background: string | undefined;
390
+ let backgroundSteps: Array<{ comment?: string; code: string }> | undefined;
327
391
  if (feature.background) {
328
- background = await this.generateBeforeEach(feature.background);
392
+ if (isParallel) {
393
+ background = await this.generateBeforeEach(feature.background);
394
+ } else {
395
+ backgroundSteps = await this.generateBackgroundSteps(feature.background);
396
+ }
329
397
  }
330
398
 
331
399
  // Generate hook blocks
@@ -369,7 +437,8 @@ export class CodeGenerator {
369
437
  const code = await this.generateScenario(
370
438
  scenario,
371
439
  !!feature.background,
372
- feature.tags || []
440
+ feature.tags || [],
441
+ isParallel
373
442
  );
374
443
  renderedScenarios.push({ code, authRole });
375
444
  }
@@ -396,6 +465,14 @@ export class CodeGenerator {
396
465
  // - Single group: flat structure (test.use at describe level if auth)
397
466
  // - Multiple groups: nested describes per auth role
398
467
  const needsGrouping = authGroups.length > 1;
468
+
469
+ if (needsGrouping && !isParallel) {
470
+ throw new Error(
471
+ `Feature "${feature.name}" has multiple auth groups but no @parallel tag.\n` +
472
+ `Serial mode uses a shared browser context — it cannot mix different auth roles.\n` +
473
+ `Fix: add @parallel tag to the feature.`
474
+ );
475
+ }
399
476
  const scenarios = renderedScenarios.map(s => s.code);
400
477
 
401
478
  // For single group, extract the auth role to put test.use at describe level
@@ -405,6 +482,7 @@ export class CodeGenerator {
405
482
 
406
483
  // Extract @cleanup:* tags for autoCleanup fixture config
407
484
  const cleanupConfig = extractCleanupConfig(feature.tags || []);
485
+ const cleanup = extractCleanupFlags(feature.tags || []);
408
486
  const screenshotOnFailure = hasScreenshotOnFailure(feature.tags || []);
409
487
 
410
488
  // Use adapter to render the complete test file structure
@@ -419,14 +497,29 @@ export class CodeGenerator {
419
497
  cleanupConfig,
420
498
  screenshotOnFailure,
421
499
  runtimeData: this.options.runtimeData,
422
- screenName: effectiveScreenName,
500
+ screenName: isFlowFeature ? `flows/${effectiveScreenName}` : effectiveScreenName,
423
501
  featureFileName: featureName,
502
+ isParallel,
503
+ cleanup,
504
+ backgroundSteps,
424
505
  scenarios: needsGrouping ? [] : scenarios,
425
506
  authGroups: needsGrouping ? authGroups : undefined,
426
507
  singleAuthRole,
427
508
  });
428
509
  }
429
510
 
511
+ private async generateBackgroundSteps(background: ParsedScenario): Promise<Array<{ comment?: string; code: string }>> {
512
+ const steps: Array<{ comment?: string; code: string }> = [];
513
+ for (const step of background.steps) {
514
+ const mapped = await Promise.resolve(this.stepMapper.mapStep(step));
515
+ steps.push({
516
+ comment: mapped.comment,
517
+ code: this.indentCode(mapped.code, 4),
518
+ });
519
+ }
520
+ return steps;
521
+ }
522
+
430
523
  private async generateBeforeEach(background: ParsedScenario): Promise<string> {
431
524
  // Map all steps
432
525
  const steps: Array<{ comment?: string; code: string }> = [];
@@ -466,9 +559,10 @@ export class CodeGenerator {
466
559
  }
467
560
 
468
561
  private async generateScenario(
469
- scenario: ParsedScenario,
562
+ scenario: ParsedScenario,
470
563
  hasBackground: boolean,
471
- featureTags: string[] = []
564
+ featureTags: string[] = [],
565
+ isParallel: boolean = false
472
566
  ): Promise<string> {
473
567
  // Resolve base steps and tags for @extend scenarios
474
568
  let stepsToMap = scenario.steps;
@@ -527,11 +621,16 @@ export class CodeGenerator {
527
621
  }
528
622
  }
529
623
 
624
+ // Extract pass-through tags (feature + scenario, excluding functional tags)
625
+ const tags = extractPassThroughTags(scenario.tags, featureTags);
626
+
530
627
  // Use adapter to render scenario
531
628
  return this.adapter.renderScenario({
532
629
  scenarioName: scenario.name,
533
630
  steps,
534
- authRole
631
+ authRole,
632
+ isParallel,
633
+ tags,
535
634
  });
536
635
  }
537
636
 
@@ -296,12 +296,34 @@ export const interactionPatterns: StepPattern[] = [
296
296
  matcher: (step: ParsedStep) =>
297
297
  (step.text.includes('wait for') || step.text.includes('waits for')) && step.elementType === 'page',
298
298
  resolver: (step, context) => {
299
- const path = step.featurePath || '/';
300
- const pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
299
+ let path = step.featurePath || '/';
300
+
301
+ if (step.selectorRef) {
302
+ try {
303
+ const resolved = context.selectorResolver.resolveSelector(
304
+ step.selectorRef, context.featureName, step.elementType, step.nth
305
+ );
306
+ path = resolved.value || path;
307
+ } catch (error) {
308
+ // fallback to featurePath
309
+ }
310
+ }
311
+
312
+ const isAbsoluteUrl = /^https?:\/\//.test(path);
313
+ let pathRegex: string;
314
+ if (isAbsoluteUrl) {
315
+ const url = new URL(path);
316
+ const hostEscaped = url.hostname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
317
+ const pathEscaped = url.pathname !== '/' ? url.pathname.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&') : '';
318
+ pathRegex = hostEscaped + pathEscaped;
319
+ } else {
320
+ pathRegex = path.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
321
+ }
322
+
301
323
  return {
302
324
  templateName: 'wait-for-page',
303
325
  data: { pathRegex },
304
- comment: `Wait for page`,
326
+ comment: step.selectorRef ? `Wait for ${step.selectorRef} page` : `Wait for page`,
305
327
  };
306
328
  },
307
329
  priority: 9,
@@ -26,10 +26,11 @@ export const navigationPatterns: StepPattern[] = [
26
26
  }
27
27
 
28
28
  const finalPath = resolvePathVariables(path, context.scenarioSteps || []);
29
+ const isAbsoluteUrl = /^https?:\/\//.test(finalPath);
29
30
 
30
31
  return {
31
32
  templateName: 'navigation',
32
- data: { baseURL: context.baseURL, path: finalPath },
33
+ data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: finalPath },
33
34
  comment: step.selectorRef ? `Open ${step.selectorRef} page` : `Navigate to page`,
34
35
  };
35
36
  },
@@ -52,10 +53,12 @@ export const navigationPatterns: StepPattern[] = [
52
53
  });
53
54
  const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
54
55
  const pathCode = getPathCode(resolvedPath);
56
+ const cleanPath = pathCode.replace(/^['`]|['`]$/g, '');
57
+ const isAbsoluteUrl = /^https?:\/\//.test(cleanPath);
55
58
 
56
59
  return {
57
60
  templateName: 'navigation',
58
- data: { baseURL: context.baseURL, path: pathCode.replace(/^['`]|['`]$/g, '') },
61
+ data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: cleanPath },
59
62
  comment: `Open ${pageName}`,
60
63
  };
61
64
  },
@@ -75,10 +78,12 @@ export const navigationPatterns: StepPattern[] = [
75
78
  });
76
79
  const resolvedPath = resolvePathVariables(inferredPath, context.scenarioSteps || []);
77
80
  const pathCode = getPathCode(resolvedPath);
81
+ const cleanPath = pathCode.replace(/^['`]|['`]$/g, '');
82
+ const isAbsoluteUrl = /^https?:\/\//.test(cleanPath);
78
83
 
79
84
  return {
80
85
  templateName: 'navigation',
81
- data: { baseURL: context.baseURL, path: pathCode.replace(/^['`]|['`]$/g, '') },
86
+ data: { baseURL: isAbsoluteUrl ? undefined : context.baseURL, path: cleanPath },
82
87
  comment: `Navigate to ${route}`,
83
88
  };
84
89
  },
@@ -71,6 +71,14 @@ export class StepMapper {
71
71
  this.dataResolver.setScreenContext(screenName);
72
72
  }
73
73
 
74
+ /**
75
+ * Enable flow mode — colon in selector refs is a namespace separator, not cross-screen
76
+ */
77
+ setFlowMode(enabled: boolean): void {
78
+ this.selectorResolver.setFlowMode(enabled);
79
+ this.dataResolver.setFlowMode(enabled);
80
+ }
81
+
74
82
  /**
75
83
  * Set scenario context for path variable resolution
76
84
  */
@@ -229,8 +229,8 @@ export class TemplateEngine {
229
229
  this.baseContext = {};
230
230
  }
231
231
 
232
- renderImports(options?: { runtimeData?: boolean }): string {
233
- return this.render('imports', { runtimeData: options?.runtimeData });
232
+ renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean }): string {
233
+ return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport });
234
234
  }
235
235
 
236
236
  renderTestFile(data: {
@@ -246,6 +246,9 @@ export class TemplateEngine {
246
246
  runtimeData?: boolean;
247
247
  screenName?: string;
248
248
  featureFileName?: string;
249
+ isParallel?: boolean;
250
+ cleanup?: { overlay?: boolean; forms?: boolean; scroll?: boolean; storage?: boolean };
251
+ backgroundSteps?: Array<{ comment?: string; code: string }>;
249
252
  scenarios: string[];
250
253
  authGroups?: Array<{ authRole?: string; scenarios: string[] }>;
251
254
  singleAuthRole?: string;