@sun-asterisk/sungen 3.2.9 → 3.2.10-beta.11
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.d.ts.map +1 -1
- package/dist/capabilities/discover.js +5 -2
- 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/capability.d.ts.map +1 -1
- package/dist/cli/commands/capability.js +44 -2
- package/dist/cli/commands/capability.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/capability.d.ts +1 -0
- package/dist/harness/capability.d.ts.map +1 -1
- package/dist/harness/capability.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +2 -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 +99 -0
- package/dist/orchestrator/templates/ai-src/commands/create-test.md +3 -0
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -0
- 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 +116 -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 +43 -8
- 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/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/dist/orchestrator/templates/specs-db.d.ts +201 -11
- package/dist/orchestrator/templates/specs-db.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-db.js +440 -91
- package/dist/orchestrator/templates/specs-db.js.map +1 -1
- package/dist/orchestrator/templates/specs-db.ts +463 -79
- package/dist/orchestrator/templates/specs-test-data.ts +39 -3
- package/package.json +8 -4
- package/src/capabilities/discover.ts +5 -2
- package/src/capabilities/registry.ts +13 -0
- package/src/cli/commands/capability.ts +40 -2
- 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/capability.ts +1 -0
- package/src/harness/catalog/drivers.yaml +2 -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 +99 -0
- package/src/orchestrator/templates/ai-src/commands/create-test.md +3 -0
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -0
- 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 +116 -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 +43 -8
- 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/ai-src/skills/sungen-tc-generation/SKILL.md +1 -0
- package/src/orchestrator/templates/specs-db.ts +463 -79
- 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.11",
|
|
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 && tsx tests/db-runtime/sql-placeholder-rewrite.run.ts && tsx tests/db-runtime/fallback-flow-codified.run.ts && tsx tests/db-runtime/mysql-integration.opt-in.run.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
|
},
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
"@babel/types": "^7.28.5",
|
|
40
40
|
"@cucumber/gherkin": "^37.0.0",
|
|
41
41
|
"@cucumber/messages": "^31.0.0",
|
|
42
|
-
"@sungen/driver-
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.10-beta.11",
|
|
43
|
+
"@sungen/driver-ui": "3.2.10-beta.11",
|
|
43
44
|
"chalk": "^5.6.2",
|
|
44
45
|
"commander": "^14.0.2",
|
|
45
46
|
"dotenv": "^17.2.3",
|
|
@@ -65,6 +66,9 @@
|
|
|
65
66
|
"LICENSE"
|
|
66
67
|
],
|
|
67
68
|
"devDependencies": {
|
|
68
|
-
"mysql2": "^3"
|
|
69
|
+
"mysql2": "^3",
|
|
70
|
+
"mongodb": "^7",
|
|
71
|
+
"@aws-sdk/client-dynamodb": "^3",
|
|
72
|
+
"@aws-sdk/lib-dynamodb": "^3"
|
|
69
73
|
}
|
|
70
74
|
}
|
|
@@ -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
|
|
@@ -36,7 +36,10 @@ const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/dri
|
|
|
36
36
|
* symlink for the package is missing). Surface it LOUDLY: otherwise the entire UI step vocabulary is
|
|
37
37
|
* absent and every step silently compiles to "Unrecognized step pattern" with no clue why.
|
|
38
38
|
*/
|
|
39
|
-
|
|
39
|
+
// `@sungen/driver-data-factory` is bundled too — but it is RUNTIME-FREE (only a knowledge catalog +
|
|
40
|
+
// codegen), so shipping it by default adds no runtime, unlike ui/db/api/mobile. It makes standardized
|
|
41
|
+
// test-data available out of the box (no `capability add` needed); inert until a field-map/recipe exists.
|
|
42
|
+
const BUNDLED_DRIVERS = new Set(['@sungen/driver-ui', '@sungen/driver-data-factory']);
|
|
40
43
|
|
|
41
44
|
function loadExternalDriver(name: string): void {
|
|
42
45
|
// Resolve from the user's PROJECT first, then from core's own location. Opt-in drivers
|
|
@@ -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 ?? []);
|
|
@@ -15,6 +15,36 @@ function coreVersion(): string {
|
|
|
15
15
|
try { return require('../../../package.json').version || 'latest'; } catch { return 'latest'; }
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the driver version to install for the running core `core`.
|
|
20
|
+
*
|
|
21
|
+
* Lockstep wants the EXACT `core` version. But the beta channel publishes `<core>-beta.N` before a
|
|
22
|
+
* stable `<core>` ever exists — so a released core (e.g. 3.2.10) asking for `@3.2.10` hits ETARGET
|
|
23
|
+
* out-of-the-box when npm only has `3.2.10-beta.2` (`sungen capability add` then fails for a fresh
|
|
24
|
+
* user). Fall back on the SAME version line: prefer exact `core`, else the highest `core-beta.N`,
|
|
25
|
+
* else the `beta` dist-tag as a last resort. Keeps the driver's pinned core-dep on the same line.
|
|
26
|
+
*/
|
|
27
|
+
function resolveDriverVersion(pkg: string, core: string): string {
|
|
28
|
+
if (core === 'latest') return 'beta';
|
|
29
|
+
try {
|
|
30
|
+
const out = spawnSync('npm', ['view', pkg, 'versions', '--json'], { encoding: 'utf-8', shell: true });
|
|
31
|
+
if (out.status !== 0 || !out.stdout) return core; // can't query → let the exact install try/fail
|
|
32
|
+
let versions: string[] = [];
|
|
33
|
+
const parsed = JSON.parse(out.stdout);
|
|
34
|
+
versions = Array.isArray(parsed) ? parsed : [parsed];
|
|
35
|
+
if (versions.includes(core)) return core; // exact exists → lockstep
|
|
36
|
+
const prefix = `${core}-beta.`;
|
|
37
|
+
const betas = versions
|
|
38
|
+
.filter((v) => typeof v === 'string' && v.startsWith(prefix))
|
|
39
|
+
.map((v) => parseInt(v.slice(prefix.length), 10))
|
|
40
|
+
.filter((n) => Number.isInteger(n));
|
|
41
|
+
if (betas.length) return `${core}-beta.${Math.max(...betas)}`; // highest same-line prerelease
|
|
42
|
+
return 'beta'; // nothing on this line → newest beta dist-tag
|
|
43
|
+
} catch {
|
|
44
|
+
return core;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
18
48
|
/**
|
|
19
49
|
* Add `test:mobile` to the project's package.json scripts (if absent).
|
|
20
50
|
* Idempotent — skips if the script is already set.
|
|
@@ -187,14 +217,22 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
187
217
|
}
|
|
188
218
|
|
|
189
219
|
const adapterName = meta.adapter || meta.id;
|
|
190
|
-
|
|
220
|
+
// Bundled = a platform adapter baked into core (e.g. `web`), OR a capability driver shipped
|
|
221
|
+
// as a core dependency (`drivers.yaml` bundled: true, e.g. data-factory) — present already,
|
|
222
|
+
// so no install/verify: `capability add` on it just records it in the profile.
|
|
223
|
+
const bundled = adapterRegistry.hasAdapter(adapterName) || meta.bundled === true;
|
|
191
224
|
|
|
192
225
|
if (!bundled && !o.skipInstall) {
|
|
193
226
|
// Pin the driver to the RUNNING core's exact version (lockstep). Without this, plain
|
|
194
227
|
// `npm install @sungen/driver-x` resolves the package's `latest` dist-tag — which on a
|
|
195
228
|
// beta channel can lag (e.g. a prerelease first-published-as-latest), pulling a driver
|
|
196
229
|
// whose core dep resolves to the wrong @sun-asterisk/sungen and fails to load.
|
|
197
|
-
const
|
|
230
|
+
const core = coreVersion();
|
|
231
|
+
const target = resolveDriverVersion(meta.package, core); // exact core, else same-line beta, else 'beta' tag
|
|
232
|
+
const spec = `${meta.package}@${target}`;
|
|
233
|
+
if (target !== core) {
|
|
234
|
+
console.log(`ℹ ${meta.package}@${core} is not published yet — installing ${spec} (closest on the ${core} line).`);
|
|
235
|
+
}
|
|
198
236
|
console.log(`📦 Installing ${spec} (dev dependency)...`);
|
|
199
237
|
const r = spawnSync('npm', ['install', '-D', spec], { stdio: 'inherit', shell: true });
|
|
200
238
|
if (r.status !== 0) throw new Error(`npm install -D ${spec} failed.`);
|
|
@@ -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}}' });
|