@sun-asterisk/sungen 2.5.2 → 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 +16 -15
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +21 -17
- 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 +16 -15
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-review.md +9 -7
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-run-test.md +21 -17
- 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,6 +1,6 @@
|
|
|
1
|
-
import { test, expect } from '
|
|
1
|
+
import { test, expect } from '{{basePath}}/base';
|
|
2
2
|
{{#if runtimeData}}
|
|
3
|
-
import { TestDataLoader } from '
|
|
3
|
+
import { TestDataLoader } from '{{basePath}}/test-data';
|
|
4
4
|
{{/if}}
|
|
5
5
|
|
|
6
6
|
// This file is auto-generated from Gherkin feature files
|
|
@@ -1 +1 @@
|
|
|
1
|
-
page.
|
|
1
|
+
page.on('dialog', dialog => dialog.accept());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
page.
|
|
1
|
+
page.on('dialog', dialog => dialog.dismiss());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
page.
|
|
1
|
+
page.on('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const dialogPromise{{stepCounter}} = new Promise<string>(resolve => {
|
|
2
|
-
page.
|
|
2
|
+
page.on('dialog', dialog => {
|
|
3
3
|
resolve(dialog.message());
|
|
4
4
|
dialog.accept();
|
|
5
5
|
});
|
|
6
6
|
});
|
|
7
7
|
// Trigger the dialog action, then:
|
|
8
|
-
// expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
|
|
8
|
+
// expect(await dialogPromise{{stepCounter}}).toContain('{{escapeQuotes dataValue}}');
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
await expect({{> locator}}.getByRole('columnheader', { name: '{{columnName}}' })).toBeVisible();
|
|
1
2
|
const {{columnIndexVar}} = (await page.getByRole('columnheader').allTextContents()).findIndex(h => h.includes('{{columnName}}'));
|
|
2
3
|
await expect(page.getByRole('row').nth({{rowNth}}).getByRole('cell').nth({{columnIndexVar}})).toHaveText('{{dataValue}}');
|
package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/navigation.hbs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
|
|
1
|
+
await page.goto('{{#if baseURL}}{{baseURL}}{{/if}}{{path}}', { waitUntil: 'load' });
|
|
2
|
+
await page.waitForLoadState('domcontentloaded');
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
await page.
|
|
2
|
-
expect(page.url()).toContain('{{path}}');
|
|
1
|
+
await expect(page).toHaveURL(/{{escapeRegex path}}/);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
await page.
|
|
1
|
+
await page.waitForLoadState('networkidle');
|
|
@@ -134,7 +134,7 @@ export class CodeGenerator {
|
|
|
134
134
|
// Steps registry built per feature during generateTestCode(); used by countSteps()
|
|
135
135
|
private stepsRegistry = new Map<string, ParsedScenario>();
|
|
136
136
|
|
|
137
|
-
constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean } = {}) {
|
|
137
|
+
constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean; flowMode?: boolean } = {}) {
|
|
138
138
|
this.options = options;
|
|
139
139
|
this.screenName = options.screenName;
|
|
140
140
|
this.stepMapper = new StepMapper(options);
|
|
@@ -165,25 +165,33 @@ export class CodeGenerator {
|
|
|
165
165
|
fileName = this.featureNameToFileName(feature.name);
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
// Extract screen name from source file path
|
|
168
|
+
// Extract screen/flow name from source file path for output subdirectory
|
|
169
169
|
// qa/screens/{screenName}/features/{featureName}.feature -> screenName
|
|
170
|
-
|
|
170
|
+
// qa/flows/{flowName}/features/{featureName}.feature -> flows/flowName
|
|
171
|
+
let outputSubdir = '';
|
|
171
172
|
if (feature.sourceFile) {
|
|
172
173
|
const sourceDir = path.dirname(feature.sourceFile);
|
|
173
174
|
const parts = sourceDir.split(path.sep);
|
|
175
|
+
const flowsIndex = parts.indexOf('flows');
|
|
174
176
|
const screensIndex = parts.indexOf('screens');
|
|
175
|
-
if (
|
|
176
|
-
|
|
177
|
+
if (flowsIndex >= 0 && flowsIndex < parts.length - 2) {
|
|
178
|
+
outputSubdir = path.join('flows', parts[flowsIndex + 1]);
|
|
179
|
+
} else if (screensIndex >= 0 && screensIndex < parts.length - 2) {
|
|
180
|
+
outputSubdir = parts[screensIndex + 1];
|
|
177
181
|
}
|
|
178
182
|
}
|
|
179
|
-
|
|
180
|
-
// Build output path with
|
|
181
|
-
const filePath =
|
|
182
|
-
? path.join(outputDir,
|
|
183
|
+
|
|
184
|
+
// Build output path with subdirectory
|
|
185
|
+
const filePath = outputSubdir
|
|
186
|
+
? path.join(outputDir, outputSubdir, fileName)
|
|
183
187
|
: path.join(outputDir, fileName);
|
|
184
188
|
|
|
189
|
+
// Compute relative path from output file back to specs/generated/
|
|
190
|
+
const depth = outputSubdir ? outputSubdir.split(path.sep).length : 0;
|
|
191
|
+
const basePath = depth > 0 ? Array(depth).fill('..').join('/') : '..';
|
|
192
|
+
|
|
185
193
|
// Generate imports using adapter
|
|
186
|
-
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData });
|
|
194
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath });
|
|
187
195
|
|
|
188
196
|
// Generate test code (async now to support AI mapping)
|
|
189
197
|
const testCode = await this.generateTestCode(feature);
|
|
@@ -278,19 +286,29 @@ export class CodeGenerator {
|
|
|
278
286
|
featureName = this.featureNameToFileName(feature.name).replace('.spec.ts', '');
|
|
279
287
|
}
|
|
280
288
|
|
|
281
|
-
// Derive screen name from source file path when not explicitly set
|
|
289
|
+
// Derive screen/flow name from source file path when not explicitly set
|
|
282
290
|
// qa/screens/{screenName}/features/{featureName}.feature -> screenName
|
|
291
|
+
// qa/flows/{flowName}/features/{featureName}.feature -> flowName
|
|
283
292
|
let effectiveScreenName = this.screenName;
|
|
293
|
+
let isFlowFeature = !!this.options.flowMode;
|
|
284
294
|
if (!this.screenName && feature.sourceFile) {
|
|
285
295
|
const sourceDir = path.dirname(feature.sourceFile);
|
|
286
296
|
const parts = sourceDir.split(path.sep);
|
|
297
|
+
const flowsIndex = parts.indexOf('flows');
|
|
287
298
|
const screensIndex = parts.indexOf('screens');
|
|
288
|
-
if (
|
|
299
|
+
if (flowsIndex >= 0 && flowsIndex < parts.length - 2) {
|
|
300
|
+
effectiveScreenName = parts[flowsIndex + 1];
|
|
301
|
+
isFlowFeature = true;
|
|
302
|
+
} else if (screensIndex >= 0 && screensIndex < parts.length - 2) {
|
|
289
303
|
effectiveScreenName = parts[screensIndex + 1];
|
|
290
|
-
|
|
304
|
+
isFlowFeature = false;
|
|
291
305
|
}
|
|
306
|
+
this.stepMapper.setScreenContext(effectiveScreenName);
|
|
292
307
|
}
|
|
293
308
|
|
|
309
|
+
// Reset flow mode per feature to prevent state leak in --all mode
|
|
310
|
+
this.stepMapper.setFlowMode(isFlowFeature);
|
|
311
|
+
|
|
294
312
|
// Set feature context for data resolution and navigation
|
|
295
313
|
this.stepMapper.setFeatureContext(featureName, feature.path);
|
|
296
314
|
|
|
@@ -419,7 +437,7 @@ export class CodeGenerator {
|
|
|
419
437
|
cleanupConfig,
|
|
420
438
|
screenshotOnFailure,
|
|
421
439
|
runtimeData: this.options.runtimeData,
|
|
422
|
-
screenName: effectiveScreenName,
|
|
440
|
+
screenName: isFlowFeature ? `flows/${effectiveScreenName}` : effectiveScreenName,
|
|
423
441
|
featureFileName: featureName,
|
|
424
442
|
scenarios: needsGrouping ? [] : scenarios,
|
|
425
443
|
authGroups: needsGrouping ? authGroups : undefined,
|
|
@@ -71,6 +71,14 @@ export class StepMapper {
|
|
|
71
71
|
this.dataResolver.setScreenContext(screenName);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Enable flow mode — colon in selector refs is a namespace separator, not cross-screen
|
|
76
|
+
*/
|
|
77
|
+
setFlowMode(enabled: boolean): void {
|
|
78
|
+
this.selectorResolver.setFlowMode(enabled);
|
|
79
|
+
this.dataResolver.setFlowMode(enabled);
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
/**
|
|
75
83
|
* Set scenario context for path variable resolution
|
|
76
84
|
*/
|
|
@@ -229,8 +229,8 @@ export class TemplateEngine {
|
|
|
229
229
|
this.baseContext = {};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
renderImports(options?: { runtimeData?: boolean }): string {
|
|
233
|
-
return this.render('imports', { runtimeData: options?.runtimeData });
|
|
232
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string }): string {
|
|
233
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..' });
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
renderTestFile(data: {
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import * as fs from 'fs';
|
|
2
1
|
import * as path from 'path';
|
|
3
|
-
import yaml from 'yaml';
|
|
4
2
|
import { readYamlIfExists } from '../../../utils/yaml-io';
|
|
5
3
|
|
|
6
4
|
/**
|
|
@@ -9,17 +7,13 @@ import { readYamlIfExists } from '../../../utils/yaml-io';
|
|
|
9
7
|
* Two modes:
|
|
10
8
|
* - Compile-time (default): resolves YAML values and bakes them into generated code
|
|
11
9
|
* - Runtime (runtimeMode=true): returns markers that post-processor converts to testData.get() calls
|
|
12
|
-
*
|
|
13
|
-
* Supports override files with priority:
|
|
14
|
-
* 1. .override.yaml (highest - user customizations)
|
|
15
|
-
* 2. -override.yaml (legacy - backward compat)
|
|
16
|
-
* 3. .yaml (lowest - auto-generated base)
|
|
17
10
|
*/
|
|
18
11
|
export class DataResolver {
|
|
19
12
|
private dataCache = new Map<string, any>();
|
|
20
13
|
private testDataDir: string;
|
|
21
14
|
private screenName?: string;
|
|
22
15
|
private runtimeMode: boolean;
|
|
16
|
+
private flowMode: boolean = false;
|
|
23
17
|
|
|
24
18
|
constructor(testDataDir?: string, screenName?: string, runtimeMode: boolean = false) {
|
|
25
19
|
this.testDataDir = testDataDir || path.join(process.cwd(), 'qa', 'test-data');
|
|
@@ -27,13 +21,14 @@ export class DataResolver {
|
|
|
27
21
|
this.runtimeMode = runtimeMode;
|
|
28
22
|
}
|
|
29
23
|
|
|
30
|
-
/**
|
|
31
|
-
* Set screen context for data resolution
|
|
32
|
-
*/
|
|
33
24
|
setScreenContext(screenName: string): void {
|
|
34
25
|
this.screenName = screenName;
|
|
35
26
|
}
|
|
36
27
|
|
|
28
|
+
setFlowMode(enabled: boolean): void {
|
|
29
|
+
this.flowMode = enabled;
|
|
30
|
+
}
|
|
31
|
+
|
|
37
32
|
/**
|
|
38
33
|
* Resolve data reference to actual value
|
|
39
34
|
* @param dataRef - Format: email.valid (path.to.value without filename)
|
|
@@ -107,89 +102,42 @@ export class DataResolver {
|
|
|
107
102
|
|
|
108
103
|
/**
|
|
109
104
|
* Load data file from disk (with caching)
|
|
110
|
-
*
|
|
105
|
+
* Merges base + env-specific: {name}.yaml + {name}.{SUNGEN_ENV}.yaml
|
|
111
106
|
*/
|
|
112
107
|
private loadDataFile(fileName: string): any {
|
|
113
108
|
if (this.dataCache.has(fileName)) {
|
|
114
109
|
return this.dataCache.get(fileName)!;
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let merged: any = {};
|
|
120
|
-
let found = false;
|
|
121
|
-
|
|
122
|
-
// Load in reverse priority order (base first, then overrides on top)
|
|
123
|
-
for (const p of [...possiblePaths].reverse()) {
|
|
124
|
-
const data = readYamlIfExists(p);
|
|
125
|
-
if (data && typeof data === 'object' && Object.keys(data).length > 0) {
|
|
126
|
-
merged = { ...merged, ...data };
|
|
127
|
-
found = true;
|
|
128
|
-
}
|
|
112
|
+
if (!this.screenName) {
|
|
113
|
+
throw new Error(`Data resolution requires screen/flow context for "${fileName}"`);
|
|
129
114
|
}
|
|
130
115
|
|
|
131
|
-
|
|
116
|
+
const subDir = this.flowMode ? 'flows' : 'screens';
|
|
117
|
+
const baseDir = path.join(process.cwd(), 'qa', subDir, this.screenName, 'test-data');
|
|
118
|
+
const basePath = path.join(baseDir, `${fileName}.yaml`);
|
|
119
|
+
const data = readYamlIfExists(basePath);
|
|
120
|
+
|
|
121
|
+
if (!data || typeof data !== 'object' || Object.keys(data).length === 0) {
|
|
132
122
|
throw new Error(
|
|
133
|
-
`Data file not found
|
|
123
|
+
`Data file not found or empty: qa/${subDir}/${this.screenName}/test-data/${fileName}.yaml`
|
|
134
124
|
);
|
|
135
125
|
}
|
|
136
126
|
|
|
137
|
-
|
|
138
|
-
return merged;
|
|
139
|
-
}
|
|
127
|
+
let merged = { ...data };
|
|
140
128
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
for (const p of possiblePaths) {
|
|
150
|
-
const data = readYamlIfExists(p);
|
|
151
|
-
if (data !== null) {
|
|
152
|
-
// Skip if file is empty or only contains null/undefined
|
|
153
|
-
if (data === undefined || (typeof data === 'object' && Object.keys(data).length === 0)) {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Emit deprecation warning for old -override.yaml pattern
|
|
158
|
-
if (p.includes('-override.yaml')) {
|
|
159
|
-
console.warn(`⚠️ Deprecated: ${path.basename(p)}`);
|
|
160
|
-
console.warn(` Rename to: ${fileName}.override.yaml for better compatibility`);
|
|
161
|
-
}
|
|
162
|
-
return p;
|
|
129
|
+
// Merge env-specific data: {name}.{SUNGEN_ENV}.yaml
|
|
130
|
+
const env = process.env.SUNGEN_ENV;
|
|
131
|
+
if (env) {
|
|
132
|
+
const envPath = path.join(baseDir, `${fileName}.${env}.yaml`);
|
|
133
|
+
const envData = readYamlIfExists(envPath);
|
|
134
|
+
if (envData && typeof envData === 'object') {
|
|
135
|
+
merged = { ...merged, ...envData };
|
|
163
136
|
}
|
|
164
137
|
}
|
|
165
|
-
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
138
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
* Priority: .override.yaml > -override.yaml > base .yaml
|
|
172
|
-
*/
|
|
173
|
-
private getPossibleDataPaths(fileName: string): string[] {
|
|
174
|
-
const paths: string[] = [];
|
|
175
|
-
|
|
176
|
-
// New structure: qa/screens/{screenName}/test-data/
|
|
177
|
-
if (this.screenName) {
|
|
178
|
-
const cwd = process.cwd();
|
|
179
|
-
const basePath = path.join(cwd, 'qa', 'screens', this.screenName, 'test-data');
|
|
180
|
-
|
|
181
|
-
// Priority 1: New .override.yaml pattern (highest)
|
|
182
|
-
paths.push(path.join(basePath, `${fileName}.override.yaml`));
|
|
183
|
-
|
|
184
|
-
// Priority 2: Old -override.yaml pattern (backward compat)
|
|
185
|
-
paths.push(path.join(basePath, `${fileName}-override.yaml`));
|
|
186
|
-
|
|
187
|
-
// Priority 3: Base file (lowest)
|
|
188
|
-
paths.push(path.join(basePath, `${fileName}.yaml`));
|
|
189
|
-
paths.push(path.join(basePath, `${fileName}.yml`));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return paths;
|
|
139
|
+
this.dataCache.set(fileName, merged);
|
|
140
|
+
return merged;
|
|
193
141
|
}
|
|
194
142
|
|
|
195
143
|
/**
|
|
@@ -82,12 +82,17 @@ export class SelectorResolver {
|
|
|
82
82
|
private selectorsDir: string;
|
|
83
83
|
private featureName?: string;
|
|
84
84
|
private screenName?: string;
|
|
85
|
+
private flowMode: boolean = false;
|
|
85
86
|
|
|
86
87
|
constructor(selectorsDir?: string, screenName?: string) {
|
|
87
88
|
this.selectorsDir = selectorsDir || path.join(process.cwd(), 'qa', 'selectors', 'screens');
|
|
88
89
|
this.screenName = screenName;
|
|
89
90
|
}
|
|
90
91
|
|
|
92
|
+
setFlowMode(enabled: boolean): void {
|
|
93
|
+
this.flowMode = enabled;
|
|
94
|
+
}
|
|
95
|
+
|
|
91
96
|
/**
|
|
92
97
|
* Set screen context for base selector file lookup
|
|
93
98
|
*/
|
|
@@ -160,9 +165,15 @@ export class SelectorResolver {
|
|
|
160
165
|
// Check if it's legacy format (contains : or has dot with screen prefix)
|
|
161
166
|
// Legacy: "login:email_field" or "login.email_field"
|
|
162
167
|
// Natural: "Email Address", "Password", "Submit Button"
|
|
168
|
+
// Flow namespaced: "Login:Submit" → key "login:submit" in flow's own YAML
|
|
163
169
|
const hasColonSeparator = selectorRef.includes(':');
|
|
164
170
|
const hasDotSeparator = selectorRef.includes('.');
|
|
165
|
-
|
|
171
|
+
|
|
172
|
+
// Flow mode: colon is a namespace separator, resolve as natural language key
|
|
173
|
+
if (hasColonSeparator && this.flowMode) {
|
|
174
|
+
return this.resolveNaturalLanguage(selectorRef, contextName!, elementType, nth);
|
|
175
|
+
}
|
|
176
|
+
|
|
166
177
|
// If it has colon, it's definitely legacy
|
|
167
178
|
if (hasColonSeparator) {
|
|
168
179
|
return this.resolveLegacyFormat(selectorRef);
|
|
@@ -601,7 +612,7 @@ export class SelectorResolver {
|
|
|
601
612
|
|
|
602
613
|
const selectors: SelectorFile = {};
|
|
603
614
|
|
|
604
|
-
// 1. Try load base file (screen-level shared selectors) - OPTIONAL
|
|
615
|
+
// 1. Try load base file (screen/flow-level shared selectors) - OPTIONAL
|
|
605
616
|
if (this.screenName) {
|
|
606
617
|
const basePath = this.findBaseSelectorPath(this.screenName);
|
|
607
618
|
if (basePath) {
|
|
@@ -610,21 +621,11 @@ export class SelectorResolver {
|
|
|
610
621
|
Object.assign(selectors, SelectorResolver.normalizeKeys(baseSelectors));
|
|
611
622
|
}
|
|
612
623
|
}
|
|
613
|
-
|
|
614
|
-
// Load base override file
|
|
615
|
-
const baseOverridePath = this.findOverrideSelectorPath(this.screenName);
|
|
616
|
-
if (baseOverridePath) {
|
|
617
|
-
const overrideSelectors = readYamlIfExists<SelectorFile>(baseOverridePath);
|
|
618
|
-
if (overrideSelectors) {
|
|
619
|
-
Object.assign(selectors, SelectorResolver.normalizeKeys(overrideSelectors));
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
624
|
}
|
|
623
625
|
|
|
624
626
|
// 2. Load feature-specific selectors - REQUIRED
|
|
625
627
|
const featurePath = this.findBaseSelectorPath(featureName);
|
|
626
628
|
if (!featurePath) {
|
|
627
|
-
// If no base file, that's OK if we have base from screen
|
|
628
629
|
if (Object.keys(selectors).length === 0) {
|
|
629
630
|
throw new Error(
|
|
630
631
|
`Selector file not found for feature "${featureName}". ` +
|
|
@@ -638,15 +639,6 @@ export class SelectorResolver {
|
|
|
638
639
|
}
|
|
639
640
|
}
|
|
640
641
|
|
|
641
|
-
// 3. Load feature override file
|
|
642
|
-
const featureOverridePath = this.findOverrideSelectorPath(featureName);
|
|
643
|
-
if (featureOverridePath) {
|
|
644
|
-
const overrideSelectors = readYamlIfExists<SelectorFile>(featureOverridePath);
|
|
645
|
-
if (overrideSelectors) {
|
|
646
|
-
Object.assign(selectors, SelectorResolver.normalizeKeys(overrideSelectors));
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
642
|
this.selectorCache.set(cacheKey, selectors);
|
|
651
643
|
return selectors;
|
|
652
644
|
}
|
|
@@ -657,72 +649,25 @@ export class SelectorResolver {
|
|
|
657
649
|
private findBaseSelectorPath(name: string): string | null {
|
|
658
650
|
const possiblePaths: string[] = [];
|
|
659
651
|
|
|
660
|
-
// New structure: qa/screens/{screenName}/selectors/
|
|
652
|
+
// New structure: qa/screens/{screenName}/selectors/ or qa/flows/{flowName}/selectors/
|
|
661
653
|
const qaDir = path.dirname(path.dirname(this.selectorsDir));
|
|
662
|
-
|
|
663
|
-
if (this.screenName) {
|
|
664
|
-
// Priority: Base .yaml only
|
|
665
|
-
possiblePaths.push(
|
|
666
|
-
path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}.yaml`)
|
|
667
|
-
);
|
|
668
|
-
} else {
|
|
669
|
-
possiblePaths.push(
|
|
670
|
-
path.join(qaDir, 'screens', name, 'selectors', `${name}.yaml`)
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Old structure
|
|
675
|
-
possiblePaths.push(path.join(this.selectorsDir, `${name}.yaml`));
|
|
676
|
-
|
|
677
|
-
for (const p of possiblePaths) {
|
|
678
|
-
if (fs.existsSync(p)) {
|
|
679
|
-
return p;
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
return null;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Find OVERRIDE selector file path
|
|
688
|
-
* Priority: .override.yaml > -override.yaml
|
|
689
|
-
*/
|
|
690
|
-
private findOverrideSelectorPath(name: string): string | null {
|
|
691
|
-
const possiblePaths: string[] = [];
|
|
654
|
+
const subDir = this.flowMode ? 'flows' : 'screens';
|
|
692
655
|
|
|
693
|
-
// New structure: qa/screens/{screenName}/selectors/
|
|
694
|
-
const qaDir = path.dirname(path.dirname(this.selectorsDir));
|
|
695
|
-
|
|
696
656
|
if (this.screenName) {
|
|
697
|
-
// Priority 1: New .override.yaml
|
|
698
657
|
possiblePaths.push(
|
|
699
|
-
path.join(qaDir,
|
|
700
|
-
);
|
|
701
|
-
|
|
702
|
-
// Priority 2: Old -override.yaml
|
|
703
|
-
possiblePaths.push(
|
|
704
|
-
path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}-override.yaml`)
|
|
658
|
+
path.join(qaDir, subDir, this.screenName, 'selectors', `${name}.yaml`)
|
|
705
659
|
);
|
|
706
660
|
} else {
|
|
707
661
|
possiblePaths.push(
|
|
708
|
-
path.join(qaDir,
|
|
709
|
-
);
|
|
710
|
-
possiblePaths.push(
|
|
711
|
-
path.join(qaDir, 'screens', name, 'selectors', `${name}-override.yaml`)
|
|
662
|
+
path.join(qaDir, subDir, name, 'selectors', `${name}.yaml`)
|
|
712
663
|
);
|
|
713
664
|
}
|
|
714
665
|
|
|
715
666
|
// Old structure
|
|
716
|
-
possiblePaths.push(path.join(this.selectorsDir, `${name}.
|
|
717
|
-
possiblePaths.push(path.join(this.selectorsDir, `${name}-override.yaml`));
|
|
667
|
+
possiblePaths.push(path.join(this.selectorsDir, `${name}.yaml`));
|
|
718
668
|
|
|
719
669
|
for (const p of possiblePaths) {
|
|
720
670
|
if (fs.existsSync(p)) {
|
|
721
|
-
// Emit deprecation warning for old -override.yaml pattern
|
|
722
|
-
if (p.includes('-override.yaml')) {
|
|
723
|
-
console.warn(`⚠️ Deprecated: ${path.basename(p)}`);
|
|
724
|
-
console.warn(` Rename to: ${name}.override.yaml for better compatibility`);
|
|
725
|
-
}
|
|
726
671
|
return p;
|
|
727
672
|
}
|
|
728
673
|
}
|
|
@@ -732,59 +677,28 @@ export class SelectorResolver {
|
|
|
732
677
|
|
|
733
678
|
/**
|
|
734
679
|
* Find selector file path (checks both old and new structures)
|
|
735
|
-
* Priority: .override.yaml > -override.yaml > base .yaml
|
|
736
680
|
*/
|
|
737
681
|
private findSelectorPath(name: string): string | null {
|
|
738
|
-
// Build list of possible paths (priority order)
|
|
739
682
|
const possiblePaths: string[] = [];
|
|
740
683
|
|
|
741
|
-
|
|
742
|
-
const
|
|
743
|
-
|
|
684
|
+
const qaDir = path.dirname(path.dirname(this.selectorsDir));
|
|
685
|
+
const subDir = this.flowMode ? 'flows' : 'screens';
|
|
686
|
+
|
|
744
687
|
if (this.screenName) {
|
|
745
|
-
// Priority 1: New .override.yaml pattern (highest)
|
|
746
688
|
possiblePaths.push(
|
|
747
|
-
path.join(qaDir,
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
// Priority 2: Old -override.yaml pattern (backward compat)
|
|
751
|
-
possiblePaths.push(
|
|
752
|
-
path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}-override.yaml`)
|
|
753
|
-
);
|
|
754
|
-
|
|
755
|
-
// Priority 3: Base file (lowest)
|
|
756
|
-
possiblePaths.push(
|
|
757
|
-
path.join(qaDir, 'screens', this.screenName, 'selectors', `${name}.yaml`)
|
|
689
|
+
path.join(qaDir, subDir, this.screenName, 'selectors', `${name}.yaml`)
|
|
758
690
|
);
|
|
759
691
|
} else {
|
|
760
|
-
// Fallback: assume name is both screen and feature
|
|
761
|
-
possiblePaths.push(
|
|
762
|
-
path.join(qaDir, 'screens', name, 'selectors', `${name}.override.yaml`)
|
|
763
|
-
);
|
|
764
692
|
possiblePaths.push(
|
|
765
|
-
path.join(qaDir,
|
|
766
|
-
);
|
|
767
|
-
possiblePaths.push(
|
|
768
|
-
path.join(qaDir, 'screens', name, 'selectors', `${name}.yaml`)
|
|
693
|
+
path.join(qaDir, subDir, name, 'selectors', `${name}.yaml`)
|
|
769
694
|
);
|
|
770
695
|
}
|
|
771
696
|
|
|
772
697
|
// Old structure: qa/selectors/screens/
|
|
773
|
-
// Priority 1: New .override.yaml
|
|
774
|
-
possiblePaths.push(path.join(this.selectorsDir, `${name}.override.yaml`));
|
|
775
|
-
// Priority 2: Old -override.yaml
|
|
776
|
-
possiblePaths.push(path.join(this.selectorsDir, `${name}-override.yaml`));
|
|
777
|
-
// Priority 3: Base
|
|
778
698
|
possiblePaths.push(path.join(this.selectorsDir, `${name}.yaml`));
|
|
779
699
|
|
|
780
|
-
// Find first existing file and emit deprecation warning for old pattern
|
|
781
700
|
for (const p of possiblePaths) {
|
|
782
701
|
if (fs.existsSync(p)) {
|
|
783
|
-
// Emit deprecation warning for old -override.yaml pattern
|
|
784
|
-
if (p.includes('-override.yaml')) {
|
|
785
|
-
console.warn(`⚠️ Deprecated: ${path.basename(p)}`);
|
|
786
|
-
console.warn(` Rename to: ${name}.override.yaml for better compatibility`);
|
|
787
|
-
}
|
|
788
702
|
return p;
|
|
789
703
|
}
|
|
790
704
|
}
|
|
@@ -16,6 +16,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
16
16
|
|
|
17
17
|
// Commands — Claude Code
|
|
18
18
|
['claude-cmd-add-screen.md', '.claude/commands/sungen/add-screen.md'],
|
|
19
|
+
['claude-cmd-add-flow.md', '.claude/commands/sungen/add-flow.md'],
|
|
19
20
|
['claude-cmd-create-test.md', '.claude/commands/sungen/create-test.md'],
|
|
20
21
|
['claude-cmd-run-test.md', '.claude/commands/sungen/run-test.md'],
|
|
21
22
|
['claude-cmd-review.md', '.claude/commands/sungen/review.md'],
|
|
@@ -23,6 +24,7 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
23
24
|
|
|
24
25
|
// Commands — GitHub Copilot
|
|
25
26
|
['copilot-cmd-add-screen.md', '.github/prompts/sungen-add-screen.prompt.md'],
|
|
27
|
+
['copilot-cmd-add-flow.md', '.github/prompts/sungen-add-flow.prompt.md'],
|
|
26
28
|
['copilot-cmd-create-test.md', '.github/prompts/sungen-create-test.prompt.md'],
|
|
27
29
|
['copilot-cmd-run-test.md', '.github/prompts/sungen-run-test.prompt.md'],
|
|
28
30
|
['copilot-cmd-review.md', '.github/prompts/sungen-review.prompt.md'],
|
|
@@ -43,6 +45,9 @@ export const AI_RULES_FILE_MAPPING: [string, string][] = [
|
|
|
43
45
|
['claude-skill-capture-live.md', '.claude/skills/sungen-capture-live/SKILL.md'],
|
|
44
46
|
['claude-skill-figma-source.md', '.claude/skills/sungen-figma-source/SKILL.md'],
|
|
45
47
|
|
|
48
|
+
// Skills — Copilot (prompt-based)
|
|
49
|
+
['copilot-skill-figma-source.md', '.github/prompts/sungen-figma-source.prompt.md'],
|
|
50
|
+
|
|
46
51
|
// Skills — GitHub Copilot
|
|
47
52
|
['github-skill-sungen-gherkin-syntax.md', '.github/skills/sungen-gherkin-syntax/SKILL.md'],
|
|
48
53
|
['github-skill-sungen-selector-keys.md', '.github/skills/sungen-selector-keys/SKILL.md'],
|