@sun-asterisk/sungen 2.4.6 → 2.5.0

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 (92) hide show
  1. package/dist/cli/commands/generate.d.ts.map +1 -1
  2. package/dist/cli/commands/generate.js +2 -0
  3. package/dist/cli/commands/generate.js.map +1 -1
  4. package/dist/cli/index.js +1 -1
  5. package/dist/generators/gherkin-parser/index.d.ts +1 -0
  6. package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
  7. package/dist/generators/gherkin-parser/index.js +3 -0
  8. package/dist/generators/gherkin-parser/index.js.map +1 -1
  9. package/dist/generators/test-generator/adapters/adapter-interface.d.ts +29 -1
  10. package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
  11. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +21 -1
  12. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
  13. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +11 -2
  14. package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
  15. package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  16. package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  17. package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  18. package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  19. package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  20. package/dist/generators/test-generator/code-generator.d.ts +2 -0
  21. package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
  22. package/dist/generators/test-generator/code-generator.js +109 -12
  23. package/dist/generators/test-generator/code-generator.js.map +1 -1
  24. package/dist/generators/test-generator/step-mapper.d.ts +1 -0
  25. package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
  26. package/dist/generators/test-generator/step-mapper.js +1 -1
  27. package/dist/generators/test-generator/step-mapper.js.map +1 -1
  28. package/dist/generators/test-generator/template-engine.d.ts +29 -1
  29. package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
  30. package/dist/generators/test-generator/template-engine.js +11 -2
  31. package/dist/generators/test-generator/template-engine.js.map +1 -1
  32. package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -2
  33. package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
  34. package/dist/generators/test-generator/utils/data-resolver.js +36 -25
  35. package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
  36. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +7 -0
  37. package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -0
  38. package/dist/generators/test-generator/utils/runtime-data-transformer.js +42 -0
  39. package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -0
  40. package/dist/generators/types.d.ts +1 -0
  41. package/dist/generators/types.d.ts.map +1 -1
  42. package/dist/generators/types.js.map +1 -1
  43. package/dist/orchestrator/project-initializer.d.ts +9 -0
  44. package/dist/orchestrator/project-initializer.d.ts.map +1 -1
  45. package/dist/orchestrator/project-initializer.js +74 -10
  46. package/dist/orchestrator/project-initializer.js.map +1 -1
  47. package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +1 -1
  48. package/dist/orchestrator/templates/ai-instructions/claude-config.md +11 -2
  49. package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
  50. package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +54 -3
  51. package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +1 -1
  52. package/dist/orchestrator/templates/ai-instructions/copilot-config.md +11 -2
  53. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
  54. package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +54 -3
  55. package/dist/orchestrator/templates/specs-base.d.ts +12 -1
  56. package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
  57. package/dist/orchestrator/templates/specs-base.js +47 -5
  58. package/dist/orchestrator/templates/specs-base.js.map +1 -1
  59. package/dist/orchestrator/templates/specs-base.ts +65 -7
  60. package/dist/orchestrator/templates/specs-test-data.d.ts +14 -0
  61. package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -0
  62. package/dist/orchestrator/templates/specs-test-data.js +100 -0
  63. package/dist/orchestrator/templates/specs-test-data.js.map +1 -0
  64. package/dist/orchestrator/templates/specs-test-data.ts +66 -0
  65. package/package.json +1 -1
  66. package/src/cli/commands/generate.ts +2 -0
  67. package/src/cli/index.ts +1 -1
  68. package/src/generators/gherkin-parser/index.ts +4 -0
  69. package/src/generators/test-generator/adapters/adapter-interface.ts +12 -1
  70. package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +14 -2
  71. package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
  72. package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
  73. package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
  74. package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
  75. package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
  76. package/src/generators/test-generator/code-generator.ts +122 -13
  77. package/src/generators/test-generator/step-mapper.ts +2 -2
  78. package/src/generators/test-generator/template-engine.ts +28 -2
  79. package/src/generators/test-generator/utils/data-resolver.ts +45 -27
  80. package/src/generators/test-generator/utils/runtime-data-transformer.ts +51 -0
  81. package/src/generators/types.ts +1 -0
  82. package/src/orchestrator/project-initializer.ts +84 -10
  83. package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +1 -1
  84. package/src/orchestrator/templates/ai-instructions/claude-config.md +11 -2
  85. package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
  86. package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +54 -3
  87. package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +1 -1
  88. package/src/orchestrator/templates/ai-instructions/copilot-config.md +11 -2
  89. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
  90. package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +54 -3
  91. package/src/orchestrator/templates/specs-base.ts +65 -7
  92. package/src/orchestrator/templates/specs-test-data.ts +66 -0
