@sun-asterisk/sungen 3.2.8 → 3.2.10-beta.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/capabilities/discover.js +1 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/capabilities/registry.d.ts +9 -0
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js +4 -0
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +15 -5
- 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 +16 -0
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/exporters/spec-parser.d.ts.map +1 -1
- package/dist/exporters/spec-parser.js +16 -5
- package/dist/exporters/spec-parser.js.map +1 -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/actions/capture-row-column.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
- package/dist/generators/test-generator/app-profile.d.ts +89 -0
- package/dist/generators/test-generator/app-profile.d.ts.map +1 -0
- package/dist/generators/test-generator/app-profile.js +157 -0
- package/dist/generators/test-generator/app-profile.js.map +1 -0
- package/dist/generators/test-generator/code-generator.d.ts +4 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +7 -0
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +36 -0
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -0
- package/dist/generators/test-generator/diagnostics.js +44 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -0
- package/dist/generators/test-generator/patterns/types.d.ts +12 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts +32 -0
- package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -0
- package/dist/generators/test-generator/recipe-expander.js +118 -0
- package/dist/generators/test-generator/recipe-expander.js.map +1 -0
- package/dist/generators/test-generator/step-mapper.d.ts +17 -0
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +70 -0
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +5 -1
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +27 -3
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +10 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +32 -0
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/query-catalog.d.ts +32 -2
- package/dist/harness/query-catalog.d.ts.map +1 -1
- package/dist/harness/query-catalog.js +0 -0
- package/dist/harness/query-catalog.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/create-data-test.md +83 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/config/claude.md +3 -1
- package/dist/orchestrator/templates/ai-src/config/copilot.md +3 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +93 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +66 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
- package/dist/orchestrator/templates/specs-db.d.ts +204 -6
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +495 -54
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +507 -54
- package/dist/orchestrator/templates/specs-test-data.ts +39 -3
- package/package.json +10 -4
- package/src/capabilities/discover.ts +1 -1
- package/src/capabilities/registry.ts +13 -0
- package/src/cli/commands/delivery.ts +13 -5
- package/src/cli/commands/generate.ts +15 -0
- package/src/exporters/spec-parser.ts +17 -5
- 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/actions/capture-row-column.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-element-with-text.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/click-with-alert-action.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/recipe-sequence.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +2 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/unknown-element-action.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-for-element.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/wait-table-refresh.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/has-text-value.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click-text.hbs +3 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-text.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-expect-value.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-for-each.hbs +5 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-scroll-into-view.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs +1 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-wait-for.hbs +1 -0
- package/src/generators/test-generator/app-profile.ts +185 -0
- package/src/generators/test-generator/code-generator.ts +9 -0
- package/src/generators/test-generator/diagnostics.ts +51 -0
- package/src/generators/test-generator/patterns/types.ts +12 -0
- package/src/generators/test-generator/recipe-expander.ts +129 -0
- package/src/generators/test-generator/step-mapper.ts +73 -0
- package/src/generators/test-generator/template-engine.ts +29 -3
- package/src/generators/test-generator/utils/selector-resolver.ts +49 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/query-catalog.ts +0 -0
- package/src/index.ts +5 -2
- package/src/orchestrator/templates/ai-src/commands/create-data-test.md +83 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +2 -2
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/config/claude.md +3 -1
- package/src/orchestrator/templates/ai-src/config/copilot.md +3 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-data-factory/SKILL.md +93 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +12 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +66 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +22 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -1
- package/src/orchestrator/templates/specs-db.ts +507 -54
- package/src/orchestrator/templates/specs-test-data.ts +39 -3
|
@@ -48,9 +48,27 @@ export class TestDataLoader {
|
|
|
48
48
|
if (value === undefined || value === null) {
|
|
49
49
|
throw new Error(`Test data key not found: ${key}`);
|
|
50
50
|
}
|
|
51
|
+
// A projected array (e.g. `{{q.rows[*].name}}`) serializes as a JSON list so the full
|
|
52
|
+
// ordered set is preserved for the assert-compare — String([...]) would flatten it to a
|
|
53
|
+
// lossy comma-join that can't be distinguished from a scalar containing commas.
|
|
54
|
+
if (Array.isArray(value)) return this.interpolate(JSON.stringify(value));
|
|
51
55
|
return this.interpolate(String(value));
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a key to its RAW, uncoerced value (array/object/number kept as-is) — for binding
|
|
60
|
+
* DB query parameters, where type-strict stores (MySQL numeric columns, MongoDB) require the
|
|
61
|
+
* native type. get() stringifies for Gherkin text; raw() must not. Throws on missing, mirroring
|
|
62
|
+
* get()'s guard.
|
|
63
|
+
*/
|
|
64
|
+
raw(key: string): any {
|
|
65
|
+
const value = this.resolve(key);
|
|
66
|
+
if (value === undefined || value === null) {
|
|
67
|
+
throw new Error(`Test data key not found: ${key}`);
|
|
68
|
+
}
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
54
72
|
/**
|
|
55
73
|
* Substitute embedded `{{ref}}` cross-references inside a resolved string value.
|
|
56
74
|
* Each ref is looked up via resolve() (flat key, @cases row column, or dotted path),
|
|
@@ -78,6 +96,7 @@ export class TestDataLoader {
|
|
|
78
96
|
* `q.count` / `q.length` → number of rows
|
|
79
97
|
* `q.first.col` / `q.last.col` / `q[2].col` → a specific row's column
|
|
80
98
|
* `q.col` → shorthand for the first row's column
|
|
99
|
+
* `q.rows[*].col` → the `col` of EVERY row, projected to an ordered array
|
|
81
100
|
*/
|
|
82
101
|
private resolve(key: string): any {
|
|
83
102
|
// 1. Exact flat key — captured vars (set()) live under a literal, possibly dotted, key.
|
|
@@ -88,9 +107,26 @@ export class TestDataLoader {
|
|
|
88
107
|
return this.data[key];
|
|
89
108
|
}
|
|
90
109
|
// 2. Structured path: head from the row (cases) or shared data, then walk segments.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
110
|
+
// `[n]` → `.n` (numeric index); `[*]` → `.*` (project the rest of the path over each element).
|
|
111
|
+
const tokens = String(key).replace(/\[(\d+)\]/g, '.$1').replace(/\[\*\]/g, '.*').split('.');
|
|
112
|
+
const cur: any = (this.row && tokens[0] in this.row) ? this.row[tokens[0]] : this.data[tokens[0]];
|
|
113
|
+
return TestDataLoader.walk(cur, tokens, 1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Walk the remaining path tokens from `i`. A `*` token on an array projects: the rest of the
|
|
118
|
+
* path is mapped over every element and the collected array returned (recurses for nested `[*]`).
|
|
119
|
+
* Non-`*` tokens use the linear single-step navigation, so numeric/first/last/object access is
|
|
120
|
+
* unchanged.
|
|
121
|
+
*/
|
|
122
|
+
private static walk(cur: any, tokens: string[], i: number): any {
|
|
123
|
+
for (; i < tokens.length && cur != null; i++) {
|
|
124
|
+
if (tokens[i] === '*' && Array.isArray(cur)) {
|
|
125
|
+
const rest = tokens.slice(i + 1);
|
|
126
|
+
return cur.map((el) => TestDataLoader.walk(el, rest, 0));
|
|
127
|
+
}
|
|
128
|
+
cur = TestDataLoader.step(cur, tokens[i]);
|
|
129
|
+
}
|
|
94
130
|
return cur;
|
|
95
131
|
}
|
|
96
132
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.10-beta.1",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"copy-templates": "mkdir -p dist/generators/test-generator/adapters/playwright/templates/steps && mkdir -p dist/generators/test-generator/templates && mkdir -p dist/orchestrator/templates && mkdir -p dist/dashboard/templates && cp -r src/generators/test-generator/adapters/playwright/templates/*.hbs dist/generators/test-generator/adapters/playwright/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/playwright/templates/steps dist/generators/test-generator/adapters/playwright/templates/ && mkdir -p dist/generators/test-generator/adapters/appium/templates/steps && cp -r src/generators/test-generator/adapters/appium/templates/*.hbs dist/generators/test-generator/adapters/appium/templates/ 2>/dev/null || true && cp -r src/generators/test-generator/adapters/appium/templates/steps dist/generators/test-generator/adapters/appium/templates/ && cp src/generators/test-generator/templates/*.hbs dist/generators/test-generator/templates/ 2>/dev/null || true && cp -r src/orchestrator/templates/* dist/orchestrator/templates/ && cp src/dashboard/templates/index.html dist/dashboard/templates/index.html && mkdir -p dist/harness/catalog && cp src/harness/catalog/*.yaml dist/harness/catalog/",
|
|
13
13
|
"build:dashboard": "cd ../../dashboard && npm install --silent && npm run build && cd - && cp ../../dashboard/dist/index.html src/dashboard/templates/index.html",
|
|
14
14
|
"dev": "tsx src/cli/index.ts",
|
|
15
|
-
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts",
|
|
15
|
+
"test": "tsx tests/golden/run.ts && tsx tests/audit/run.ts && tsx tests/ingest/run.ts && tsx tests/eval/run.ts && tsx tests/exporter/run.ts && tsx tests/exporter/feature-parser-category.run.ts && tsx tests/exporter/api-detail-sheet.run.ts && tsx tests/exporter/overview-sheet.run.ts && tsx tests/exporter/delivery-divider-tcid.run.ts && tsx tests/exporter/note-expected-actual.run.ts && tsx tests/exporter/test-data-vars.run.ts && tsx tests/exporter/mobile-app-id.run.ts && tsx tests/exporter/api-testcase-cells.run.ts && tsx tests/exporter/delivery-cases-result.run.ts && tsx tests/exporter/precondition-auth-role.run.ts && tsx tests/exporter/report-builder-parity.run.ts && tsx tests/exporter/delivery-preflight-testdata.run.ts && tsx tests/exporter/delivery-preflight-selectors.run.ts && tsx tests/dashboard/api-flows-discovery.run.ts && tsx tests/dashboard/status-results-fallback.run.ts && tsx tests/codegen/verb-ref-masking.run.ts && tsx tests/codegen/table-aria-hidden-locators.run.ts && tsx tests/api-runtime/base-path-url-join.run.ts && tsx tests/api-runtime/file-upload-multipart.run.ts && tsx tests/api-runtime/schema-assertion.run.ts && tsx tests/runtime/test-data-cross-ref.run.ts && tsx tests/runtime/test-data-array-projection.run.ts &&tsx tests/capabilities/run.ts && tsx tests/openapi/run.ts && tsx tests/api-field-coverage/run.ts && tsx tests/packaging/run.ts && tsx tests/generate-hint/run.ts && tsx tests/template-assertion/run.ts && tsx tests/journey/run.ts && tsx tests/harness/serial-cascade.run.ts && tsx tests/ai-skills/cross-assistant-orphan.run.ts && tsx tests/init/codex-mcp-config.run.ts && tsx src/orchestrator/ai-skills/golden-skills.test.ts && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/cosmos-engine.run.ts && tsx tests/db-runtime/mongodb-engine.run.ts && tsx tests/db-runtime/dynamodb-engine.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.ts && tsx tests/db-runtime/mongodb-integration.opt-in.run.ts && tsx tests/db-runtime/dynamodb-integration.opt-in.run.ts",
|
|
16
16
|
"test:update": "tsx tests/golden/run.ts --update && tsx tests/audit/run.ts --update && tsx tests/ingest/run.ts --update",
|
|
17
17
|
"prepublishOnly": "npm run build:dashboard && npm run build"
|
|
18
18
|
},
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"node": ">=18.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@sungen/driver-ui": "3.2.8",
|
|
37
36
|
"@anthropic-ai/sdk": "^0.71.0",
|
|
38
37
|
"@babel/parser": "^7.28.5",
|
|
39
38
|
"@babel/traverse": "^7.28.5",
|
|
40
39
|
"@babel/types": "^7.28.5",
|
|
41
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
42
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
+
"@sungen/driver-ui": "3.2.10-beta.1",
|
|
43
43
|
"chalk": "^5.6.2",
|
|
44
44
|
"commander": "^14.0.2",
|
|
45
45
|
"dotenv": "^17.2.3",
|
|
@@ -63,5 +63,11 @@
|
|
|
63
63
|
"src",
|
|
64
64
|
"README.md",
|
|
65
65
|
"LICENSE"
|
|
66
|
-
]
|
|
66
|
+
],
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"mysql2": "^3",
|
|
69
|
+
"mongodb": "^7",
|
|
70
|
+
"@aws-sdk/client-dynamodb": "^3",
|
|
71
|
+
"@aws-sdk/lib-dynamodb": "^3"
|
|
72
|
+
}
|
|
67
73
|
}
|
|
@@ -27,7 +27,7 @@ import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
|
|
|
27
27
|
* move to their packages (R5.5/R5.6) they join this list and leave `LOCAL_DRIVERS`; eventually this
|
|
28
28
|
* becomes a scan of installed `@sungen/driver-*` packages (R5.7).
|
|
29
29
|
*/
|
|
30
|
-
const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile'];
|
|
30
|
+
const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile', '@sungen/driver-data-factory'];
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Bundled (non-opt-in) drivers: shipped as a core dependency (`drivers.yaml` `bundled: true`), so the
|
|
@@ -62,6 +62,14 @@ export interface CapabilityDescriptor {
|
|
|
62
62
|
*/
|
|
63
63
|
cliCommands?: Array<(program: unknown) => void>;
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* App-profile presets this capability ships (spec: sungen_app_profile_spec.md §3).
|
|
67
|
+
* Keyed by short name (`radix`, `antd`, `native`); each value is an AppProfile-shaped
|
|
68
|
+
* plain object that `qa/app.yaml` can pull in via `extends:`. UI knowledge stays in
|
|
69
|
+
* the UI driver — core only merges data.
|
|
70
|
+
*/
|
|
71
|
+
appProfilePresets?: Record<string, unknown>;
|
|
72
|
+
|
|
65
73
|
// --- Declared for later R-steps; not consumed yet (kept here so the SPI shape is visible). ---
|
|
66
74
|
// adapter?: string; // codegen adapter id (existing adapterRegistry)
|
|
67
75
|
}
|
|
@@ -97,6 +105,11 @@ export class CapabilityRegistry {
|
|
|
97
105
|
return this.all().flatMap((c) => c.patterns ?? []);
|
|
98
106
|
}
|
|
99
107
|
|
|
108
|
+
/** All app-profile presets contributed by registered capabilities (later registration wins on key collision). */
|
|
109
|
+
appProfilePresets(): Record<string, unknown> {
|
|
110
|
+
return Object.assign({}, ...this.all().map((c) => c.appProfilePresets ?? {}));
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
/** All registered sensors, optionally filtered by kind ('advisory' | 'gate'). */
|
|
101
114
|
sensors(kind?: Sensor['kind']): Sensor[] {
|
|
102
115
|
const all = this.all().flatMap((c) => c.sensors ?? []);
|
|
@@ -355,11 +355,19 @@ function featureReferencesTestData(featureFile: string): boolean {
|
|
|
355
355
|
|
|
356
356
|
function checkSelectorsHasEntries(selectorsFile: string, screen: string): boolean {
|
|
357
357
|
if (!fs.existsSync(selectorsFile)) return false;
|
|
358
|
-
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
358
|
+
// Parse the YAML and count real top-level keys. The old regex heuristic required
|
|
359
|
+
// key names without whitespace, but sungen's own selector-key convention produces
|
|
360
|
+
// multi-word keys ("email address:", "contact points table:") — a file whose keys
|
|
361
|
+
// are ALL multi-word counted as 0 entries and falsely blocked the export.
|
|
362
|
+
try {
|
|
363
|
+
const parsed = parseYaml(fs.readFileSync(selectorsFile, 'utf-8'));
|
|
364
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return false;
|
|
365
|
+
// More than just the single `<screen>: page` entry.
|
|
366
|
+
return Object.keys(parsed as Record<string, unknown>).length > 1;
|
|
367
|
+
} catch {
|
|
368
|
+
// Malformed YAML — surface as "missing entries" so preflight points at the file.
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
363
371
|
}
|
|
364
372
|
|
|
365
373
|
export function hasBlockingMissing(p: PreflightCheck): boolean {
|
|
@@ -99,6 +99,7 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
99
99
|
.option('--all', 'Generate tests for all screens, flows, and API areas')
|
|
100
100
|
.option('--framework <name>', 'Override the platform driver (else read from qa/capabilities.yaml)')
|
|
101
101
|
.option('--inline-data', 'Hardcode test data at compile time instead of runtime loading')
|
|
102
|
+
.option('--strict', 'Exit non-zero when compile diagnostics are found (unmatched steps, dropped {{values}})')
|
|
102
103
|
.action(async (options) => {
|
|
103
104
|
try {
|
|
104
105
|
const screenName = options.screen;
|
|
@@ -192,6 +193,20 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
192
193
|
|
|
193
194
|
console.log(`\n${results.length} test file(s) generated.`);
|
|
194
195
|
|
|
196
|
+
// Compile diagnostics (SG-W001 unmatched step, SG-W002 dropped {{value}}, ...).
|
|
197
|
+
// Warning-only by default; --strict turns any diagnostic into exit 1.
|
|
198
|
+
const diagnostics = generator.getDiagnostics();
|
|
199
|
+
if (diagnostics.count > 0) {
|
|
200
|
+
console.log(`\n⚠️ Compile diagnostics (${diagnostics.count}):`);
|
|
201
|
+
for (const line of diagnostics.formatLines().slice(0, 20)) console.log(` ${line}`);
|
|
202
|
+
if (diagnostics.count > 20) console.log(` … and ${diagnostics.count - 20} more`);
|
|
203
|
+
console.log(` Unmatched steps compile to no-op // TODO comments — the test runs weaker than it reads.`);
|
|
204
|
+
if (options.strict) {
|
|
205
|
+
console.error(`\n--strict: failing generation on ${diagnostics.count} diagnostic(s).`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
195
210
|
// Security S0 — warn (never block) if test-data looks to hold a real secret.
|
|
196
211
|
const scanDirs: string[] = [];
|
|
197
212
|
if (screenName) scanDirs.push(path.join(process.cwd(), 'qa', 'screens', screenName));
|
|
@@ -20,6 +20,10 @@ 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
|
+
// `\\.` consumes any backslash-escaped char FIRST, so the wrapping quote may also appear
|
|
24
|
+
// ESCAPED inside the title (codegen emits `row\'s` for an apostrophe in a scenario name).
|
|
25
|
+
// Without it the title capture stopped at `\'` and the whole test() silently vanished from
|
|
26
|
+
// the parse — the scenario was then mislabelled "Not compiled"/N/A although it ran and passed.
|
|
23
27
|
// Quote char may be ' " or ` — the backtick form is a data-driven (@cases) title
|
|
24
28
|
// like `VP-… — ${__row.__label}`.
|
|
25
29
|
// The options object uses `\{.*?\}` (non-greedy, not `[^}]*`) so a tag value containing a
|
|
@@ -27,13 +31,14 @@ function extractTestBlock(content: string, startIdx: number): {
|
|
|
27
31
|
// single-line, so `.*?` anchored on `}, async` is safe.
|
|
28
32
|
// Accept both Playwright `test(...)` and mobile/Appium mocha `it(...)` (issue #391). The leading
|
|
29
33
|
// `\b` prevents `it(` from matching the tail of a larger word like `submit(` / `edit(`.
|
|
30
|
-
const testRegex = /\b(?:test|it)\s*\(\s*(['"`])((
|
|
34
|
+
const testRegex = /\b(?:test|it)\s*\(\s*(['"`])((?:\\.|(?!\1).)+)\1\s*,\s*(?:\{.*?\}\s*,\s*)?async\s*\([^)]*\)\s*=>\s*\{/g;
|
|
31
35
|
testRegex.lastIndex = startIdx;
|
|
32
36
|
const match = testRegex.exec(content);
|
|
33
37
|
if (!match) return null;
|
|
34
38
|
|
|
35
|
-
// [1] = outer quote char, [2] = title
|
|
36
|
-
|
|
39
|
+
// [1] = outer quote char, [2] = title. Unescape `\'`/`\"`/backtick/backslash: the results.json
|
|
40
|
+
// title is the RUNTIME string (`row's`), so the escaped source form would never match it.
|
|
41
|
+
const title = unescapeQuoted(match[2]);
|
|
37
42
|
const bodyStart = match.index + match[0].length;
|
|
38
43
|
|
|
39
44
|
// Find matching closing brace (accounting for nested braces)
|
|
@@ -54,6 +59,11 @@ function extractTestBlock(content: string, startIdx: number): {
|
|
|
54
59
|
return { title, body, endIdx };
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
/** Unescape backslash-escaped quote chars (and backslashes) captured from a quoted JS string. */
|
|
63
|
+
function unescapeQuoted(s: string): string {
|
|
64
|
+
return s.replace(/\\(['"`\\])/g, '$1');
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
/**
|
|
58
68
|
* Find describe('name', ...) blocks and return their names + body ranges.
|
|
59
69
|
*/
|
|
@@ -61,10 +71,12 @@ function extractDescribeBlocks(content: string): Array<{ name: string; bodyStart
|
|
|
61
71
|
const result: Array<{ name: string; bodyStart: number; bodyEnd: number }> = [];
|
|
62
72
|
// Accept Playwright `test.describe(...)` / `test.describe.serial(...)` AND mobile/Appium mocha
|
|
63
73
|
// `describe(...)` (issue #391). The optional `test.` prefix keeps web specs matching unchanged.
|
|
64
|
-
|
|
74
|
+
// Same escaped-quote handling as the test-title regex: a feature name with an apostrophe
|
|
75
|
+
// (describe('Manager\'s dashboard')) must not drop the whole describe block from the parse.
|
|
76
|
+
const regex = /(?:test\.)?describe(?:\.serial)?\s*\(\s*(['"])((?:\\.|(?!\1).)+)\1\s*,\s*\(\s*\)\s*=>\s*\{/g;
|
|
65
77
|
let match: RegExpExecArray | null;
|
|
66
78
|
while ((match = regex.exec(content)) !== null) {
|
|
67
|
-
const name = match[
|
|
79
|
+
const name = unescapeQuoted(match[2]);
|
|
68
80
|
const bodyStart = match.index + match[0].length;
|
|
69
81
|
let depth = 1;
|
|
70
82
|
let i = bodyStart;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
page.
|
|
1
|
+
page.once('dialog', dialog => dialog.accept());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
page.
|
|
1
|
+
page.once('dialog', dialog => dialog.dismiss());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
page.
|
|
1
|
+
page.once('dialog', dialog => dialog.accept('{{escapeQuotes fillValue}}'));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
await {{> locator-base}}.filter({ hasText: {{
|
|
1
|
+
await {{> locator-base}}.filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact legacyTail=(and (eq strategy "text") (eq value ""))}} }){{> locator-nth}}.click();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
await page.getByText('{{escapeQuotes selectorValue}}').filter({ hasText: {{
|
|
1
|
+
await page.getByText('{{escapeQuotes selectorValue}}').filter({ hasText: {{> has-text-value dataValue=dataValue exact=exact legacyTail=(eq selectorValue "")}} }){{#if (gte nth 0)}}.nth({{nth}}){{/if}}.{{action}}();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
await {{> locator}}.waitFor({ state: '{{state}}' });
|
|
1
|
+
{{#if (eq state "enabled")}}await expect({{> locator}}).toBeEnabled();{{else if (eq state "disabled")}}await expect({{> locator}}).toBeDisabled();{{else}}await {{> locator}}.waitFor({ state: '{{state}}' });{{/if}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{#if exact}}/^{{escapeRegex dataValue}}$/{{else if legacyTail}}/.*{{escapeRegex dataValue}}$/{{else}}'{{escapeQuotes dataValue}}'{{/if}}
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{{~#case 'id'}}locator('{{value}}'){{/case~}}
|
|
9
9
|
{{~#default}}locator('{{value}}'){{/default~}}
|
|
10
10
|
{{~/switch}}{{else~}}
|
|
11
|
-
{{#if scope}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{#switch strategy~}}
|
|
11
|
+
{{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
|
|
12
12
|
{{~#case 'testid'}}getByTestId('{{value}}'){{/case~}}
|
|
13
13
|
{{~#case 'role'}}{{#if name}}getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{else}}getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}){{/if}}{{/case~}}
|
|
14
14
|
{{~#case 'placeholder'}}getByPlaceholder('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{{~#case 'id'}}locator('{{value}}'){{/case~}}
|
|
9
9
|
{{~#default}}locator('{{value}}'){{/default~}}
|
|
10
10
|
{{~/switch}}{{> locator-nth}}{{else~}}
|
|
11
|
-
{{#if scope}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{#switch strategy~}}
|
|
11
|
+
{{#if scope}}{{#if (eq scope "dialog")}}{{pageRoot}}.{{else}}{{pageRoot}}.getByLabel('{{escapeQuotes scope}}').{{/if}}{{#switch strategy~}}
|
|
12
12
|
{{~#case 'testid'}}getByTestId('{{value}}'){{/case~}}
|
|
13
13
|
{{~#case 'role'}}{{#if name}}getByRole('{{role}}', { name: '{{escapeQuotes name}}'{{#if exact}}, exact: true{{/if}} }){{else}}getByRole('{{role}}'{{#if exact}}, { exact: true }{{/if}}){{/if}}{{/case~}}
|
|
14
14
|
{{~#case 'placeholder'}}getByPlaceholder('{{escapeQuotes value}}'{{#if exact}}, { exact: true }{{/if}}){{/case~}}
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-blur.hbs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await {{{targetExpr}}}.evaluate((el) => (el instanceof HTMLElement) && el.blur());
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-click.hbs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await {{{targetExpr}}}.click();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await expect({{{targetExpr}}}).toContainText({{{valueExpr}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await expect({{{targetExpr}}}).toHaveValue({{{valueExpr}}});
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-hover.hbs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await {{{targetExpr}}}.hover();
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-press.hbs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await page.keyboard.press('{{key}}');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await {{{targetExpr}}}.scrollIntoViewIfNeeded();
|
package/src/generators/test-generator/adapters/playwright/templates/steps/partials/recipe-type.hbs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await page.keyboard.type({{{valueExpr}}});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
await {{{targetExpr}}}.first().waitFor({ state: '{{state}}' });
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* App Profile (`qa/app.yaml`) — the per-project BEHAVIOR layer (spec:
|
|
7
|
+
* docs/spec/sungen_app_profile_spec.md). Three kinds of knowledge live in three files:
|
|
8
|
+
* INTENT → .feature + test-data (portable across projects)
|
|
9
|
+
* IDENTITY → selectors.yaml (per-screen: WHERE an element is)
|
|
10
|
+
* BEHAVIOR → qa/app.yaml (per-project: HOW this app's UI behaves)
|
|
11
|
+
*
|
|
12
|
+
* The file describes the app under test, never the tool (no timeouts/retries/sleeps —
|
|
13
|
+
* those belong to sungen.config.yaml). One file per project; presets merge in via
|
|
14
|
+
* `extends:`; per-element `variant:`/`scope:` in selectors.yaml wins over the profile.
|
|
15
|
+
* No file → the built-in defaults below → pre-profile behavior stays intact.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export interface AppProfile {
|
|
19
|
+
version?: number;
|
|
20
|
+
extends?: string | string[];
|
|
21
|
+
app?: { ui_library?: string; framework?: string; locales?: string[]; base_locale?: string };
|
|
22
|
+
widgets?: Record<string, string | { recipe?: string; variant?: string }>;
|
|
23
|
+
recipes?: Record<string, unknown>;
|
|
24
|
+
layers?: { portal_roles?: string[] };
|
|
25
|
+
feedback?: {
|
|
26
|
+
toast?: { role?: string; named_by?: 'content' | 'label'; stacking?: string };
|
|
27
|
+
loading?: { indicator?: string };
|
|
28
|
+
field_error?: { locate?: string; indicator?: string };
|
|
29
|
+
confirm?: string;
|
|
30
|
+
};
|
|
31
|
+
conventions?: Record<string, unknown>;
|
|
32
|
+
detection?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ProfileWarning {
|
|
36
|
+
code: 'SG-W005' | 'SG-W006';
|
|
37
|
+
message: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ResolvedAppProfile {
|
|
41
|
+
/** ARIA roles rendered via portal — never wrapped in dialog/landmark scope (issue #465). */
|
|
42
|
+
portalRoles: string[];
|
|
43
|
+
/** Roles whose accessible name derives from CONTENT — defaulting name from the Gherkin
|
|
44
|
+
* reference can never match (issue #467). */
|
|
45
|
+
contentNamedRoles: string[];
|
|
46
|
+
/** widget kind → variant/recipe name, normalized (`select: radix-select`). Per-element
|
|
47
|
+
* `variant:` in selectors.yaml overrides this. */
|
|
48
|
+
widgetVariants: Record<string, string>;
|
|
49
|
+
/** recipe name → { intent → primitive list } — built-in variant recipes merged under
|
|
50
|
+
* project-defined ones (project wins). */
|
|
51
|
+
recipes: Record<string, Record<string, unknown[]>>;
|
|
52
|
+
/** Refresh/loading signal for `wait for [T] table to refresh` (feedback.loading.indicator). */
|
|
53
|
+
loadingIndicator: string;
|
|
54
|
+
/** Positional-row conventions (`row {{n}}` steps): first row number + whether the header
|
|
55
|
+
* counts as a row in getByRole('row') indexing. */
|
|
56
|
+
tableRowBase: number;
|
|
57
|
+
tableHeaderIsRow: boolean;
|
|
58
|
+
/** The merged profile (presets ← project), or null when qa/app.yaml does not exist. */
|
|
59
|
+
raw: AppProfile | null;
|
|
60
|
+
warnings: ProfileWarning[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Portal-rendered option layers (Radix/MUI/Headless UI/antd portal to document.body). */
|
|
64
|
+
export const DEFAULT_PORTAL_ROLES = ['option', 'listbox', 'menu', 'menuitem', 'tooltip'];
|
|
65
|
+
|
|
66
|
+
/** ARIA name-from-content roles (per spec): a `{ name }` filter invented from the Gherkin
|
|
67
|
+
* reference text can never match these. */
|
|
68
|
+
export const DEFAULT_CONTENT_NAMED_ROLES = ['status', 'alert', 'log', 'marquee', 'timer'];
|
|
69
|
+
|
|
70
|
+
/** Default refresh signal — apps that set aria-busy while loading need zero config. */
|
|
71
|
+
export const DEFAULT_LOADING_INDICATOR = '[aria-busy="true"]';
|
|
72
|
+
|
|
73
|
+
const KNOWN_TOP_KEYS = ['version', 'extends', 'app', 'widgets', 'recipes', 'layers', 'feedback', 'conventions', 'detection'];
|
|
74
|
+
|
|
75
|
+
/** Reserved preset key: always merged as the base layer (built-in widget recipes ship here). */
|
|
76
|
+
const BUILTIN_PRESET_KEY = 'builtin';
|
|
77
|
+
|
|
78
|
+
export function defaultAppProfile(presets: Record<string, unknown> = {}): ResolvedAppProfile {
|
|
79
|
+
return finalizeProfile((presets[BUILTIN_PRESET_KEY] as AppProfile) ?? {}, null, []);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Derive the compiler-consumed slices from a merged profile document. */
|
|
83
|
+
function finalizeProfile(merged: AppProfile, raw: AppProfile | null, warnings: ProfileWarning[]): ResolvedAppProfile {
|
|
84
|
+
const portalRoles = merged.layers?.portal_roles ?? DEFAULT_PORTAL_ROLES;
|
|
85
|
+
const contentNamed = new Set(DEFAULT_CONTENT_NAMED_ROLES);
|
|
86
|
+
if (merged.feedback?.toast?.named_by === 'content' && merged.feedback.toast.role) {
|
|
87
|
+
contentNamed.add(merged.feedback.toast.role);
|
|
88
|
+
}
|
|
89
|
+
const widgetVariants: Record<string, string> = {};
|
|
90
|
+
for (const [kind, v] of Object.entries(merged.widgets ?? {})) {
|
|
91
|
+
const name = typeof v === 'string' ? v : (v?.recipe ?? v?.variant);
|
|
92
|
+
if (name) widgetVariants[kind] = name;
|
|
93
|
+
}
|
|
94
|
+
const conventions = merged.conventions as { table?: { row_base?: number; header_is_row?: boolean } } | undefined;
|
|
95
|
+
return {
|
|
96
|
+
portalRoles,
|
|
97
|
+
contentNamedRoles: [...contentNamed],
|
|
98
|
+
widgetVariants,
|
|
99
|
+
recipes: (merged.recipes ?? {}) as Record<string, Record<string, unknown[]>>,
|
|
100
|
+
loadingIndicator: merged.feedback?.loading?.indicator ?? DEFAULT_LOADING_INDICATOR,
|
|
101
|
+
tableRowBase: conventions?.table?.row_base ?? 1,
|
|
102
|
+
tableHeaderIsRow: conventions?.table?.header_is_row ?? false,
|
|
103
|
+
raw,
|
|
104
|
+
warnings,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Load and resolve `qa/app.yaml`: merge the `extends` preset chain (project wins),
|
|
110
|
+
* validate top-level keys, and derive the compiler-consumed slices. Never throws on
|
|
111
|
+
* a bad profile — problems surface as SG-W005/W006 diagnostics, and each slice falls
|
|
112
|
+
* back to its built-in default.
|
|
113
|
+
*/
|
|
114
|
+
export function loadAppProfile(cwd: string, presets: Record<string, unknown> = {}): ResolvedAppProfile {
|
|
115
|
+
const warnings: ProfileWarning[] = [];
|
|
116
|
+
const qaDir = path.join(cwd, 'qa');
|
|
117
|
+
|
|
118
|
+
// Per-environment profiles are unsupported BY DESIGN — widget behavior must not differ
|
|
119
|
+
// between environments (test-data/locale overlays already cover what legitimately does).
|
|
120
|
+
if (fs.existsSync(qaDir)) {
|
|
121
|
+
for (const name of fs.readdirSync(qaDir)) {
|
|
122
|
+
if (/^app\.[^.]+\.ya?ml$/.test(name)) {
|
|
123
|
+
warnings.push({
|
|
124
|
+
code: 'SG-W006',
|
|
125
|
+
message: `qa/${name}: per-environment app profiles are unsupported by design — describe the app once in qa/app.yaml`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const file = path.join(qaDir, 'app.yaml');
|
|
132
|
+
if (!fs.existsSync(file)) {
|
|
133
|
+
return { ...defaultAppProfile(presets), warnings };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let doc: AppProfile;
|
|
137
|
+
try {
|
|
138
|
+
doc = (yaml.parse(fs.readFileSync(file, 'utf-8')) ?? {}) as AppProfile;
|
|
139
|
+
} catch (e) {
|
|
140
|
+
warnings.push({ code: 'SG-W005', message: `qa/app.yaml: unparseable YAML (${e instanceof Error ? e.message : e}) — using built-in defaults` });
|
|
141
|
+
return { ...defaultAppProfile(presets), warnings };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
for (const key of Object.keys(doc)) {
|
|
145
|
+
if (!KNOWN_TOP_KEYS.includes(key)) {
|
|
146
|
+
warnings.push({ code: 'SG-W005', message: `qa/app.yaml: unknown key "${key}" (known: ${KNOWN_TOP_KEYS.join(', ')})` });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// extends: preset chain, merged in order, project file last (narrow beats broad).
|
|
151
|
+
// The reserved `builtin` preset (built-in widget recipes from driver-ui) is always
|
|
152
|
+
// the base layer, so per-element `variant: radix-select` works without any extends.
|
|
153
|
+
const extendNames = doc.extends ? (Array.isArray(doc.extends) ? doc.extends : [doc.extends]) : [];
|
|
154
|
+
const availableNames = Object.keys(presets).filter((k) => k !== BUILTIN_PRESET_KEY);
|
|
155
|
+
const chain: AppProfile[] = presets[BUILTIN_PRESET_KEY] ? [presets[BUILTIN_PRESET_KEY] as AppProfile] : [];
|
|
156
|
+
for (const name of extendNames) {
|
|
157
|
+
const key = String(name).replace(/^@sungen\/app-/, '');
|
|
158
|
+
const preset = presets[key] as AppProfile | undefined;
|
|
159
|
+
if (!preset || key === BUILTIN_PRESET_KEY) {
|
|
160
|
+
warnings.push({
|
|
161
|
+
code: 'SG-W005',
|
|
162
|
+
message: `qa/app.yaml: unknown preset "${name}" in extends (available: ${availableNames.join(', ') || 'none — is the driver that ships it installed?'})`,
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
chain.push(preset);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const merged = [...chain, doc].reduce<AppProfile>((acc, layer) => deepMerge(acc, layer) as AppProfile, {});
|
|
169
|
+
|
|
170
|
+
return finalizeProfile(merged, merged, warnings);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Plain-object deep merge; arrays and scalars replace (a preset's list is a whole answer). */
|
|
174
|
+
function deepMerge(base: unknown, layer: unknown): unknown {
|
|
175
|
+
if (!isPlainObject(base) || !isPlainObject(layer)) return layer === undefined ? base : layer;
|
|
176
|
+
const out: Record<string, unknown> = { ...base };
|
|
177
|
+
for (const [k, v] of Object.entries(layer)) {
|
|
178
|
+
out[k] = k in out ? deepMerge(out[k], v) : v;
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
184
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
185
|
+
}
|
|
@@ -4,6 +4,7 @@ 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 { DiagnosticsCollector } from './diagnostics';
|
|
7
8
|
import { capabilityRegistry } from '../../capabilities/registry';
|
|
8
9
|
import { discoverAndRegisterCapabilities } from '../../capabilities/discover';
|
|
9
10
|
import { readCapabilities } from '../../harness/capability';
|
|
@@ -227,10 +228,13 @@ export class CodeGenerator {
|
|
|
227
228
|
// Steps registry built per feature during generateTestCode(); used by countSteps()
|
|
228
229
|
private stepsRegistry = new Map<string, ParsedScenario>();
|
|
229
230
|
|
|
231
|
+
private diagnostics = new DiagnosticsCollector();
|
|
232
|
+
|
|
230
233
|
constructor(options: { useAI?: boolean; verbose?: boolean; framework?: string; baseURL?: string; screenName?: string; runtimeData?: boolean; flowMode?: boolean } = {}) {
|
|
231
234
|
this.options = options;
|
|
232
235
|
this.screenName = options.screenName;
|
|
233
236
|
this.stepMapper = new StepMapper(options);
|
|
237
|
+
this.stepMapper.setDiagnostics(this.diagnostics);
|
|
234
238
|
// Get adapter from registry. Platform comes from the caller (resolved from
|
|
235
239
|
// qa/capabilities.yaml); default `web` = the bundled Playwright adapter
|
|
236
240
|
// (back-compat baseline). External platforms (e.g. mobile) are loaded by the
|
|
@@ -243,6 +247,11 @@ export class CodeGenerator {
|
|
|
243
247
|
}
|
|
244
248
|
}
|
|
245
249
|
|
|
250
|
+
/** Compile diagnostics collected across all generateTest calls (SG-W001/W002...) */
|
|
251
|
+
getDiagnostics(): DiagnosticsCollector {
|
|
252
|
+
return this.diagnostics;
|
|
253
|
+
}
|
|
254
|
+
|
|
246
255
|
/**
|
|
247
256
|
* Generate test file from parsed feature
|
|
248
257
|
*/
|