@sun-asterisk/sungen 2.5.1 → 2.6.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/add-flow.d.ts +3 -0
- package/dist/cli/commands/add-flow.d.ts.map +1 -0
- package/dist/cli/commands/add-flow.js +27 -0
- package/dist/cli/commands/add-flow.js.map +1 -0
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +95 -60
- package/dist/cli/commands/delivery.js.map +1 -1
- package/dist/cli/commands/generate.d.ts.map +1 -1
- package/dist/cli/commands/generate.js +38 -6
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/playwright-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +2 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +2 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
- package/dist/generators/test-generator/code-generator.d.ts +1 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +30 -12
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +4 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +7 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +3 -20
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +23 -66
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +2 -6
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +18 -80
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/orchestrator/ai-rules-updater.d.ts.map +1 -1
- package/dist/orchestrator/ai-rules-updater.js +4 -0
- package/dist/orchestrator/ai-rules-updater.js.map +1 -1
- package/dist/orchestrator/flow-manager.d.ts +22 -0
- package/dist/orchestrator/flow-manager.d.ts.map +1 -0
- package/dist/orchestrator/flow-manager.js +251 -0
- package/dist/orchestrator/flow-manager.js.map +1 -0
- package/dist/orchestrator/project-initializer.d.ts.map +1 -1
- package/dist/orchestrator/project-initializer.js +1 -0
- package/dist/orchestrator/project-initializer.js.map +1 -1
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +88 -0
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -8
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -6
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +15 -11
- package/dist/orchestrator/templates/ai-instructions/claude-config.md +41 -10
- package/dist/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-delivery.md +19 -18
- package/dist/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +52 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +31 -3
- package/dist/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +45 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +69 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +30 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +86 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +13 -10
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +18 -17
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +23 -19
- package/dist/orchestrator/templates/ai-instructions/copilot-config.md +40 -9
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +19 -18
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +12 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +52 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +31 -3
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +45 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +70 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +30 -0
- package/dist/orchestrator/templates/playwright.config.d.ts.map +1 -1
- package/dist/orchestrator/templates/playwright.config.js +3 -1
- package/dist/orchestrator/templates/playwright.config.js.map +1 -1
- package/dist/orchestrator/templates/playwright.config.ts +4 -1
- package/dist/orchestrator/templates/specs-base.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-base.js +11 -56
- package/dist/orchestrator/templates/specs-base.js.map +1 -1
- package/dist/orchestrator/templates/specs-base.ts +11 -61
- package/dist/orchestrator/templates/specs-test-data.d.ts +3 -1
- package/dist/orchestrator/templates/specs-test-data.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-test-data.js +10 -2
- package/dist/orchestrator/templates/specs-test-data.js.map +1 -1
- package/dist/orchestrator/templates/specs-test-data.ts +9 -2
- package/package.json +1 -1
- package/src/cli/commands/add-flow.ts +25 -0
- package/src/cli/commands/delivery.ts +109 -58
- package/src/cli/commands/generate.ts +43 -6
- package/src/cli/index.ts +3 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-accept-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-dismiss-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/alert-fill-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/alert-text-assertion.hbs +2 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/column-cell-assertion.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs +2 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-timeout.hbs +1 -1
- package/src/generators/test-generator/code-generator.ts +32 -14
- package/src/generators/test-generator/step-mapper.ts +8 -0
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/generators/test-generator/utils/data-resolver.ts +25 -77
- package/src/generators/test-generator/utils/selector-resolver.ts +23 -109
- package/src/orchestrator/ai-rules-updater.ts +5 -0
- package/src/orchestrator/flow-manager.ts +243 -0
- package/src/orchestrator/project-initializer.ts +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-add-flow.md +88 -0
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +11 -8
- package/src/orchestrator/templates/ai-instructions/claude-cmd-review.md +8 -6
- package/src/orchestrator/templates/ai-instructions/claude-cmd-run-test.md +15 -11
- package/src/orchestrator/templates/ai-instructions/claude-config.md +41 -10
- package/src/orchestrator/templates/ai-instructions/claude-skill-capture-live.md +12 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-delivery.md +19 -18
- package/src/orchestrator/templates/ai-instructions/claude-skill-error-mapping.md +12 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +52 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-fix.md +31 -3
- package/src/orchestrator/templates/ai-instructions/claude-skill-selector-keys.md +45 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +69 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +30 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-add-flow.md +86 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +13 -10
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-delivery.md +18 -17
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +23 -19
- package/src/orchestrator/templates/ai-instructions/copilot-config.md +40 -9
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-capture-live.md +12 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-delivery.md +19 -18
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-error-mapping.md +12 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +52 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-fix.md +31 -3
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-selector-keys.md +45 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +70 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +30 -0
- package/src/orchestrator/templates/playwright.config.ts +4 -1
- package/src/orchestrator/templates/specs-base.ts +11 -61
- package/src/orchestrator/templates/specs-test-data.ts +9 -2
- package/dist/utils/feature-finder.d.ts +0 -9
- package/dist/utils/feature-finder.d.ts.map +0 -1
- package/dist/utils/feature-finder.js +0 -67
- package/dist/utils/feature-finder.js.map +0 -1
- package/dist/utils/screen-paths.d.ts +0 -10
- package/dist/utils/screen-paths.d.ts.map +0 -1
- package/dist/utils/screen-paths.js +0 -73
- package/dist/utils/screen-paths.js.map +0 -1
- package/dist/utils/selector-loader.d.ts +0 -6
- package/dist/utils/selector-loader.d.ts.map +0 -1
- package/dist/utils/selector-loader.js +0 -20
- package/dist/utils/selector-loader.js.map +0 -1
- package/dist/utils/test-data-loader.d.ts +0 -6
- package/dist/utils/test-data-loader.d.ts.map +0 -1
- package/dist/utils/test-data-loader.js +0 -20
- package/dist/utils/test-data-loader.js.map +0 -1
- package/src/utils/feature-finder.ts +0 -33
- package/src/utils/screen-paths.ts +0 -37
- package/src/utils/selector-loader.ts +0 -23
- package/src/utils/test-data-loader.ts +0 -23
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { test as base, expect } from '@playwright/test';
|
|
2
|
-
import type { BrowserContext, Page } from '@playwright/test';
|
|
3
2
|
|
|
4
3
|
type CleanupConfig = {
|
|
5
4
|
overlay?: boolean;
|
|
@@ -8,11 +7,6 @@ type CleanupConfig = {
|
|
|
8
7
|
storage?: boolean;
|
|
9
8
|
};
|
|
10
9
|
|
|
11
|
-
// Share one context per storageState — avoids creating multiple sessions
|
|
12
|
-
// that trigger server rate limiting or session invalidation
|
|
13
|
-
const contextCache = new Map<string, { context: BrowserContext; page: Page }>();
|
|
14
|
-
const GOTO_PATCHED = Symbol('goto-patched');
|
|
15
|
-
|
|
16
10
|
const test = base.extend<{
|
|
17
11
|
autoCleanup: CleanupConfig;
|
|
18
12
|
screenshotOnFailure: boolean;
|
|
@@ -23,69 +17,25 @@ const test = base.extend<{
|
|
|
23
17
|
screenshotOnFailure: [false, { option: true }],
|
|
24
18
|
|
|
25
19
|
page: async ({ browser, storageState }, use) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cached = { context, page };
|
|
34
|
-
contextCache.set(cacheKey, cached);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const page = cached.page;
|
|
38
|
-
|
|
39
|
-
// Patch goto once: skip navigation if already on the target path
|
|
40
|
-
if (!(page as any)[GOTO_PATCHED]) {
|
|
41
|
-
const originalGoto = page.goto.bind(page);
|
|
42
|
-
page.goto = async function (url: string, options?: any) {
|
|
43
|
-
try {
|
|
44
|
-
const currentPath = new URL(page.url()).pathname;
|
|
45
|
-
if (currentPath === url || currentPath === url + '/') {
|
|
46
|
-
const hasOverlay = await page.evaluate(`(() => {
|
|
47
|
-
const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
|
|
48
|
-
if (!el) return false;
|
|
49
|
-
let current = el;
|
|
50
|
-
while (current && current !== document.body) {
|
|
51
|
-
if (getComputedStyle(current).position === 'fixed') return true;
|
|
52
|
-
current = current.parentElement;
|
|
53
|
-
}
|
|
54
|
-
return false;
|
|
55
|
-
})()`).catch(() => true);
|
|
56
|
-
|
|
57
|
-
if (hasOverlay) {
|
|
58
|
-
return originalGoto(url, options);
|
|
59
|
-
}
|
|
60
|
-
return null as any;
|
|
61
|
-
}
|
|
62
|
-
} catch {
|
|
63
|
-
// page.url() might be about:blank on first run
|
|
64
|
-
}
|
|
65
|
-
return originalGoto(url, options);
|
|
66
|
-
};
|
|
67
|
-
(page as any)[GOTO_PATCHED] = true;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
await use(page);
|
|
71
|
-
} else {
|
|
72
|
-
// No storageState: fresh context (e.g., unauthenticated tests)
|
|
73
|
-
const context = await browser.newContext();
|
|
74
|
-
const page = await context.newPage();
|
|
75
|
-
await use(page);
|
|
76
|
-
await page.close();
|
|
77
|
-
await context.close();
|
|
78
|
-
}
|
|
20
|
+
const context = storageState
|
|
21
|
+
? await browser.newContext({ storageState })
|
|
22
|
+
: await browser.newContext();
|
|
23
|
+
const page = await context.newPage();
|
|
24
|
+
await use(page);
|
|
25
|
+
await page.close();
|
|
26
|
+
await context.close();
|
|
79
27
|
},
|
|
80
28
|
|
|
81
29
|
// Auto-cleanup fixture: runs teardown after each test based on @cleanup:* tags
|
|
82
|
-
_autoCleanup: [async ({ page, autoCleanup }, use) => {
|
|
30
|
+
_autoCleanup: [async ({ page, autoCleanup }, use, testInfo) => {
|
|
83
31
|
await use();
|
|
84
32
|
|
|
33
|
+
// Only run cleanup when the test failed — avoids masking state for the next test
|
|
34
|
+
if (testInfo.status === testInfo.expectedStatus) return;
|
|
35
|
+
|
|
85
36
|
if (autoCleanup.overlay) {
|
|
86
37
|
await page.keyboard.press('Escape').catch(() => {});
|
|
87
38
|
await page.locator('body').click({ position: { x: 1, y: 1 }, force: true }).catch(() => {});
|
|
88
|
-
// Dismiss persistent fixed overlays (modals, dialogs)
|
|
89
39
|
const hasOverlay = await page.evaluate(`(() => {
|
|
90
40
|
const el = document.elementFromPoint(window.innerWidth / 2, window.innerHeight / 2);
|
|
91
41
|
if (!el) return false;
|
|
@@ -2,11 +2,13 @@ export declare class TestDataLoader {
|
|
|
2
2
|
private data;
|
|
3
3
|
private constructor();
|
|
4
4
|
/**
|
|
5
|
-
* Load test data for a screen/feature combination.
|
|
5
|
+
* Load test data for a screen/feature or flow/feature combination.
|
|
6
6
|
*
|
|
7
7
|
* Priority (later wins):
|
|
8
8
|
* 1. {feature}.yaml — base data
|
|
9
9
|
* 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
|
|
10
|
+
*
|
|
11
|
+
* Paths: screenName starting with "flows/" loads from qa/flows/, otherwise qa/screens/
|
|
10
12
|
*/
|
|
11
13
|
static load(screenName: string, featureName: string): TestDataLoader;
|
|
12
14
|
get(key: string): string;
|
|
@@ -1 +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
|
|
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;;;;;;;;OAQG;IACH,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,cAAc;IAmBpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;CAczB"}
|
|
@@ -45,14 +45,22 @@ class TestDataLoader {
|
|
|
45
45
|
this.data = data;
|
|
46
46
|
}
|
|
47
47
|
/**
|
|
48
|
-
* Load test data for a screen/feature combination.
|
|
48
|
+
* Load test data for a screen/feature or flow/feature combination.
|
|
49
49
|
*
|
|
50
50
|
* Priority (later wins):
|
|
51
51
|
* 1. {feature}.yaml — base data
|
|
52
52
|
* 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
|
|
53
|
+
*
|
|
54
|
+
* Paths: screenName starting with "flows/" loads from qa/flows/, otherwise qa/screens/
|
|
53
55
|
*/
|
|
54
56
|
static load(screenName, featureName) {
|
|
55
|
-
|
|
57
|
+
let baseDir;
|
|
58
|
+
if (screenName.startsWith('flows/')) {
|
|
59
|
+
baseDir = path.join(process.cwd(), 'qa', screenName, 'test-data');
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
|
|
63
|
+
}
|
|
56
64
|
const env = process.env.SUNGEN_ENV;
|
|
57
65
|
let data = loadYamlSync(path.join(baseDir, `${featureName}.yaml`)) || {};
|
|
58
66
|
if (env) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-test-data.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-test-data.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,uCAAyB;AACzB,2CAA6B;AAC7B,gDAAwB;AAExB,MAAa,cAAc;IAGzB,YAAoB,IAAyB;QAC3C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"specs-test-data.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-test-data.ts"],"names":[],"mappings":"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"}
|
|
@@ -10,14 +10,21 @@ export class TestDataLoader {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Load test data for a screen/feature combination.
|
|
13
|
+
* Load test data for a screen/feature or flow/feature combination.
|
|
14
14
|
*
|
|
15
15
|
* Priority (later wins):
|
|
16
16
|
* 1. {feature}.yaml — base data
|
|
17
17
|
* 2. {feature}.{SUNGEN_ENV}.yaml — environment-specific (if SUNGEN_ENV set)
|
|
18
|
+
*
|
|
19
|
+
* Paths: screenName starting with "flows/" loads from qa/flows/, otherwise qa/screens/
|
|
18
20
|
*/
|
|
19
21
|
static load(screenName: string, featureName: string): TestDataLoader {
|
|
20
|
-
|
|
22
|
+
let baseDir: string;
|
|
23
|
+
if (screenName.startsWith('flows/')) {
|
|
24
|
+
baseDir = path.join(process.cwd(), 'qa', screenName, 'test-data');
|
|
25
|
+
} else {
|
|
26
|
+
baseDir = path.join(process.cwd(), 'qa', 'screens', screenName, 'test-data');
|
|
27
|
+
}
|
|
21
28
|
const env = process.env.SUNGEN_ENV;
|
|
22
29
|
|
|
23
30
|
let data = loadYamlSync(path.join(baseDir, `${featureName}.yaml`)) || {};
|
package/package.json
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
|
|
3
|
+
export function registerAddFlowCommand(program: Command): void {
|
|
4
|
+
program
|
|
5
|
+
.command('add-flow')
|
|
6
|
+
.description('Add a flow definition with scaffolded files for E2E cross-screen testing')
|
|
7
|
+
.requiredOption('--flow <name>', 'Flow name')
|
|
8
|
+
.option('-p, --path <path>', 'Starting page route path (e.g. /login)')
|
|
9
|
+
.option('-d, --description <text>', 'Flow description')
|
|
10
|
+
.action(async (options) => {
|
|
11
|
+
try {
|
|
12
|
+
const { FlowManager } = require('../../orchestrator/flow-manager');
|
|
13
|
+
const manager = new FlowManager();
|
|
14
|
+
|
|
15
|
+
await manager.addFlow({
|
|
16
|
+
name: options.flow,
|
|
17
|
+
path: options.path,
|
|
18
|
+
description: options.description,
|
|
19
|
+
});
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('Error:', error instanceof Error ? error.message : error);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -9,7 +9,7 @@ import * as fs from 'fs';
|
|
|
9
9
|
import { execSync } from 'child_process';
|
|
10
10
|
import { parseFeatureMetadata } from '../../exporters/feature-parser';
|
|
11
11
|
import { parseSpecFile } from '../../exporters/spec-parser';
|
|
12
|
-
import { loadTestData
|
|
12
|
+
import { loadTestData } from '../../exporters/test-data-resolver';
|
|
13
13
|
import { loadPlaywrightReport } from '../../exporters/playwright-report-parser';
|
|
14
14
|
import { mergeFeatureAndSpec } from '../../exporters/scenario-merger';
|
|
15
15
|
import {
|
|
@@ -39,14 +39,46 @@ function log(msg: string): void {
|
|
|
39
39
|
// Discovery
|
|
40
40
|
// ----------------------------------------------------------------------------
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
interface DeliveryTarget {
|
|
43
|
+
name: string;
|
|
44
|
+
isFlow: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function listAllTargets(cwd: string): DeliveryTarget[] {
|
|
48
|
+
const targets: DeliveryTarget[] = [];
|
|
49
|
+
|
|
43
50
|
const screensDir = path.join(cwd, 'qa', 'screens');
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
if (fs.existsSync(screensDir)) {
|
|
52
|
+
for (const d of fs.readdirSync(screensDir, { withFileTypes: true })) {
|
|
53
|
+
if (d.isDirectory()) targets.push({ name: d.name, isFlow: false });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const flowsDir = path.join(cwd, 'qa', 'flows');
|
|
58
|
+
if (fs.existsSync(flowsDir)) {
|
|
59
|
+
for (const d of fs.readdirSync(flowsDir, { withFileTypes: true })) {
|
|
60
|
+
if (d.isDirectory()) targets.push({ name: d.name, isFlow: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return targets.sort((a, b) => a.name.localeCompare(b.name));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resolveTargetType(cwd: string, name: string): DeliveryTarget {
|
|
68
|
+
if (fs.existsSync(path.join(cwd, 'qa', 'flows', name))) {
|
|
69
|
+
return { name, isFlow: true };
|
|
70
|
+
}
|
|
71
|
+
return { name, isFlow: false };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function qaDir(cwd: string, target: DeliveryTarget): string {
|
|
75
|
+
return path.join(cwd, 'qa', target.isFlow ? 'flows' : 'screens', target.name);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function generatedDir(cwd: string, target: DeliveryTarget): string {
|
|
79
|
+
return target.isFlow
|
|
80
|
+
? path.join(cwd, 'specs', 'generated', 'flows', target.name)
|
|
81
|
+
: path.join(cwd, 'specs', 'generated', target.name);
|
|
50
82
|
}
|
|
51
83
|
|
|
52
84
|
// ----------------------------------------------------------------------------
|
|
@@ -54,56 +86,65 @@ function listAllScreens(cwd: string): string[] {
|
|
|
54
86
|
// ----------------------------------------------------------------------------
|
|
55
87
|
|
|
56
88
|
/**
|
|
57
|
-
* Resolve the results file path for a
|
|
58
|
-
* Prefer the per-
|
|
89
|
+
* Resolve the results file path for a target.
|
|
90
|
+
* Prefer the per-target co-located file, fall back to the global `test-results/results.json`.
|
|
59
91
|
*/
|
|
60
|
-
function resolveResultsPath(cwd: string,
|
|
61
|
-
const
|
|
62
|
-
|
|
92
|
+
function resolveResultsPath(cwd: string, target: DeliveryTarget): string | null {
|
|
93
|
+
const genDir = generatedDir(cwd, target);
|
|
94
|
+
const perTarget = path.join(genDir, `${target.name}-test-result.json`);
|
|
95
|
+
if (fs.existsSync(perTarget)) return perTarget;
|
|
63
96
|
const global = path.join(cwd, 'test-results', 'results.json');
|
|
64
97
|
if (fs.existsSync(global)) return global;
|
|
65
98
|
return null;
|
|
66
99
|
}
|
|
67
100
|
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
101
|
+
function resolveTestDataPathForTarget(cwd: string, target: DeliveryTarget): string {
|
|
102
|
+
return path.join(qaDir(cwd, target), 'test-data', `${target.name}.yaml`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function runPreflight(cwd: string, target: DeliveryTarget): PreflightCheck {
|
|
106
|
+
const base = qaDir(cwd, target);
|
|
107
|
+
const genBase = generatedDir(cwd, target);
|
|
108
|
+
const featureFile = path.join(base, 'features', `${target.name}.feature`);
|
|
109
|
+
const testDataFile = resolveTestDataPathForTarget(cwd, target);
|
|
110
|
+
const selectorsFile = path.join(base, 'selectors', `${target.name}.yaml`);
|
|
111
|
+
const specFile = path.join(genBase, `${target.name}.spec.ts`);
|
|
112
|
+
const resultsFile = resolveResultsPath(cwd, target);
|
|
74
113
|
|
|
75
114
|
const featureOk = checkFeatureReal(featureFile);
|
|
76
115
|
const testDataOk = checkTestDataHasVars(testDataFile);
|
|
77
|
-
const selectorsOk = checkSelectorsHasEntries(selectorsFile,
|
|
116
|
+
const selectorsOk = checkSelectorsHasEntries(selectorsFile, target.name);
|
|
78
117
|
const specOk = fs.existsSync(specFile);
|
|
79
118
|
const resultsOk = resultsFile !== null;
|
|
80
119
|
|
|
120
|
+
const label = target.isFlow ? `flow/${target.name}` : target.name;
|
|
81
121
|
const missing: string[] = [];
|
|
82
122
|
const suggestions: string[] = [];
|
|
83
123
|
|
|
84
124
|
if (!featureOk) {
|
|
85
125
|
missing.push(`feature file missing/empty: ${path.relative(cwd, featureFile)}`);
|
|
86
|
-
suggestions.push(`/sungen:create-test ${
|
|
126
|
+
suggestions.push(`/sungen:create-test ${target.name}`);
|
|
87
127
|
}
|
|
88
128
|
if (!testDataOk) {
|
|
89
129
|
missing.push(`test-data.yaml has no variables: ${path.relative(cwd, testDataFile)}`);
|
|
90
|
-
suggestions.push(`/sungen:create-test ${
|
|
130
|
+
suggestions.push(`/sungen:create-test ${target.name}`);
|
|
91
131
|
}
|
|
92
132
|
if (!selectorsOk) {
|
|
93
133
|
missing.push(`selectors.yaml missing entries: ${path.relative(cwd, selectorsFile)}`);
|
|
94
|
-
suggestions.push(`/sungen:run-test ${
|
|
134
|
+
suggestions.push(`/sungen:run-test ${target.name}`);
|
|
95
135
|
}
|
|
96
136
|
if (!specOk) {
|
|
97
137
|
missing.push(`compiled .spec.ts missing: ${path.relative(cwd, specFile)}`);
|
|
98
|
-
suggestions.push(`sungen generate --screen ${
|
|
138
|
+
suggestions.push(target.isFlow ? `sungen generate --flow ${target.name}` : `sungen generate --screen ${target.name}`);
|
|
99
139
|
}
|
|
100
140
|
if (!resultsOk) {
|
|
101
141
|
missing.push(`test-result.json missing (optional)`);
|
|
102
|
-
|
|
142
|
+
const genRel = path.relative(cwd, genBase);
|
|
143
|
+
suggestions.push(`PLAYWRIGHT_JSON_OUTPUT_NAME=${genRel}/${target.name}-test-result.json npx playwright test ${genRel}/${target.name}.spec.ts`);
|
|
103
144
|
}
|
|
104
145
|
|
|
105
146
|
return {
|
|
106
|
-
screen,
|
|
147
|
+
screen: label,
|
|
107
148
|
featureOk,
|
|
108
149
|
testDataOk,
|
|
109
150
|
selectorsOk,
|
|
@@ -178,16 +219,19 @@ function getEnvironment(cwd: string): EnvironmentInfo {
|
|
|
178
219
|
// Per-screen export
|
|
179
220
|
// ----------------------------------------------------------------------------
|
|
180
221
|
|
|
181
|
-
async function
|
|
222
|
+
async function exportTarget(
|
|
182
223
|
cwd: string,
|
|
183
|
-
|
|
224
|
+
target: DeliveryTarget,
|
|
184
225
|
env: EnvironmentInfo
|
|
185
226
|
): Promise<ScreenSummary | null> {
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const
|
|
227
|
+
const base = qaDir(cwd, target);
|
|
228
|
+
const genBase = generatedDir(cwd, target);
|
|
229
|
+
const featureFile = path.join(base, 'features', `${target.name}.feature`);
|
|
230
|
+
const testDataFile = resolveTestDataPathForTarget(cwd, target);
|
|
231
|
+
const specFile = path.join(genBase, `${target.name}.spec.ts`);
|
|
232
|
+
const resultsFile = resolveResultsPath(cwd, target);
|
|
233
|
+
const specMdFile = path.join(base, 'requirements', 'spec.md');
|
|
234
|
+
const label = target.isFlow ? `flow/${target.name}` : target.name;
|
|
191
235
|
|
|
192
236
|
try {
|
|
193
237
|
const feature = parseFeatureMetadata(featureFile);
|
|
@@ -197,7 +241,7 @@ async function exportScreen(
|
|
|
197
241
|
|
|
198
242
|
const merged = mergeFeatureAndSpec(feature, spec);
|
|
199
243
|
const rows = buildTestCaseRows({
|
|
200
|
-
screen,
|
|
244
|
+
screen: label,
|
|
201
245
|
featureName: feature.featureName,
|
|
202
246
|
merged,
|
|
203
247
|
testData,
|
|
@@ -206,14 +250,14 @@ async function exportScreen(
|
|
|
206
250
|
});
|
|
207
251
|
|
|
208
252
|
const specLink = fs.existsSync(specMdFile) ? path.relative(cwd, specMdFile) : '';
|
|
209
|
-
const tempSummary = buildSummary(
|
|
253
|
+
const tempSummary = buildSummary(label, rows, '');
|
|
210
254
|
const csv = renderCsv(tempSummary, rows, specLink);
|
|
211
|
-
const csvPath = writeCsv(cwd,
|
|
255
|
+
const csvPath = writeCsv(cwd, target.name, csv);
|
|
212
256
|
const wb = renderXlsx(tempSummary, rows, specLink);
|
|
213
|
-
await writeXlsx(cwd,
|
|
214
|
-
return buildSummary(
|
|
257
|
+
await writeXlsx(cwd, target.name, wb);
|
|
258
|
+
return buildSummary(label, rows, path.relative(cwd, csvPath));
|
|
215
259
|
} catch (err) {
|
|
216
|
-
console.error(`${COLOR.red}Error exporting ${
|
|
260
|
+
console.error(`${COLOR.red}Error exporting ${label}:${COLOR.reset} ${err instanceof Error ? err.message : err}`);
|
|
217
261
|
return null;
|
|
218
262
|
}
|
|
219
263
|
}
|
|
@@ -284,57 +328,64 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
284
328
|
program
|
|
285
329
|
.command('delivery')
|
|
286
330
|
.description('Export Gherkin + Playwright results → CSV test case deliverable')
|
|
287
|
-
.argument('[
|
|
331
|
+
.argument('[names...]', 'Specific screen or flow names. Omit to process all.')
|
|
288
332
|
.option('--skip-preflight', 'Skip pre-flight checks (not recommended)')
|
|
289
|
-
.option('--continue-on-missing', 'Skip
|
|
290
|
-
.action(async (
|
|
333
|
+
.option('--continue-on-missing', 'Skip targets with blocking misses instead of aborting')
|
|
334
|
+
.action(async (names: string[], options: { skipPreflight?: boolean; continueOnMissing?: boolean }) => {
|
|
291
335
|
try {
|
|
292
336
|
const cwd = process.cwd();
|
|
293
337
|
|
|
294
338
|
// 1. Scope detection
|
|
295
|
-
let
|
|
296
|
-
if (
|
|
297
|
-
|
|
339
|
+
let targets: DeliveryTarget[];
|
|
340
|
+
if (names && names.length > 0) {
|
|
341
|
+
targets = names.map((n) => resolveTargetType(cwd, n));
|
|
298
342
|
} else {
|
|
299
|
-
|
|
300
|
-
if (
|
|
301
|
-
console.error(`${COLOR.red}No screens found in qa/screens/${COLOR.reset}`);
|
|
343
|
+
targets = listAllTargets(cwd);
|
|
344
|
+
if (targets.length === 0) {
|
|
345
|
+
console.error(`${COLOR.red}No screens or flows found in qa/screens/ or qa/flows/${COLOR.reset}`);
|
|
302
346
|
process.exit(1);
|
|
303
347
|
}
|
|
304
348
|
}
|
|
305
349
|
|
|
306
|
-
|
|
350
|
+
const labels = targets.map((t) => t.isFlow ? `flow/${t.name}` : t.name);
|
|
351
|
+
log(`${COLOR.bold}sungen delivery${COLOR.reset} — exporting ${targets.length} target(s): ${labels.join(', ')}\n`);
|
|
307
352
|
|
|
308
353
|
// 2. Pre-flight
|
|
309
|
-
let toExport:
|
|
354
|
+
let toExport: DeliveryTarget[];
|
|
310
355
|
if (options.skipPreflight) {
|
|
311
|
-
toExport =
|
|
356
|
+
toExport = targets;
|
|
312
357
|
} else {
|
|
313
|
-
const checks =
|
|
358
|
+
const checks = targets.map((t) => runPreflight(cwd, t));
|
|
314
359
|
printPreflightTable(checks);
|
|
315
360
|
|
|
316
361
|
const blockers = checks.filter(hasBlockingMissing);
|
|
317
362
|
if (blockers.length > 0) {
|
|
318
363
|
if (options.continueOnMissing) {
|
|
319
|
-
|
|
320
|
-
|
|
364
|
+
const passedScreens = new Set(
|
|
365
|
+
checks.filter((c) => !hasBlockingMissing(c)).map((c) => c.screen)
|
|
366
|
+
);
|
|
367
|
+
toExport = targets.filter((t) => {
|
|
368
|
+
const label = t.isFlow ? `flow/${t.name}` : t.name;
|
|
369
|
+
return passedScreens.has(label);
|
|
370
|
+
});
|
|
371
|
+
log(`${COLOR.yellow}Continuing with ${toExport.length} ready target(s).${COLOR.reset}\n`);
|
|
321
372
|
} else {
|
|
322
373
|
console.error(
|
|
323
|
-
`${COLOR.red}Aborted:${COLOR.reset} ${blockers.length}
|
|
374
|
+
`${COLOR.red}Aborted:${COLOR.reset} ${blockers.length} target(s) have blocking issues.\n` +
|
|
324
375
|
`Run the suggested commands above, or use ${COLOR.cyan}--continue-on-missing${COLOR.reset} to skip them.`
|
|
325
376
|
);
|
|
326
377
|
process.exit(1);
|
|
327
378
|
}
|
|
328
379
|
} else {
|
|
329
|
-
toExport =
|
|
380
|
+
toExport = targets;
|
|
330
381
|
}
|
|
331
382
|
}
|
|
332
383
|
|
|
333
384
|
// 3. Export
|
|
334
385
|
const env = getEnvironment(cwd);
|
|
335
386
|
const summaries: ScreenSummary[] = [];
|
|
336
|
-
for (const
|
|
337
|
-
const s = await
|
|
387
|
+
for (const target of toExport) {
|
|
388
|
+
const s = await exportTarget(cwd, target, env);
|
|
338
389
|
if (s) summaries.push(s);
|
|
339
390
|
}
|
|
340
391
|
|
|
@@ -31,11 +31,20 @@ function findFeatureFilesForScreen(screenName: string): string[] {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Find
|
|
34
|
+
* Find feature files for a specific flow
|
|
35
|
+
*/
|
|
36
|
+
function findFeatureFilesForFlow(flowName: string): string[] {
|
|
37
|
+
const flowFeaturesDir = path.join(process.cwd(), 'qa', 'flows', flowName, 'features');
|
|
38
|
+
return findFeatureFiles(flowFeaturesDir);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find all feature files across all screens and flows
|
|
35
43
|
*/
|
|
36
44
|
function findAllFeatureFiles(): string[] {
|
|
37
45
|
const allFiles: string[] = [];
|
|
38
46
|
const screensDir = path.join(process.cwd(), 'qa', 'screens');
|
|
47
|
+
const flowsDir = path.join(process.cwd(), 'qa', 'flows');
|
|
39
48
|
|
|
40
49
|
if (fs.existsSync(screensDir)) {
|
|
41
50
|
const screenDirs = fs.readdirSync(screensDir, { withFileTypes: true })
|
|
@@ -48,6 +57,17 @@ function findAllFeatureFiles(): string[] {
|
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
if (fs.existsSync(flowsDir)) {
|
|
61
|
+
const flowDirs = fs.readdirSync(flowsDir, { withFileTypes: true })
|
|
62
|
+
.filter(entry => entry.isDirectory())
|
|
63
|
+
.map(entry => entry.name);
|
|
64
|
+
|
|
65
|
+
for (const flowName of flowDirs) {
|
|
66
|
+
const flowFeaturesDir = path.join(flowsDir, flowName, 'features');
|
|
67
|
+
allFiles.push(...findFeatureFiles(flowFeaturesDir));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
return allFiles;
|
|
52
72
|
}
|
|
53
73
|
|
|
@@ -56,15 +76,19 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
56
76
|
.command('generate')
|
|
57
77
|
.description('Generate Playwright test code from Gherkin features')
|
|
58
78
|
.option('-s, --screen <name>', 'Generate tests for a specific screen')
|
|
59
|
-
.option('--
|
|
79
|
+
.option('--flow <name>', 'Generate tests for a specific flow')
|
|
80
|
+
.option('--all', 'Generate tests for all screens and flows')
|
|
60
81
|
.option('--framework <name>', 'Test framework (default: playwright)', 'playwright')
|
|
61
82
|
.option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
|
|
62
83
|
.action(async (options) => {
|
|
63
84
|
try {
|
|
64
85
|
const screenName = options.screen;
|
|
86
|
+
const flowName = options.flow;
|
|
65
87
|
|
|
66
88
|
// Find feature files
|
|
67
89
|
let featureFiles: string[];
|
|
90
|
+
let qaSourceDir: string;
|
|
91
|
+
|
|
68
92
|
if (screenName) {
|
|
69
93
|
featureFiles = findFeatureFilesForScreen(screenName);
|
|
70
94
|
if (featureFiles.length === 0) {
|
|
@@ -73,13 +97,25 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
73
97
|
`Looked in: qa/screens/${screenName}/features/`
|
|
74
98
|
);
|
|
75
99
|
}
|
|
100
|
+
qaSourceDir = path.join(process.cwd(), 'qa', 'screens');
|
|
76
101
|
console.log(`\nGenerating tests: ${screenName}\n`);
|
|
102
|
+
} else if (flowName) {
|
|
103
|
+
featureFiles = findFeatureFilesForFlow(flowName);
|
|
104
|
+
if (featureFiles.length === 0) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`No feature files found for flow: ${flowName}\n` +
|
|
107
|
+
`Looked in: qa/flows/${flowName}/features/`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
qaSourceDir = path.join(process.cwd(), 'qa', 'flows');
|
|
111
|
+
console.log(`\nGenerating tests: flow/${flowName}\n`);
|
|
77
112
|
} else {
|
|
78
113
|
featureFiles = findAllFeatureFiles();
|
|
79
114
|
if (featureFiles.length === 0) {
|
|
80
|
-
throw new Error('No feature files found in qa/screens/');
|
|
115
|
+
throw new Error('No feature files found in qa/screens/ or qa/flows/');
|
|
81
116
|
}
|
|
82
|
-
|
|
117
|
+
qaSourceDir = path.join(process.cwd(), 'qa');
|
|
118
|
+
console.log(`\nGenerating tests for all screens and flows\n`);
|
|
83
119
|
}
|
|
84
120
|
|
|
85
121
|
// Output directory
|
|
@@ -88,13 +124,14 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
88
124
|
// Create generator and compile
|
|
89
125
|
const generator = new CodeGenerator({
|
|
90
126
|
framework: options.framework || 'playwright',
|
|
91
|
-
screenName,
|
|
127
|
+
screenName: screenName || flowName,
|
|
92
128
|
verbose: program.opts().verbose,
|
|
93
129
|
runtimeData: !options.inlineData,
|
|
130
|
+
flowMode: !!flowName,
|
|
94
131
|
});
|
|
95
132
|
|
|
96
133
|
const results = await generator.generateAllTests(
|
|
97
|
-
|
|
134
|
+
qaSourceDir,
|
|
98
135
|
outputDir,
|
|
99
136
|
featureFiles
|
|
100
137
|
);
|
package/src/cli/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { registerMakeauthCommand } from './commands/makeauth';
|
|
|
12
12
|
import { registerUpdateCommand } from './commands/update';
|
|
13
13
|
import { registerDeliveryCommand } from './commands/delivery';
|
|
14
14
|
import { registerFigmaCommand } from './commands/figma';
|
|
15
|
+
import { registerAddFlowCommand } from './commands/add-flow';
|
|
15
16
|
|
|
16
17
|
async function main() {
|
|
17
18
|
const program = new Command();
|
|
@@ -19,7 +20,7 @@ async function main() {
|
|
|
19
20
|
program
|
|
20
21
|
.name('sungen')
|
|
21
22
|
.description('Deterministic E2E Test Compiler — Gherkin + Selectors → Playwright')
|
|
22
|
-
.version('2.
|
|
23
|
+
.version('2.6.0');
|
|
23
24
|
|
|
24
25
|
// Global options
|
|
25
26
|
program
|
|
@@ -33,6 +34,7 @@ async function main() {
|
|
|
33
34
|
registerUpdateCommand(program);
|
|
34
35
|
registerDeliveryCommand(program);
|
|
35
36
|
registerFigmaCommand(program);
|
|
37
|
+
registerAddFlowCommand(program);
|
|
36
38
|
|
|
37
39
|
await program.parseAsync(process.argv);
|
|
38
40
|
}
|
|
@@ -57,7 +57,7 @@ export interface TestGeneratorAdapter {
|
|
|
57
57
|
// Template rendering methods
|
|
58
58
|
renderTestFile(data: TestFileData): string;
|
|
59
59
|
renderScenario(data: ScenarioData): string;
|
|
60
|
-
renderImports(options?: { runtimeData?: boolean }): string;
|
|
60
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string }): string;
|
|
61
61
|
renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
62
62
|
renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
63
63
|
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 }): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|