@@ -112,20 +112,59 @@ Given User is on [Screen] page
112
112
  And User wait for [Page Title] heading is visible
113
113
  ```
114
114
 
115
+ ## Cleanup & Hooks
116
+
117
+ ### Auto-assign `@cleanup:*` tags based on screen sections
118
+
119
+ After identifying screen sections, add appropriate `@cleanup:*` feature-level tags. These activate base.ts fixtures that auto-clean state between tests.
120
+
121
+ | Screen has | Add tag | Why |
122
+ |---|---|---|
123
+ | Modal / Dialog / Drawer | `@cleanup:overlay` | Dismiss leftover overlays between tests |
124
+ | Form & Inputs / Search / Filter | `@cleanup:forms` | Clear form fields, reset selects |
125
+ | Long scrollable content | `@cleanup:scroll` | Scroll to top for consistent assertions |
126
+ | Auth tokens / session data in tests | `@cleanup:storage` | Clear sessionStorage |
127
+ | CI/CD or debug-heavy screens | `@screenshot:on-failure` | Auto-capture screenshot on test failure |
128
+
129
+ **Always add `@cleanup:overlay`** if ANY section opens a dialog (Create/Add, Update/Edit, Delete confirmation). Most CRUD screens need it.
130
+
131
+ **Always add `@cleanup:forms`** if the screen has inline search, filter dropdowns, or editable forms that persist between tests.
132
+
133
+ ### When to add `@afterEach` hook scenario
134
+
135
+ Only when `@cleanup:*` tags aren't enough — feature-specific cleanup logic:
136
+ - Reset a dropdown filter to default value (not just clear)
137
+ - Navigate away from a sub-tab back to the main tab
138
+ - Close a specific sidebar panel
139
+
140
+ ```gherkin
141
+ @afterEach
142
+ Scenario: Reset filters to default
143
+ When User select [Status Filter] dropdown with {{default_status}}
144
+ ```
145
+
146
+ ### `@beforeAll` / `@afterAll` — optional, low priority
147
+
148
+ For one-time setup/teardown. Most screens don't need these.
149
+
115
150
  ## Output Format
116
151
 
117
152
  **Feature file** — `qa/screens/<screen>/features/<screen>.feature`
118
153
 
119
- **Never use `Background:`.** Use `@steps` + `@extend` for shared setup (see `sungen-gherkin-syntax` skill).
154
+ `Background` is valid for simple shared setup (navigate to page). Use `@steps`/`@extend` for complex flows with scope (dialog, frame).
120
155
 
121
156
  ```gherkin
122
157
  @auth:role
158
+ @cleanup:overlay
159
+ @cleanup:forms
123
160
  Feature: <Screen> Screen
124
161
 
162
+ Background:
163
+ Given User is on [Screen] page
164
+
125
165
  # Shared setup — NO priority tag on @steps
126
166
  @steps:open_form
127
167
  Scenario: Open form
128
- Given User is on [Screen] page
129
168
  When User click [Create] button
130
169
  Then User see [Form] dialog
131
170
 
@@ -171,6 +210,18 @@ Feature: <Screen> Screen
171
210
 
172
211
  **Naming**: `VP-<CATEGORY>-<NNN>` prefix. Scenario name must use the **same element type** as the steps — e.g., if the step uses `dialog`, write "dialog opens" not "modal opens".
173
212
 
174
- **Test data** — `qa/screens/<screen>/test-data/<screen>.yaml`, grouped by section.
213
+ **Test data** — `qa/screens/<screen>/test-data/<screen>.yaml`, grouped by section. Data is loaded **at runtime** — keys become runtime lookups, not hardcoded strings. The same compiled test works across environments.
214
+
215
+ **Environment-specific data**: For values that differ per environment (credentials, URLs, test users), create `<screen>.<env>.yaml` alongside the base file. Users run `SUNGEN_ENV=staging npx playwright test` to merge overrides. Structure env YAML with the same keys, only including values that change:
216
+
217
+ ```yaml
218
+ # login.yaml (base)
219
+ valid_email: admin@dev.example.com
220
+ valid_password: DevPass123
221
+
222
+ # login.staging.yaml (override for staging)
223
+ valid_email: admin@staging.example.com
224
+ valid_password: StagingPass456
225
+ ```
175
226
 
