@sun-asterisk/sungen 3.1.0 → 3.1.2-beta.100
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/README.md +4 -428
- package/dist/capabilities/builtins.d.ts +31 -0
- package/dist/capabilities/builtins.d.ts.map +1 -0
- package/dist/capabilities/builtins.js +84 -0
- package/dist/capabilities/builtins.js.map +1 -0
- package/dist/capabilities/context-router.d.ts +34 -0
- package/dist/capabilities/context-router.d.ts.map +1 -0
- package/dist/capabilities/context-router.js +49 -0
- package/dist/capabilities/context-router.js.map +1 -0
- package/dist/capabilities/context.d.ts +51 -0
- package/dist/capabilities/context.d.ts.map +1 -0
- package/dist/capabilities/context.js +17 -0
- package/dist/capabilities/context.js.map +1 -0
- package/dist/capabilities/discover.d.ts +2 -0
- package/dist/capabilities/discover.d.ts.map +1 -0
- package/dist/capabilities/discover.js +48 -0
- package/dist/capabilities/discover.js.map +1 -0
- package/dist/capabilities/registry.d.ts +90 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +43 -0
- package/dist/capabilities/registry.js.map +1 -0
- package/dist/capabilities/sensor.d.ts +49 -0
- package/dist/capabilities/sensor.d.ts.map +1 -0
- package/dist/capabilities/sensor.js +3 -0
- package/dist/capabilities/sensor.js.map +1 -0
- package/dist/cli/commands/challenge.d.ts.map +1 -1
- package/dist/cli/commands/challenge.js +9 -2
- package/dist/cli/commands/challenge.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +3 -2
- 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 +12 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/cli/index.js +10 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +92 -76
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +6 -1
- package/dist/exporters/spec-parser.js.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +2 -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 +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
- package/dist/generators/test-generator/code-generator.d.ts +21 -4
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +118 -57
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts +3 -0
- package/dist/generators/test-generator/patterns/expect-patterns.d.ts.map +1 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js +54 -0
- package/dist/generators/test-generator/patterns/expect-patterns.js.map +1 -0
- package/dist/generators/test-generator/patterns/index.d.ts +0 -10
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +10 -45
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +6 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +8 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +4 -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/runtime-data-transformer.d.ts +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +5 -5
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/harness/annotation-overrides.d.ts +9 -0
- package/dist/harness/annotation-overrides.d.ts.map +1 -0
- package/dist/harness/annotation-overrides.js +36 -0
- package/dist/harness/annotation-overrides.js.map +1 -0
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +35 -7
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +35 -12
- package/dist/harness/challenge.d.ts +1 -0
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +49 -2
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts +7 -0
- package/dist/harness/data-driven-lint.d.ts.map +1 -0
- package/dist/harness/data-driven-lint.js +153 -0
- package/dist/harness/data-driven-lint.js.map +1 -0
- package/dist/harness/parse.d.ts +3 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +25 -0
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/query-catalog.d.ts +48 -0
- package/dist/harness/query-catalog.d.ts.map +1 -0
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -0
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +7 -4
- package/dist/harness/script-check.js.map +1 -1
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/dist/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +41 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +22 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +41 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +22 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/dist/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/dist/orchestrator/templates/specs-api.d.ts +19 -0
- package/dist/orchestrator/templates/specs-api.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-api.js +128 -0
- package/dist/orchestrator/templates/specs-api.js.map +1 -0
- package/dist/orchestrator/templates/specs-api.ts +101 -0
- package/dist/orchestrator/templates/specs-db.d.ts +8 -0
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +22 -0
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +22 -0
- package/dist/orchestrator/templates/specs-test-data.ts +76 -15
- package/package.json +7 -30
- package/src/capabilities/builtins.ts +85 -0
- package/src/capabilities/context-router.ts +66 -0
- package/src/capabilities/context.ts +46 -0
- package/src/capabilities/discover.ts +42 -0
- package/src/capabilities/registry.ts +111 -0
- package/src/capabilities/sensor.ts +47 -0
- package/src/cli/commands/challenge.ts +6 -2
- package/src/cli/commands/delivery.ts +3 -2
- package/src/cli/commands/generate.ts +12 -0
- package/src/cli/index.ts +10 -1
- package/src/exporters/csv-exporter.ts +22 -6
- package/src/exporters/spec-parser.ts +6 -1
- package/src/generators/test-generator/adapters/adapter-interface.ts +2 -1
- package/src/generators/test-generator/adapters/playwright/playwright-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/imports.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/scenario.hbs +19 -1
- package/src/generators/test-generator/code-generator.ts +114 -59
- package/src/generators/test-generator/patterns/expect-patterns.ts +49 -0
- package/src/generators/test-generator/patterns/index.ts +9 -33
- package/src/generators/test-generator/step-mapper.ts +9 -0
- package/src/generators/test-generator/template-engine.ts +5 -2
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +5 -5
- package/src/harness/annotation-overrides.ts +25 -0
- package/src/harness/audit.ts +37 -8
- package/src/harness/catalog/drivers.yaml +35 -12
- package/src/harness/challenge.ts +47 -2
- package/src/harness/data-driven-lint.ts +119 -0
- package/src/harness/parse.ts +17 -0
- package/src/harness/query-catalog.ts +0 -0
- package/src/harness/script-check.ts +8 -5
- package/src/index.ts +30 -0
- package/src/orchestrator/templates/ai-instructions/claude-agent-challenge.md +3 -2
- package/src/orchestrator/templates/ai-instructions/claude-cmd-create-test.md +4 -3
- package/src/orchestrator/templates/ai-instructions/claude-skill-gherkin-syntax.md +41 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-generation.md +22 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/claude-skill-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/ai-instructions/copilot-cmd-create-test.md +4 -3
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-gherkin-syntax.md +41 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-generation.md +22 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-tc-review.md +1 -0
- package/src/orchestrator/templates/ai-instructions/github-skill-sungen-test-design-techniques.md +6 -0
- package/src/orchestrator/templates/specs-api.ts +101 -0
- package/src/orchestrator/templates/specs-db.ts +22 -0
- package/src/orchestrator/templates/specs-test-data.ts +76 -15
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/assertion-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/assertion-patterns.js +0 -626
- package/dist/generators/test-generator/patterns/assertion-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts +0 -21
- package/dist/generators/test-generator/patterns/capture-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/capture-patterns.js +0 -87
- package/dist/generators/test-generator/patterns/capture-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/database-patterns.d.ts +0 -5
- package/dist/generators/test-generator/patterns/database-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/database-patterns.js +0 -94
- package/dist/generators/test-generator/patterns/database-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/form-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/form-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/form-patterns.js +0 -160
- package/dist/generators/test-generator/patterns/form-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/interaction-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/interaction-patterns.js +0 -433
- package/dist/generators/test-generator/patterns/interaction-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/keyboard-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/keyboard-patterns.js +0 -47
- package/dist/generators/test-generator/patterns/keyboard-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/navigation-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/navigation-patterns.js +0 -125
- package/dist/generators/test-generator/patterns/navigation-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/scope-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/scope-patterns.js +0 -36
- package/dist/generators/test-generator/patterns/scope-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts +0 -7
- package/dist/generators/test-generator/patterns/scroll-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/scroll-patterns.js +0 -25
- package/dist/generators/test-generator/patterns/scroll-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts +0 -6
- package/dist/generators/test-generator/patterns/setup-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/setup-patterns.js +0 -72
- package/dist/generators/test-generator/patterns/setup-patterns.js.map +0 -1
- package/dist/generators/test-generator/patterns/table-patterns.d.ts +0 -19
- package/dist/generators/test-generator/patterns/table-patterns.d.ts.map +0 -1
- package/dist/generators/test-generator/patterns/table-patterns.js +0 -239
- package/dist/generators/test-generator/patterns/table-patterns.js.map +0 -1
- package/docs/orchestration-spec.md +0 -267
- package/src/generators/test-generator/patterns/assertion-patterns.ts +0 -691
- package/src/generators/test-generator/patterns/capture-patterns.ts +0 -97
- package/src/generators/test-generator/patterns/database-patterns.ts +0 -95
- package/src/generators/test-generator/patterns/form-patterns.ts +0 -167
- package/src/generators/test-generator/patterns/interaction-patterns.ts +0 -465
- package/src/generators/test-generator/patterns/keyboard-patterns.ts +0 -51
- package/src/generators/test-generator/patterns/navigation-patterns.ts +0 -140
- package/src/generators/test-generator/patterns/scope-patterns.ts +0 -40
- package/src/generators/test-generator/patterns/scroll-patterns.ts +0 -27
- package/src/generators/test-generator/patterns/setup-patterns.ts +0 -76
- package/src/generators/test-generator/patterns/table-patterns.ts +0 -279
package/src/cli/index.ts
CHANGED
|
@@ -26,6 +26,8 @@ import { registerChallengeCommand } from './commands/challenge';
|
|
|
26
26
|
import { registerBlindspotCommand } from './commands/blindspot';
|
|
27
27
|
import { registerCapabilityCommand } from './commands/capability';
|
|
28
28
|
import { registerFlowCheckCommand } from './commands/flow-check';
|
|
29
|
+
import { capabilityRegistry } from '../capabilities/registry';
|
|
30
|
+
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
29
31
|
|
|
30
32
|
// Read version from package.json so `--version` never drifts from the released version.
|
|
31
33
|
const { version } = require('../../package.json') as { version: string };
|
|
@@ -42,7 +44,7 @@ async function main() {
|
|
|
42
44
|
program
|
|
43
45
|
.option('-v, --verbose', 'Enable verbose logging');
|
|
44
46
|
|
|
45
|
-
// Register commands
|
|
47
|
+
// Register commands
|
|
46
48
|
registerInitCommand(program);
|
|
47
49
|
registerAddCommand(program);
|
|
48
50
|
registerGenerateCommand(program);
|
|
@@ -65,6 +67,13 @@ async function main() {
|
|
|
65
67
|
registerIngestCommand(program);
|
|
66
68
|
registerEvalCommand(program);
|
|
67
69
|
|
|
70
|
+
// Capability-contributed CLI commands (Capability SPI): drivers own their authoring commands
|
|
71
|
+
// (e.g. @sungen/driver-api → `sungen api import`). Discover, then register each.
|
|
72
|
+
discoverAndRegisterCapabilities();
|
|
73
|
+
for (const cap of capabilityRegistry.all()) {
|
|
74
|
+
for (const registerCommand of cap.cliCommands ?? []) registerCommand(program);
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
await program.parseAsync(process.argv);
|
|
69
78
|
}
|
|
70
79
|
|
|
@@ -53,7 +53,25 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
53
53
|
let fallbackIndex = 1;
|
|
54
54
|
|
|
55
55
|
for (const m of input.merged) {
|
|
56
|
-
|
|
56
|
+
// Data-driven (@cases): one scenario ran once per input row — emit one CSV row per
|
|
57
|
+
// executed input. The Playwright results are titled "<scenario> — <label>"; expand
|
|
58
|
+
// by matching that prefix. With no results yet, fall back to a single (Pending) row.
|
|
59
|
+
const isCases = m.feature.tags.some((t) => t.startsWith('@cases:'));
|
|
60
|
+
let variants: Array<{ nameSuffix: string; result?: PlaywrightResult }> = [{ nameSuffix: '' }];
|
|
61
|
+
if (isCases && input.results) {
|
|
62
|
+
// Result titles are "<describe> > <scenario> — <label>"; match the scenario+label marker.
|
|
63
|
+
const marker = `${m.feature.name} — `;
|
|
64
|
+
const rowResults = [...input.results.entries()].filter(([t]) => t.includes(marker));
|
|
65
|
+
if (rowResults.length) {
|
|
66
|
+
variants = rowResults.map(([t, r]) => ({ nameSuffix: ` — ${t.slice(t.indexOf(marker) + marker.length)}`, result: r }));
|
|
67
|
+
}
|
|
68
|
+
} else if (input.results && m.spec) {
|
|
69
|
+
variants = [{ nameSuffix: '', result: input.results.get(m.spec.testTitle) }];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const variant of variants) {
|
|
73
|
+
const displayName = `${m.feature.name}${variant.nameSuffix}`;
|
|
74
|
+
const { vpId, category1 } = splitVpAndName(displayName);
|
|
57
75
|
const tcId = generateTcId(input.screen, vpId, fallbackIndex);
|
|
58
76
|
if (!vpId) fallbackIndex++;
|
|
59
77
|
|
|
@@ -87,11 +105,8 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
87
105
|
// automatically) and render natively as multi-line in XLSX.
|
|
88
106
|
const testData = formatTestData(m.feature.referencedVars, input.testData, Infinity, '\n');
|
|
89
107
|
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
if (input.results && m.spec) {
|
|
93
|
-
result = input.results.get(m.spec.testTitle);
|
|
94
|
-
}
|
|
108
|
+
// Status for this (possibly per-input) row — resolved into `variant` above.
|
|
109
|
+
const result: PlaywrightResult | undefined = variant.result;
|
|
95
110
|
|
|
96
111
|
// Determine Test Result
|
|
97
112
|
let testResult: string;
|
|
@@ -141,6 +156,7 @@ export function buildTestCaseRows(input: BuildCsvInput): TestCaseRow[] {
|
|
|
141
156
|
testEnvironment: environment,
|
|
142
157
|
note,
|
|
143
158
|
});
|
|
159
|
+
}
|
|
144
160
|
}
|
|
145
161
|
|
|
146
162
|
return rows;
|
|
@@ -20,7 +20,12 @@ function extractTestBlock(content: string, startIdx: number): {
|
|
|
20
20
|
// test('title', { tag: [...] }, async ({ page }) => {
|
|
21
21
|
// Backreference \1 lets the inner title contain the opposite quote type
|
|
22
22
|
// (e.g. test('Footer "X" link', ...) — common when scenarios cite UI labels).
|
|
23
|
-
|
|
23
|
+
// Quote char may be ' " or ` — the backtick form is a data-driven (@cases) title
|
|
24
|
+
// like `VP-… — ${__row.__label}`.
|
|
25
|
+
// The options object uses `\{.*?\}` (non-greedy, not `[^}]*`) so a tag value containing a
|
|
26
|
+
// `}` — e.g. `@query:q(p={{var}})` — doesn't truncate the match (issue #271). The header is
|
|
27
|
+
// single-line, so `.*?` anchored on `}, async` is safe.
|
|
28
|
+
const testRegex = /test\s*\(\s*(['"`])((?:(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
|
|
24
29
|
testRegex.lastIndex = startIdx;
|
|
25
30
|
const match = testRegex.exec(content);
|
|
26
31
|
if (!match) return null;
|
|
@@ -36,6 +36,7 @@ export interface ScenarioData {
|
|
|
36
36
|
authRole?: string; // Auth role for storage state
|
|
37
37
|
isParallel?: boolean; // @parallel: use fresh page from fixture
|
|
38
38
|
tags?: string; // Pass-through tags for Playwright { tag: [...] }, e.g. "'@smoke', '@high'"
|
|
39
|
+
casesDataset?: string; // @cases:<dataset> — emit one test() per row of the runtime dataset
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
export interface StepTemplateData {
|
|
@@ -63,7 +64,7 @@ export interface TestGeneratorAdapter {
|
|
|
63
64
|
// Template rendering methods
|
|
64
65
|
renderTestFile(data: TestFileData): string;
|
|
65
66
|
renderScenario(data: ScenarioData): string;
|
|
66
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string;
|
|
67
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string;
|
|
67
68
|
renderBeforeEach(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
68
69
|
renderBeforeAll(data: { steps: Array<{ comment?: string; code: string }> }): string;
|
|
69
70
|
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; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
29
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
|
|
30
30
|
return this.templateEngine.renderImports(options);
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -6,6 +6,9 @@ import { TestDataLoader } from '{{basePath}}/test-data';
|
|
|
6
6
|
{{#if needsDb}}
|
|
7
7
|
import { db } from '{{basePath}}/db';
|
|
8
8
|
{{/if}}
|
|
9
|
+
{{#if needsApi}}
|
|
10
|
+
import { api } from '{{basePath}}/api';
|
|
11
|
+
{{/if}}
|
|
9
12
|
|
|
10
13
|
// This file is auto-generated from Gherkin feature files
|
|
11
14
|
// DO NOT EDIT MANUALLY - changes will be overwritten
|
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
{{#if casesDataset}}
|
|
2
|
+
for (const __row of testData.cases('{{casesDataset}}')) {
|
|
3
|
+
{{#if tags}}
|
|
4
|
+
test(`{{scenarioName}} — ${__row.__label}`, { tag: [{{{tags}}}] }, async ({{#if isParallel}}{ page }{{/if}}) => {
|
|
5
|
+
{{else}}
|
|
6
|
+
test(`{{scenarioName}} — ${__row.__label}`, async ({{#if isParallel}}{ page }{{/if}}) => {
|
|
7
|
+
{{/if}}
|
|
8
|
+
const rowData = testData.withRow(__row);
|
|
9
|
+
{{#each steps}}
|
|
10
|
+
{{#if comment}}
|
|
11
|
+
// {{comment}}
|
|
12
|
+
{{/if}}
|
|
13
|
+
{{code}}
|
|
14
|
+
{{/each}}
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
{{else}}
|
|
1
18
|
{{#if isParallel}}
|
|
2
19
|
{{#if tags}}
|
|
3
20
|
test('{{scenarioName}}', { tag: [{{{tags}}}] }, async ({ page }) => {
|
|
@@ -24,4 +41,5 @@
|
|
|
24
41
|
{{code}}
|
|
25
42
|
{{/each}}
|
|
26
43
|
});
|
|
27
|
-
{{/if}}
|
|
44
|
+
{{/if}}
|
|
45
|
+
{{/if}}
|
|
@@ -4,7 +4,8 @@ import { ParsedFeature, ParsedScenario, ParsedStep } from '../gherkin-parser';
|
|
|
4
4
|
import { StepMapper } from './step-mapper';
|
|
5
5
|
import { TestGeneratorAdapter, adapterRegistry } from './adapters';
|
|
6
6
|
import { transformToRuntimeData } from './utils/runtime-data-transformer';
|
|
7
|
-
import {
|
|
7
|
+
import { capabilityRegistry } from '../../capabilities/registry';
|
|
8
|
+
import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Filter base scenario steps for @extend: only keep Given→When steps.
|
|
@@ -74,7 +75,7 @@ function extractCleanupFlags(tags: string[]): { overlay?: boolean; forms?: boole
|
|
|
74
75
|
const FUNCTIONAL_TAG_PREFIXES = [
|
|
75
76
|
'@parallel', '@cleanup:', '@auth:', '@manual', '@no-auth',
|
|
76
77
|
'@steps:', '@extend:', '@screenshot:', '@beforeAll', '@afterEach', '@afterAll',
|
|
77
|
-
'@flow',
|
|
78
|
+
'@flow', '@cases:',
|
|
78
79
|
];
|
|
79
80
|
|
|
80
81
|
function extractPassThroughTags(scenarioTags: string[], featureTags: string[]): string | undefined {
|
|
@@ -170,6 +171,8 @@ export class CodeGenerator {
|
|
|
170
171
|
private adapter: TestGeneratorAdapter;
|
|
171
172
|
private screenName?: string;
|
|
172
173
|
private options: any;
|
|
174
|
+
// Screen/flow name for the CURRENT feature, in catalog-resolution form (`flows/<x>` for flows).
|
|
175
|
+
private queryScreenName: string = '';
|
|
173
176
|
// Steps registry built per feature during generateTestCode(); used by countSteps()
|
|
174
177
|
private stepsRegistry = new Map<string, ParsedScenario>();
|
|
175
178
|
|
|
@@ -237,11 +240,17 @@ export class CodeGenerator {
|
|
|
237
240
|
const hasCleanupTags = (feature.tags || []).some(t => t.startsWith('@cleanup:'));
|
|
238
241
|
const needsCleanupImport = !isParallelFeature && hasCleanupTags;
|
|
239
242
|
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
// Active capabilities for this feature (registry-driven): the default UI + any whose annotation
|
|
244
|
+
// tags appear (@query) or whose detectsStep matches (declarative DB steps). Each active
|
|
245
|
+
// capability emits its declared runtime helpers (db → specs/db.ts).
|
|
246
|
+
const activeCapabilityIds = this.activeCapabilityIds(feature);
|
|
247
|
+
const needsDb = activeCapabilityIds.includes('db');
|
|
248
|
+
const needsApi = activeCapabilityIds.includes('api');
|
|
249
|
+
for (const id of activeCapabilityIds) {
|
|
250
|
+
for (const h of capabilityRegistry.get(id)?.runtimeHelpers ?? []) this.syncGeneratedHelper(outputDir, h.file, h.template);
|
|
251
|
+
}
|
|
243
252
|
|
|
244
|
-
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb });
|
|
253
|
+
const imports = this.adapter.renderImports({ runtimeData: this.options.runtimeData, basePath, needsCleanupImport, needsDb, needsApi });
|
|
245
254
|
|
|
246
255
|
// Generate test code (async now to support AI mapping)
|
|
247
256
|
const testCode = await this.generateTestCode(feature);
|
|
@@ -297,67 +306,68 @@ export class CodeGenerator {
|
|
|
297
306
|
/**
|
|
298
307
|
* Ensure specs/base.ts exists in the output directory
|
|
299
308
|
*/
|
|
300
|
-
/**
|
|
301
|
-
|
|
309
|
+
/**
|
|
310
|
+
* Capabilities active for a feature (registry-driven): the default (UI) capability, plus any
|
|
311
|
+
* whose annotation tags appear on a scenario (e.g. `@query` → db) or whose `detectsStep`
|
|
312
|
+
* matches a step (db's declarative `User see [table] row where …`). Drives runtime-helper
|
|
313
|
+
* emission + the `db` import — replaces the hardcoded `featureUsesDb` check (R4).
|
|
314
|
+
*/
|
|
315
|
+
private activeCapabilityIds(feature: ParsedFeature): string[] {
|
|
316
|
+
discoverAndRegisterCapabilities();
|
|
302
317
|
const steps: ParsedStep[] = [];
|
|
303
318
|
if (feature.background?.steps) steps.push(...feature.background.steps);
|
|
304
319
|
for (const sc of feature.scenarios || []) if (sc.steps) steps.push(...sc.steps);
|
|
305
|
-
|
|
320
|
+
const scenarioTags = (feature.scenarios || []).flatMap((sc) => sc.tags || []);
|
|
321
|
+
const ids = new Set<string>();
|
|
322
|
+
const def = capabilityRegistry.defaultCapabilityId();
|
|
323
|
+
if (def) ids.add(def);
|
|
324
|
+
for (const cap of capabilityRegistry.all()) {
|
|
325
|
+
const annoMatch = (cap.annotations ?? []).some((a) => scenarioTags.some((t) => t === a || t.startsWith(a + ':')));
|
|
326
|
+
const stepMatch = cap.detectsStep ? steps.some((s) => s && typeof s.text === 'string' && cap.detectsStep!(s.text)) : false;
|
|
327
|
+
if (annoMatch || stepMatch) ids.add(cap.id);
|
|
328
|
+
}
|
|
329
|
+
return [...ids];
|
|
306
330
|
}
|
|
307
331
|
|
|
308
|
-
/**
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
}
|
|
332
|
+
/**
|
|
333
|
+
* Precondition steps a scenario's capabilities inject (e.g. db `@query:<name>` → bind {{name}}).
|
|
334
|
+
* Each capability owns its annotation codegen via the SPI (`preconditionCodegen`); the compiler
|
|
335
|
+
* just composes + indents the returned statements (R4).
|
|
336
|
+
*/
|
|
337
|
+
private capabilityPreconditions(scenario: ParsedScenario): Array<{ comment?: string; code: string; boundVars?: string[] }> {
|
|
338
|
+
discoverAndRegisterCapabilities();
|
|
339
|
+
const out: Array<{ comment?: string; code: string; boundVars?: string[] }> = [];
|
|
340
|
+
for (const cap of capabilityRegistry.all()) {
|
|
341
|
+
if (!cap.preconditionCodegen) continue;
|
|
342
|
+
out.push(...cap.preconditionCodegen({ tags: scenario.tags || [], screenName: this.queryScreenName, cwd: process.cwd() }));
|
|
343
|
+
}
|
|
344
|
+
return out;
|
|
320
345
|
}
|
|
321
346
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const localeFixturePath = path.join(outputDir, 'locale-fixture.ts');
|
|
340
|
-
if (!fs.existsSync(localeFixturePath)) {
|
|
341
|
-
const templatePath = path.join(templatesRoot, 'specs-locale-fixture.ts');
|
|
342
|
-
if (fs.existsSync(templatePath)) {
|
|
343
|
-
const baseDir = path.dirname(localeFixturePath);
|
|
344
|
-
if (!fs.existsSync(baseDir)) {
|
|
345
|
-
fs.mkdirSync(baseDir, { recursive: true });
|
|
346
|
-
}
|
|
347
|
-
fs.copyFileSync(templatePath, localeFixturePath);
|
|
348
|
-
console.log('✓ Created: specs/locale-fixture.ts');
|
|
349
|
-
}
|
|
350
|
-
}
|
|
347
|
+
/**
|
|
348
|
+
* Copy an auto-generated runtime helper into specs/, **refreshing it when the template changed**.
|
|
349
|
+
* These files carry a `DO NOT EDIT — regenerated` header, so a stale copy left by an older sungen
|
|
350
|
+
* (e.g. a `specs/db.ts` from before a feature was added) is replaced instead of kept (issue #270).
|
|
351
|
+
* No-ops when the on-disk content already matches the template, so it stays quiet + idempotent.
|
|
352
|
+
*/
|
|
353
|
+
private syncGeneratedHelper(outputDir: string, fileName: string, templateName: string): void {
|
|
354
|
+
const templatePath = path.join(__dirname, '..', '..', 'orchestrator', 'templates', templateName);
|
|
355
|
+
if (!fs.existsSync(templatePath)) return;
|
|
356
|
+
const targetPath = path.join(outputDir, fileName);
|
|
357
|
+
const next = fs.readFileSync(templatePath, 'utf8');
|
|
358
|
+
const exists = fs.existsSync(targetPath);
|
|
359
|
+
if (exists && fs.readFileSync(targetPath, 'utf8') === next) return; // already current
|
|
360
|
+
if (!fs.existsSync(outputDir)) fs.mkdirSync(outputDir, { recursive: true });
|
|
361
|
+
fs.writeFileSync(targetPath, next);
|
|
362
|
+
console.log(`✓ ${exists ? 'Updated' : 'Created'}: specs/${fileName}`);
|
|
363
|
+
}
|
|
351
364
|
|
|
365
|
+
ensureBaseFile(outputDir: string): void {
|
|
366
|
+
this.syncGeneratedHelper(outputDir, 'base.ts', 'specs-base.ts');
|
|
367
|
+
// base.ts depends on locale-fixture.ts — keep them paired.
|
|
368
|
+
this.syncGeneratedHelper(outputDir, 'locale-fixture.ts', 'specs-locale-fixture.ts');
|
|
352
369
|
if (this.options.runtimeData) {
|
|
353
|
-
|
|
354
|
-
if (!fs.existsSync(testDataPath)) {
|
|
355
|
-
const templatePath = path.join(templatesRoot, 'specs-test-data.ts');
|
|
356
|
-
if (fs.existsSync(templatePath)) {
|
|
357
|
-
fs.copyFileSync(templatePath, testDataPath);
|
|
358
|
-
console.log('✓ Created: specs/test-data.ts');
|
|
359
|
-
}
|
|
360
|
-
}
|
|
370
|
+
this.syncGeneratedHelper(outputDir, 'test-data.ts', 'specs-test-data.ts');
|
|
361
371
|
}
|
|
362
372
|
}
|
|
363
373
|
|
|
@@ -391,6 +401,8 @@ export class CodeGenerator {
|
|
|
391
401
|
}
|
|
392
402
|
this.stepMapper.setScreenContext(effectiveScreenName);
|
|
393
403
|
}
|
|
404
|
+
// Catalog-resolution screen name for @query binds (flows are prefixed `flows/`).
|
|
405
|
+
this.queryScreenName = isFlowFeature ? `flows/${effectiveScreenName}` : (effectiveScreenName || '');
|
|
394
406
|
|
|
395
407
|
// Reset flow mode per feature to prevent state leak in --all mode
|
|
396
408
|
this.stepMapper.setFlowMode(isFlowFeature);
|
|
@@ -637,6 +649,33 @@ export class CodeGenerator {
|
|
|
637
649
|
// Set scenario context for path variable resolution (full merged list)
|
|
638
650
|
this.stepMapper.setScenarioContext(stepsToMap);
|
|
639
651
|
|
|
652
|
+
// Data-driven (@cases): the scenario's {{col}} refs are dataset *row columns* that
|
|
653
|
+
// exist only at runtime — register them as captured so they resolve to a runtime
|
|
654
|
+
// get() (→ rowData.get) instead of compile-time YAML lookup.
|
|
655
|
+
const casesTag = scenario.tags.find((t) => t.startsWith('@cases:'));
|
|
656
|
+
const casesDataset = casesTag ? casesTag.slice('@cases:'.length).trim() : undefined;
|
|
657
|
+
if (casesDataset) {
|
|
658
|
+
const refs = new Set<string>();
|
|
659
|
+
for (const st of stepsToMap) {
|
|
660
|
+
for (const m of (st.text || '').matchAll(/\{\{\s*([\w.]+)\s*\}\}/g)) refs.add(m[1]);
|
|
661
|
+
}
|
|
662
|
+
for (const r of refs) this.stepMapper.registerCaptured(r);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Capability preconditions (db `@query:<name>` → bind {{name}}) are owned by the capability via
|
|
666
|
+
// the SPI. Their bound `{{name.*}}` vars exist only at runtime → register them as captured so
|
|
667
|
+
// they resolve to a runtime get() instead of a compile-time YAML lookup that would fail.
|
|
668
|
+
const preconditions = this.capabilityPreconditions(scenario);
|
|
669
|
+
const boundVars = preconditions.flatMap((p) => p.boundVars || []);
|
|
670
|
+
if (boundVars.length) {
|
|
671
|
+
for (const st of stepsToMap) {
|
|
672
|
+
for (const mt of (st.text || '').matchAll(/\{\{\s*([^}]+?)\s*\}\}/g)) {
|
|
673
|
+
const head = mt[1].split(/[.[]/)[0];
|
|
674
|
+
if (boundVars.includes(head)) this.stepMapper.registerCaptured(mt[1]);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
640
679
|
const steps: Array<{ comment?: string; code: string }> = [];
|
|
641
680
|
|
|
642
681
|
if (scenario.extendsName && this.stepsRegistry.has(scenario.extendsName)) {
|
|
@@ -666,17 +705,33 @@ export class CodeGenerator {
|
|
|
666
705
|
}
|
|
667
706
|
}
|
|
668
707
|
|
|
708
|
+
// Capability preconditions (db `@query:<name>` → bind {{name}}; computed above) run BEFORE the
|
|
709
|
+
// scenario's own steps — prepend them, indenting the capability-supplied statements.
|
|
710
|
+
if (preconditions.length) {
|
|
711
|
+
steps.unshift(...preconditions.map((p) => ({ comment: p.comment, code: this.indentCode(p.code, 4) })));
|
|
712
|
+
}
|
|
713
|
+
|
|
669
714
|
// Extract pass-through tags (feature + scenario, excluding functional tags)
|
|
670
715
|
const tags = extractPassThroughTags(scenario.tags, featureTags);
|
|
671
716
|
|
|
672
717
|
// Use adapter to render scenario
|
|
673
|
-
|
|
718
|
+
const rendered = this.adapter.renderScenario({
|
|
674
719
|
scenarioName: scenario.name,
|
|
675
720
|
steps,
|
|
676
721
|
authRole,
|
|
677
722
|
isParallel,
|
|
678
723
|
tags,
|
|
724
|
+
casesDataset,
|
|
679
725
|
});
|
|
726
|
+
|
|
727
|
+
// Data-driven (@cases): the per-row test() binds a row-scoped view (`rowData`).
|
|
728
|
+
// Pre-transform THIS scenario's runtime-data markers to read from `rowData`, so the
|
|
729
|
+
// global `testData` transform that runs next on the rest of the file leaves it alone.
|
|
730
|
+
// The loop header's `testData.cases()/withRow()` are literal code (no markers) → untouched.
|
|
731
|
+
if (casesDataset && this.options.runtimeData) {
|
|
732
|
+
return transformToRuntimeData(rendered, 'rowData');
|
|
733
|
+
}
|
|
734
|
+
return rendered;
|
|
680
735
|
}
|
|
681
736
|
|
|
682
737
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { ParsedStep } from '../../gherkin-parser';
|
|
2
|
+
import { StepPattern, PatternContext } from './types';
|
|
3
|
+
import { MappedStep } from '../step-mapper';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Data-vs-data assertions — compare two runtime values directly (no UI element).
|
|
7
|
+
* The primary use is asserting on an `@query`-bound result via path access, e.g.
|
|
8
|
+
*
|
|
9
|
+
* Then expect {{products.count}} is {{expected}}
|
|
10
|
+
* Then expect {{products.count}} is at least {{one}}
|
|
11
|
+
* Then expect {{active_user.status}} is "active"
|
|
12
|
+
* Then expect {{order.total}} is not {{zero}}
|
|
13
|
+
*
|
|
14
|
+
* Both sides are `{{var|path}}` | "literal" | 'literal' | number. Generic — works for any
|
|
15
|
+
* test-data values, not only DB results.
|
|
16
|
+
*/
|
|
17
|
+
const VALUE = String.raw`\{\{[^}]+\}\}|"[^"]*"|'[^']*'|-?\d+(?:\.\d+)?`;
|
|
18
|
+
const reExpect = new RegExp(`^\\s*(?:User\\s+)?expect\\s+(${VALUE})\\s+is\\s+(not\\s+|at\\s+least\\s+|at\\s+most\\s+)?(${VALUE})\\s*$`, 'i');
|
|
19
|
+
|
|
20
|
+
/** Render a value token (`{{var}}` | "literal" | 'literal' | number) as a JS expression. */
|
|
21
|
+
function valueExpr(token: string): string {
|
|
22
|
+
const t = token.trim();
|
|
23
|
+
const v = t.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
24
|
+
if (v) return `testData.get(${JSON.stringify(v[1])})`;
|
|
25
|
+
const q = t.match(/^["'](.*)["']$/);
|
|
26
|
+
if (q) return JSON.stringify(q[1]);
|
|
27
|
+
if (/^-?\d+(?:\.\d+)?$/.test(t)) return t;
|
|
28
|
+
return JSON.stringify(t);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const expectPatterns: StepPattern[] = [
|
|
32
|
+
{
|
|
33
|
+
name: 'expect-data',
|
|
34
|
+
priority: 62, // above generic see-assertions; sibling of the DB assertions
|
|
35
|
+
matcher: (step: ParsedStep) => reExpect.test(step.text),
|
|
36
|
+
generator: (step: ParsedStep, _ctx: PatternContext): MappedStep => {
|
|
37
|
+
const m = step.text.match(reExpect)!;
|
|
38
|
+
const a = valueExpr(m[1]);
|
|
39
|
+
const op = (m[2] || '').trim().toLowerCase();
|
|
40
|
+
const b = valueExpr(m[3]);
|
|
41
|
+
let code: string;
|
|
42
|
+
if (op === 'at least') code = `expect(Number(${a})).toBeGreaterThanOrEqual(Number(${b}));`;
|
|
43
|
+
else if (op === 'at most') code = `expect(Number(${a})).toBeLessThanOrEqual(Number(${b}));`;
|
|
44
|
+
else if (op === 'not') code = `expect(String(${a})).not.toBe(String(${b}));`;
|
|
45
|
+
else code = `expect(String(${a})).toBe(String(${b}));`;
|
|
46
|
+
return { code, comment: `Expect ${m[1]} ${op ? op + ' ' : 'is '}${m[3]}` };
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { ParsedStep } from '../../gherkin-parser';
|
|
2
2
|
import { MappedStep } from '../step-mapper';
|
|
3
3
|
import { StepPattern, PatternContext } from './types';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { interactionPatterns } from './interaction-patterns';
|
|
7
|
-
import { assertionPatterns } from './assertion-patterns';
|
|
8
|
-
import { setupPatterns } from './setup-patterns';
|
|
9
|
-
import { keyboardPatterns } from './keyboard-patterns';
|
|
10
|
-
import { scrollPatterns } from './scroll-patterns';
|
|
11
|
-
import { scopePatterns } from './scope-patterns';
|
|
12
|
-
import { tablePatterns } from './table-patterns';
|
|
13
|
-
import { capturePatterns } from './capture-patterns';
|
|
14
|
-
import { databasePatterns } from './database-patterns';
|
|
4
|
+
import { capabilityRegistry } from '../../../capabilities/registry';
|
|
5
|
+
import { discoverAndRegisterCapabilities } from '../../../capabilities/discover';
|
|
15
6
|
|
|
16
7
|
/**
|
|
17
8
|
* Pattern Registry - manages all step patterns
|
|
@@ -27,17 +18,11 @@ export class PatternRegistry {
|
|
|
27
18
|
* Register default patterns from all pattern modules
|
|
28
19
|
*/
|
|
29
20
|
private registerDefaultPatterns(): void {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
this.patterns.push(...
|
|
35
|
-
this.patterns.push(...keyboardPatterns);
|
|
36
|
-
this.patterns.push(...scrollPatterns);
|
|
37
|
-
this.patterns.push(...scopePatterns);
|
|
38
|
-
this.patterns.push(...tablePatterns);
|
|
39
|
-
this.patterns.push(...capturePatterns);
|
|
40
|
-
this.patterns.push(...databasePatterns);
|
|
21
|
+
// Patterns are composed from the capability registry (Capability SPI, R1) instead of a
|
|
22
|
+
// hardcoded push list. Built-ins (ui · db · core) register the same set as before, so the
|
|
23
|
+
// composed list + priority sort is behaviour-identical (golden snapshots are the contract).
|
|
24
|
+
discoverAndRegisterCapabilities();
|
|
25
|
+
this.patterns.push(...capabilityRegistry.patterns());
|
|
41
26
|
|
|
42
27
|
// Sort by priority (higher first)
|
|
43
28
|
this.patterns.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
@@ -157,15 +142,6 @@ export class PatternRegistry {
|
|
|
157
142
|
}
|
|
158
143
|
}
|
|
159
144
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
export { navigationPatterns } from './navigation-patterns';
|
|
163
|
-
export { formPatterns } from './form-patterns';
|
|
164
|
-
export { interactionPatterns } from './interaction-patterns';
|
|
165
|
-
export { assertionPatterns } from './assertion-patterns';
|
|
166
|
-
export { keyboardPatterns } from './keyboard-patterns';
|
|
167
|
-
export { scrollPatterns } from './scroll-patterns';
|
|
168
|
-
export { scopePatterns } from './scope-patterns';
|
|
169
|
-
export { tablePatterns } from './table-patterns';
|
|
170
|
-
export { databasePatterns, isDbStep } from './database-patterns';
|
|
145
|
+
// The UI step patterns now live in @sungen/driver-ui (R5.4) and the DB patterns in @sungen/driver-db
|
|
146
|
+
// (R5.5); both are contributed via the capability registry, not re-exported here.
|
|
171
147
|
export * from './types';
|
|
@@ -94,6 +94,15 @@ export class StepMapper {
|
|
|
94
94
|
this.templateEngine.resetBaseContext();
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Register a runtime data variable so `{{name}}` resolves to `testData.get('name')`
|
|
99
|
+
* (skipping compile-time YAML validation). Used for @cases row columns, which exist
|
|
100
|
+
* only at runtime in the dataset rows. Scenario-scoped (cleared by setScenarioContext).
|
|
101
|
+
*/
|
|
102
|
+
registerCaptured(name: string): void {
|
|
103
|
+
this.dataResolver.registerCaptured(name);
|
|
104
|
+
}
|
|
105
|
+
|
|
97
106
|
/**
|
|
98
107
|
* Map a Gherkin step to Playwright code
|
|
99
108
|
* Uses pattern registry first, falls back to AI if enabled
|
|
@@ -229,8 +229,8 @@ export class TemplateEngine {
|
|
|
229
229
|
this.baseContext = {};
|
|
230
230
|
}
|
|
231
231
|
|
|
232
|
-
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean }): string {
|
|
233
|
-
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb });
|
|
232
|
+
renderImports(options?: { runtimeData?: boolean; basePath?: string; isParallel?: boolean; needsCleanupImport?: boolean ; needsDb?: boolean; needsApi?: boolean }): string {
|
|
233
|
+
return this.render('imports', { runtimeData: options?.runtimeData, basePath: options?.basePath || '..', isParallel: options?.isParallel, needsCleanupImport: options?.needsCleanupImport, needsDb: options?.needsDb, needsApi: options?.needsApi });
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
renderTestFile(data: {
|
|
@@ -284,6 +284,9 @@ export class TemplateEngine {
|
|
|
284
284
|
scenarioName: string;
|
|
285
285
|
steps: Array<{ comment?: string; code: string }>;
|
|
286
286
|
authRole?: string;
|
|
287
|
+
isParallel?: boolean;
|
|
288
|
+
tags?: string;
|
|
289
|
+
casesDataset?: string;
|
|
287
290
|
}): string {
|
|
288
291
|
return this.render('scenario', data);
|
|
289
292
|
}
|
|
@@ -4,7 +4,7 @@ const MARKER_PATTERN = /__SUNGEN_TD_([A-Za-z0-9_]+)__/;
|
|
|
4
4
|
* Replace __SUNGEN_TD_ markers with testData.get() calls in generated code.
|
|
5
5
|
* Three passes: comments, string literals, then regex literals.
|
|
6
6
|
*/
|
|
7
|
-
export function transformToRuntimeData(code: string): string {
|
|
7
|
+
export function transformToRuntimeData(code: string, accessor: string = 'testData'): string {
|
|
8
8
|
// Pass 0: Comments — replace markers in // comments with decoded key name
|
|
9
9
|
// Prevents Pass 2 from misinterpreting // comment markers as regex delimiters
|
|
10
10
|
code = code.replace(
|
|
@@ -20,9 +20,9 @@ export function transformToRuntimeData(code: string): string {
|
|
|
20
20
|
(_, _quote, prefix, enc, suffix) => {
|
|
21
21
|
const key = decodeKey(enc);
|
|
22
22
|
if (!prefix && !suffix) {
|
|
23
|
-
return
|
|
23
|
+
return `${accessor}.get('${key}')`;
|
|
24
24
|
}
|
|
25
|
-
return `\`${prefix}\${
|
|
25
|
+
return `\`${prefix}\${${accessor}.get('${key}')}${suffix}\``;
|
|
26
26
|
}
|
|
27
27
|
);
|
|
28
28
|
|
|
@@ -32,7 +32,7 @@ export function transformToRuntimeData(code: string): string {
|
|
|
32
32
|
/\/((?:[^/\\\n]|\\.)*?)__SUNGEN_TD_([A-Za-z0-9_]+)__((?:[^/\\\n]|\\.)*?)\/([gimsuy]*)/g,
|
|
33
33
|
(_, prefix, enc, suffix, flags) => {
|
|
34
34
|
const key = decodeKey(enc);
|
|
35
|
-
const ref =
|
|
35
|
+
const ref = `${accessor}.get('${key}')`;
|
|
36
36
|
const flagStr = flags ? `, '${flags}'` : '';
|
|
37
37
|
if (!prefix && !suffix) return `new RegExp(${ref}${flagStr})`;
|
|
38
38
|
return `new RegExp(\`${prefix}\${${ref}}${suffix}\`${flagStr})`;
|
|
@@ -44,7 +44,7 @@ export function transformToRuntimeData(code: string): string {
|
|
|
44
44
|
// table/list count templates). testData.get() returns a string, so coerce with Number().
|
|
45
45
|
code = code.replace(
|
|
46
46
|
/__SUNGEN_TD_([A-Za-z0-9_]+)__/g,
|
|
47
|
-
(_, enc) => `Number(
|
|
47
|
+
(_, enc) => `Number(${accessor}.get('${decodeKey(enc)}'))`
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
return code;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared annotation-override grammar for precondition annotations (`@query`/`@api`).
|
|
3
|
+
*
|
|
4
|
+
* Parses `name(a={{x}},b="lit",c=3)` overrides into a map of JS expressions, e.g.
|
|
5
|
+
* `{ a: "testData.get('x')", b: "\"lit\"", c: "3" }`. Used by the DB and API capability drivers'
|
|
6
|
+
* precondition codegen; lives in core so both drivers (and core's `api` until R5.6) can share it.
|
|
7
|
+
*/
|
|
8
|
+
export function parseQueryOverrides(raw?: string): Record<string, string> {
|
|
9
|
+
const out: Record<string, string> = {};
|
|
10
|
+
if (!raw) return out;
|
|
11
|
+
for (const part of raw.split(',')) {
|
|
12
|
+
const eq = part.indexOf('=');
|
|
13
|
+
if (eq < 0) continue;
|
|
14
|
+
const key = part.slice(0, eq).trim();
|
|
15
|
+
const val = part.slice(eq + 1).trim();
|
|
16
|
+
if (!key) continue;
|
|
17
|
+
const v = val.match(/^\{\{\s*([^}]+?)\s*\}\}$/);
|
|
18
|
+
const q = val.match(/^["'](.*)["']$/);
|
|
19
|
+
if (v) out[key] = `testData.get(${JSON.stringify(v[1])})`;
|
|
20
|
+
else if (q) out[key] = JSON.stringify(q[1]);
|
|
21
|
+
else if (/^-?\d+(?:\.\d+)?$/.test(val)) out[key] = val;
|
|
22
|
+
else out[key] = JSON.stringify(val);
|
|
23
|
+
}
|
|
24
|
+
return out;
|
|
25
|
+
}
|