@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.
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +2 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +1 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +3 -0
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +29 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +21 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js +11 -2
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
- package/dist/generators/test-generator/code-generator.d.ts +2 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +109 -12
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +1 -1
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +29 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +11 -2
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +11 -2
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +36 -25
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +7 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +42 -0
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -0
- package/dist/generators/types.d.ts +1 -0
- package/dist/generators/types.d.ts.map +1 -1
- package/dist/generators/types.js.map +1 -1
- package/dist/orchestrator/project-initializer.d.ts +9 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +74 -10
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +11 -2
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +54 -3
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +1 -1
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +11 -2
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +54 -3
- package/dist/orchestrator/templates/specs-base.d.ts +12 -1
- package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-base.js +47 -5
- package/dist/orchestrator/templates/specs-base.js.map +1 -1
- package/dist/orchestrator/templates/specs-base.ts +65 -7
- package/dist/orchestrator/templates/specs-test-data.d.ts +14 -0
- package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-test-data.js +100 -0
- package/dist/orchestrator/templates/specs-test-data.js.map +1 -0
- package/dist/orchestrator/templates/specs-test-data.ts +66 -0
- package/package.json +1 -1
- package/src/cli/commands/generate.ts +2 -0
- package/src/cli/index.ts +1 -1
- package/src/generators/gherkin-parser/index.ts +4 -0
- package/src/generators/test-generator/adapters/adapter-interface.ts +12 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +14 -2
- package/src/generators/test-generator/adapters/playwright/templates/after-all.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/after-each.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/before-all.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/test-file.hbs +24 -0
- package/src/generators/test-generator/code-generator.ts +122 -13
- package/src/generators/test-generator/step-mapper.ts +2 -2
- package/src/generators/test-generator/template-engine.ts +28 -2
- package/src/generators/test-generator/utils/data-resolver.ts +45 -27
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +51 -0
- package/src/generators/types.ts +1 -0
- package/src/orchestrator/project-initializer.ts +84 -10
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +1 -1
- package/src/orchestrator/templates/ai-instructions/claude-config.md +11 -2
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +66 -13
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +54 -3
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +1 -1
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +11 -2
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +86 -13
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +54 -3
- package/src/orchestrator/templates/specs-base.ts +65 -7
- 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
|
-
|
|
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
|
-
|
|
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;
|
|
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":";;;AAAA,2CAAwD;
|
|
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
|
@@ -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
|
@@ -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
|
}
|