176
227
  **Do NOT generate**: `selectors.yaml` (created during run-test), Playwright code (sungen compiles).
@@ -1,4 +1,15 @@
1
1
  import { expect } from '@playwright/test';
2
- declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
2
+ type CleanupConfig = {
3
+ overlay?: boolean;
4
+ forms?: boolean;
5
+ scroll?: boolean;
6
+ storage?: boolean;
7
+ };
8
+ declare const test: import("@playwright/test").TestType<import("@playwright/test").PlaywrightTestArgs & import("@playwright/test").PlaywrightTestOptions & {
9
+ autoCleanup: CleanupConfig;
10
+ screenshotOnFailure: boolean;
11
+ _autoCleanup: void;
12
+ _autoScreenshot: void;
13
+ }, import("@playwright/test").PlaywrightWorkerArgs & import("@playwright/test").PlaywrightWorkerOptions>;
3
14
  export { test, expect };
4
15
  //# sourceMappingURL=specs-base.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"specs-base.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAQxD,QAAA,MAAM,IAAI,6OA8DR,CAAC;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC"}
1
+ {"version":3,"file":"specs-base.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAGxD,KAAK,aAAa,GAAG;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAOF,QAAA,MAAM,IAAI;iBACK,aAAa;yBACL,OAAO;kBACd,IAAI;qBACD,IAAI;wGA6GrB,CAAC;AAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC"}
@@ -8,6 +8,8 @@ Object.defineProperty(exports, "expect", { enumerable: true, get: function () {
8
8
  const contextCache = new Map();
9
9
  const GOTO_PATCHED = Symbol('goto-patched');
10
10
  const test = test_1.test.extend({
11
+ autoCleanup: [{}, { option: true }],
12
+ screenshotOnFailure: [false, { option: true }],
11
13
  page: async ({ browser, storageState }, use) => {
12
14
  if (storageState) {
13
15
  const cacheKey = typeof storageState === 'string' ? storageState : JSON.stringify(storageState);
@@ -26,11 +28,6 @@ const test = test_1.test.extend({
26
28
  try {
27
29
  const currentPath = new URL(page.url()).pathname;
28
30
  if (currentPath === url || currentPath === url + '/') {
29
- // Dismiss any open overlays (dropdowns, dialogs) from previous test
30
- await page.keyboard.press('Escape').catch(() => { });
31
- await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => { });
32
- // Safety check: if a fixed-position overlay (modal/dialog) is still present, full reload
33
- // eslint-disable-next-line no-eval -- runs in browser context via Playwright
34
31
  const hasOverlay = await page.evaluate(`(() => {
35
32
  const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
36
33
  if (!el) return false;
@@ -65,6 +62,51 @@ const test = test_1.test.extend({
65
62
  await context.close();
66
63
  }
67
64
  },
65
+ // Auto-cleanup fixture: runs teardown after each test based on @cleanup:* tags
66
+ _autoCleanup: [async ({ page, autoCleanup }, use) => {
67
+ await use();
68
+ if (autoCleanup.overlay) {
69
+ await page.keyboard.press('Escape').catch(() => { });
70
+ await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => { });
71
+ // Dismiss persistent fixed overlays (modals, dialogs)
72
+ const hasOverlay = await page.evaluate(`(() => {
73
+ const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
74
+ if (!el) return false;
75
+ let current = el;
76
+ while (current && current !== document.body) {
77
+ if (getComputedStyle(current).position === 'fixed') return true;
78
+ current = current.parentElement;
79
+ }
80
+ return false;
81
+ })()`).catch(() => false);
82
+ if (hasOverlay) {
83
+ await page.keyboard.press('Escape').catch(() => { });
84
+ }
85
+ }
86
+ if (autoCleanup.forms) {
87
+ await page.evaluate(`(() => {
88
+ document.querySelectorAll('input:not([type=hidden]):not([type=submit])').forEach(el => { el.value = ''; });
89
+ document.querySelectorAll('textarea').forEach(el => { el.value = ''; });
90
+ document.querySelectorAll('select').forEach(el => { el.selectedIndex = 0; });
91
+ })()`).catch(() => { });
92
+ }
93
+ if (autoCleanup.scroll) {
94
+ await page.evaluate('window.scrollTo(0, 0)').catch(() => { });
95
+ }
96
+ if (autoCleanup.storage) {
97
+ await page.evaluate('sessionStorage.clear()').catch(() => { });
98
+ }
99
+ }, { auto: true }],
100
+ // Auto-screenshot fixture: captures screenshot on test failure
101
+ _autoScreenshot: [async ({ page, screenshotOnFailure }, use, testInfo) => {
102
+ await use();
103
+ if (screenshotOnFailure && testInfo.status !== testInfo.expectedStatus) {
104
+ await testInfo.attach('screenshot', {
105
+ body: await page.screenshot(),
106
+ contentType: 'image/png',
107
+ });
108
+ }
109
+ }, { auto: true }],
68
110
  });
69
111
  exports.test = test;
70
112
  //# sourceMappingURL=specs-base.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"specs-base.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"specs-base.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-base.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,27 @@
1
1
  import { test as base, expect } from '@playwright/test';
2
2
  import type { BrowserContext, Page } from '@playwright/test';
3
3
 
4
+ type CleanupConfig = {
5
+ overlay?: boolean;
6
+ forms?: boolean;
7
+ scroll?: boolean;
8
+ storage?: boolean;
9
+ };
10
+
4
11
  // Share one context per storageState — avoids creating multiple sessions
5
12
  // that trigger server rate limiting or session invalidation
6
13
  const contextCache = new Map<string, { context: BrowserContext; page: Page }>();
7
14
  const GOTO_PATCHED = Symbol('goto-patched');
8
15
 
9
- const test = base.extend({
16
+ const test = base.extend<{
17
+ autoCleanup: CleanupConfig;
18
+ screenshotOnFailure: boolean;
19
+ _autoCleanup: void;
20
+ _autoScreenshot: void;
21
+ }>({
22
+ autoCleanup: [{}, { option: true }],
23
+ screenshotOnFailure: [false, { option: true }],
24
+
10
25
  page: async ({ browser, storageState }, use) => {
11
26
  if (storageState) {
12
27
  const cacheKey = typeof storageState === 'string' ? storageState : JSON.stringify(storageState);
@@ -28,12 +43,6 @@ const test = base.extend({
28
43
  try {
29
44
  const currentPath = new URL(page.url()).pathname;
30
45
  if (currentPath === url || currentPath === url + '/') {
31
- // Dismiss any open overlays (dropdowns, dialogs) from previous test
32
- await page.keyboard.press('Escape').catch(() => {});
33
- await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => {});
34
-
35
- // Safety check: if a fixed-position overlay (modal/dialog) is still present, full reload
36
- // eslint-disable-next-line no-eval -- runs in browser context via Playwright
37
46
  const hasOverlay = await page.evaluate(`(() => {
38
47
  const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
39
48
  if (!el) return false;
@@ -68,6 +77,55 @@ const test = base.extend({
68
77
  await context.close();
69
78
  }
70
79
  },
80
+
81
+ // Auto-cleanup fixture: runs teardown after each test based on @cleanup:* tags
82
+ _autoCleanup: [async ({ page, autoCleanup }, use) => {
83
+ await use();
84
+
85
+ if (autoCleanup.overlay) {
86
+ await page.keyboard.press('Escape').catch(() => {});
87
+ await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => {});
88
+ // Dismiss persistent fixed overlays (modals, dialogs)
89
+ const hasOverlay = await page.evaluate(`(() => {
90
+ const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
91
+ if (!el) return false;
92
+ let current = el;
93
+ while (current && current !== document.body) {
94
+ if (getComputedStyle(current).position === 'fixed') return true;
95
+ current = current.parentElement;
96
+ }
97
+ return false;
98
+ })()`).catch(() => false);
99
+ if (hasOverlay) {
100
+ await page.keyboard.press('Escape').catch(() => {});
101
+ }
102
+ }
103
+ if (autoCleanup.forms) {
104
+ await page.evaluate(`(() => {
105
+ document.querySelectorAll('input:not([type=hidden]):not([type=submit])').forEach(el => { el.value = ''; });
106
+ document.querySelectorAll('textarea').forEach(el => { el.value = ''; });
107
+ document.querySelectorAll('select').forEach(el => { el.selectedIndex = 0; });
108
+ })()`).catch(() => {});
109
+ }
110
+ if (autoCleanup.scroll) {
111
+ await page.evaluate('window.scrollTo(0, 0)').catch(() => {});
112
+ }
113
+ if (autoCleanup.storage) {
114
+ await page.evaluate('sessionStorage.clear()').catch(() => {});
115
+ }
116
+ }, { auto: true }],
117
+
118
+ // Auto-screenshot fixture: captures screenshot on test failure
119
+ _autoScreenshot: [async ({ page, screenshotOnFailure }, use, testInfo) => {
120
+ await use();
121
+
122
+ if (screenshotOnFailure && testInfo.status !== testInfo.expectedStatus) {
123
+ await testInfo.attach('screenshot', {
124
+ body: await page.screenshot(),
125
+ contentType: 'image/png',
126
+ });
127
+ }
128
+ }, { auto: true }],
71
129
  });
72
130
 
73
131
  export { test, expect };
@@ -0,0 +1,14 @@
1
+ export declare class TestDataLoader {
2
+ private data;
3
+ private constructor();
4
+ /**
5
+ * Load test data for a screen/feature combination.
6
+ *
7
+ * Priority (later wins):
8
+ * 1. {feature}.yaml — base data
9
+ * 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
10
+ */
11
+ static load(screenName: string, featureName: string): TestDataLoader;
12
+ get(key: string): string;
13
+ }
14
+ //# sourceMappingURL=specs-test-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs-test-data.d.ts","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-test-data.ts"],"names":[],"mappings":"AAIA,qBAAa,cAAc;IACzB,OAAO,CAAC,IAAI,CAAsB;IAElC,OAAO;IAIP;;;;;;OAMG;IACH,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc;IAcpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;CAczB"}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.TestDataLoader = void 0;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const yaml_1 = __importDefault(require("yaml"));
43
+ class TestDataLoader {
44
+ constructor(data) {
45
+ this.data = data;
46
+ }
47
+ /**
48
+ * Load test data for a screen/feature combination.
49
+ *
50
+ * Priority (later wins):
51
+ * 1. {feature}.yaml — base data
52
+ * 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
53
+ */
54
+ static load(screenName, featureName) {
55
+ const baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
56
+ const env = process.env.SUNGEN_ENV;
57
+ let data = loadYamlSync(path.join(baseDir, `${featureName}.yaml`)) || {};
58
+ if (env) {
59
+ const envData = loadYamlSync(path.join(baseDir, `${featureName}.${env}.yaml`));
60
+ if (envData)
61
+ data = deepMerge(data, envData);
62
+ }
63
+ return new TestDataLoader(data);
64
+ }
65
+ get(key) {
66
+ const parts = key.split('.');
67
+ let current = this.data;
68
+ for (const part of parts) {
69
+ if (current == null || typeof current !== 'object') {
70
+ throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
71
+ }
72
+ current = current[part];
73
+ }
74
+ if (current === undefined || current === null) {
75
+ throw new Error(`Test data key not found: ${key}`);
76
+ }
77
+ return String(current);
78
+ }
79
+ }
80
+ exports.TestDataLoader = TestDataLoader;
81
+ function loadYamlSync(filePath) {
82
+ if (!fs.existsSync(filePath))
83
+ return null;
84
+ const content = fs.readFileSync(filePath, 'utf-8');
85
+ return yaml_1.default.parse(content) || null;
86
+ }
87
+ function deepMerge(base, override) {
88
+ const result = { ...base };
89
+ for (const [key, value] of Object.entries(override)) {
90
+ if (value && typeof value === 'object' && !Array.isArray(value) &&
91
+ result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
92
+ result[key] = deepMerge(result[key], value);
93
+ }
94
+ else {
95
+ result[key] = value;
96
+ }
97
+ }
98
+ return result;
99
+ }
100
+ //# sourceMappingURL=specs-test-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specs-test-data.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-test-data.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,66 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import yaml from 'yaml';
4
+
5
+ export class TestDataLoader {
6
+ private data: Record<string, any>;
7
+
8
+ private constructor(data: Record<string, any>) {
9
+ this.data = data;
10
+ }
11
+
12
+ /**
13
+ * Load test data for a screen/feature combination.
14
+ *
15
+ * Priority (later wins):
16
+ * 1. {feature}.yaml — base data
17
+ * 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
18
+ */
19
+ static load(screenName: string, featureName: string): TestDataLoader {
20
+ const baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
21
+ const env = process.env.SUNGEN_ENV;
22
+
23
+ let data = loadYamlSync(path.join(baseDir, `${featureName}.yaml`)) || {};
24
+
25
+ if (env) {
26
+ const envData = loadYamlSync(path.join(baseDir, `${featureName}.${env}.yaml`));
27
+ if (envData) data = deepMerge(data, envData);
28
+ }
29
+
30
+ return new TestDataLoader(data);
31
+ }
32
+
33
+ get(key: string): string {
34
+ const parts = key.split('.');
35
+ let current: any = this.data;
36
+ for (const part of parts) {
37
+ if (current == null || typeof current !== 'object') {
38
+ throw new Error(`Test data key not found: ${key} (failed at '${part}')`);
39
+ }
40
+ current = current[part];
41
+ }
42
+ if (current === undefined || current === null) {
43
+ throw new Error(`Test data key not found: ${key}`);
44
+ }
45
+ return String(current);
46
+ }
47
+ }
48
+
49
+ function loadYamlSync(filePath: string): Record<string, any> | null {
50
+ if (!fs.existsSync(filePath)) return null;
51
+ const content = fs.readFileSync(filePath, 'utf-8');
52
+ return yaml.parse(content) || null;
53
+ }
54
+
55
+ function deepMerge(base: Record<string, any>, override: Record<string, any>): Record<string, any> {
56
+ const result = { ...base };
57
+ for (const [key, value] of Object.entries(override)) {
58
+ if (value && typeof value === 'object' && !Array.isArray(value) &&
59
+ result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])) {
60
+ result[key] = deepMerge(result[key], value);
61
+ } else {
62
+ result[key] = value;
63
+ }
64
+ }
65
+ return result;
66
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sungen",
3
- "version": "2.4.6",
3
+ "version": "2.5.0",
4
4
  "description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -58,6 +58,7 @@ export function registerGenerateCommand(program: Command): void {
58
58
  .option('-s, --screen <name>', 'Generate tests for a specific screen')
59
59
  .option('--all', 'Generate tests for all screens')
60
60
  .option('--framework <name>', 'Test framework (default: playwright)', 'playwright')
61
+ .option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
61
62
  .action(async (options) => {
62
63
  try {
63
64
  const screenName = options.screen;
@@ -89,6 +90,7 @@ export function registerGenerateCommand(program: Command): void {
89
90
  framework: options.framework || 'playwright',
90
91
  screenName,
91
92
  verbose: program.opts().verbose,
93
+ runtimeData: !options.inlineData,
92
94
  });
93
95
 
94
96
  const results = await generator.generateAllTests(
package/src/cli/index.ts CHANGED
@@ -18,7 +18,7 @@ async function main() {
18
18
  program
19
19
  .name('sungen')
20
20
  .description('Deterministic E2E Test Compiler — Gherkin + Selectors → Playwright')
21
- .version('2.4.6');
21
+ .version('2.5.0');
22
22
 
23
23
  // Global options
24
24
  program
@@ -34,6 +34,7 @@ export interface ParsedScenario {
34
34
  steps: ParsedStep[];
35
35
  stepsName?: string; // set when scenario has @steps:<name> — marks it as a reusable block
36
36
  extendsName?: string; // set when scenario has @extend:<name> — merges base steps inline
37
+ hookType?: 'beforeAll' | 'afterEach' | 'afterAll'; // set when scenario has @beforeAll/@afterEach/@afterAll
37
38
  }
38
39
 
39
40
  export interface ParsedFeature {
@@ -120,12 +121,15 @@ export class GherkinParser {
120
121
  const tags = scenario.tags.map((tag) => tag.name);
121
122
  const stepsName = tags.find(t => t.startsWith('@steps:'))?.replace('@steps:', '') || undefined;
122
123
  const extendsName = tags.find(t => t.startsWith('@extend:'))?.replace('@extend:', '') || undefined;
124
+ const hookTag = tags.find(t => /^@(beforeAll|afterEach|afterAll)$/.test(t));
125
+ const hookType = hookTag?.replace('@', '') as ParsedScenario['hookType'];
123
126
  return {
124
127
  name: scenario.name,
125
128
  tags,
126
129
  steps: scenario.steps.map((step) => this.parseStep(step)),
127
130
  stepsName,
128
131
  extendsName,
132
+ hookType,
129
133
  };
130
134
  });
131
135
  }
@@ -13,6 +13,14 @@ export interface TestFileData {
13
13
  featureName: string;
14
14
  featureDescription?: string;
15
15
  background?: string;
16
+ beforeAll?: string;
17
+ afterEach?: string;
18
+ afterAll?: string;
19
+ cleanupConfig?: string; // Pre-formatted autoCleanup config from @cleanup:* tags
20
+ screenshotOnFailure?: boolean; // @screenshot:on-failure tag
21
+ runtimeData?: boolean; // --runtime-data flag: testData.get() instead of hardcoded values
22
+ screenName?: string; // Screen name for TestDataLoader.load()
23
+ featureFileName?: string; // Feature file name for TestDataLoader.load()
16
24
  scenarios: string[];
17
25
  authGroups?: AuthGroup[]; // Grouped by auth role for nested describes
18
26
  singleAuthRole?: string; // Auth role when all scenarios share the same role
@@ -49,8 +57,11 @@ export interface TestGeneratorAdapter {
49
57
  // Template rendering methods
50
58
  renderTestFile(data: TestFileData): string;
51
59
  renderScenario(data: ScenarioData): string;
52
- renderImports(): string;
60
+ renderImports(options?: { runtimeData?: boolean }): string;
53
61
  renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
62
+ renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
63
+ renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
64
+ renderAfterAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
54
65
 
55
66
  // Step rendering
56
67
  renderStep(templateName: string, data: any): string;
@@ -26,14 +26,26 @@ export class PlaywrightAdapter implements TestGeneratorAdapter {
26
26
  return this.templateEngine.renderScenario(data);
27
27
  }
28
28
 
29
- renderImports(): string {
30
- return this.templateEngine.renderImports();
29
+ renderImports(options?: { runtimeData?: boolean }): string {
30
+ return this.templateEngine.renderImports(options);
31
31
  }
32
32
 
33
33
  renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string {
34
34
  return this.templateEngine.renderBeforeEach(data);
35
35
  }
36
36
 
37
+ renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string {
38
+ return this.templateEngine.renderBeforeAll(data);
39
+ }
40
+
41
+ renderAfterEach(data: { steps: Array<{ comment?: string; code: string }> }): string {
42
+ return this.templateEngine.renderAfterEach(data);
43
+ }
44
+
45
+ renderAfterAll(data: { steps: Array<{ comment?: string; code: string }> }): string {
46
+ return this.templateEngine.renderAfterAll(data);
47
+ }
48
+
37
49
  renderStep(templateName: string, data: any): string {
38
50
  return this.templateEngine.renderStep(templateName, data);
39
51
  }
@@ -0,0 +1,8 @@
1
+ test.afterAll(async ({ request }) => {
2
+ {{#each steps}}
3
+ {{#if comment}}
4
+ // {{comment}}
5
+ {{/if}}
6
+ {{code}}
7
+ {{/each}}
8
+ });
@@ -0,0 +1,8 @@
1
+ test.afterEach(async ({ page }) => {
2
+ {{#each steps}}
3
+ {{#if comment}}
4
+ // {{comment}}
5
+ {{/if}}
6
+ {{code}}
7
+ {{/each}}
8
+ });
@@ -0,0 +1,8 @@
1
+ test.beforeAll(async ({ request }) => {
2
+ {{#each steps}}
3
+ {{#if comment}}
4
+ // {{comment}}
5
+ {{/if}}
6
+ {{code}}
7
+ {{/each}}
8
+ });
@@ -1,4 +1,7 @@
1
1
  import { test, expect } from '../base';
2
+ {{#if runtimeData}}
3
+ import { TestDataLoader } from '../test-data';
4
+ {{/if}}
2
5
 
3
6
  // This file is auto-generated from Gherkin feature files
4
7
  // DO NOT EDIT MANUALLY - changes will be overwritten