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