@sun-asterisk/sungen 3.2.18 → 3.2.19
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 +4 -1
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/cli/commands/add.d.ts.map +1 -1
- package/dist/cli/commands/add.js +4 -2
- package/dist/cli/commands/add.js.map +1 -1
- package/dist/cli/commands/dashboard.d.ts.map +1 -1
- package/dist/cli/commands/dashboard.js +10 -1
- package/dist/cli/commands/dashboard.js.map +1 -1
- package/dist/cli/commands/delivery.d.ts.map +1 -1
- package/dist/cli/commands/delivery.js +19 -1
- 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 +4 -1
- package/dist/cli/commands/generate.js.map +1 -1
- package/dist/dashboard/html-renderer.d.ts.map +1 -1
- package/dist/dashboard/html-renderer.js +7 -2
- package/dist/dashboard/html-renderer.js.map +1 -1
- package/dist/dashboard/snapshot-builder.d.ts.map +1 -1
- package/dist/dashboard/snapshot-builder.js +3 -0
- package/dist/dashboard/snapshot-builder.js.map +1 -1
- package/dist/dashboard/types.d.ts +4 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/exporters/api-environment.d.ts.map +1 -1
- package/dist/exporters/api-environment.js +4 -24
- package/dist/exporters/api-environment.js.map +1 -1
- package/dist/exporters/api-testcase-formatter.d.ts +6 -0
- package/dist/exporters/api-testcase-formatter.d.ts.map +1 -1
- package/dist/exporters/api-testcase-formatter.js +11 -4
- package/dist/exporters/api-testcase-formatter.js.map +1 -1
- package/dist/exporters/csv-exporter.d.ts.map +1 -1
- package/dist/exporters/csv-exporter.js +40 -9
- package/dist/exporters/csv-exporter.js.map +1 -1
- package/dist/exporters/env-qa.d.ts +13 -0
- package/dist/exporters/env-qa.d.ts.map +1 -0
- package/dist/exporters/env-qa.js +82 -0
- package/dist/exporters/env-qa.js.map +1 -0
- package/dist/exporters/feature-parser.d.ts.map +1 -1
- package/dist/exporters/feature-parser.js +16 -33
- package/dist/exporters/feature-parser.js.map +1 -1
- package/dist/exporters/json-exporter.d.ts.map +1 -1
- package/dist/exporters/json-exporter.js +57 -20
- package/dist/exporters/json-exporter.js.map +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +6 -5
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/gates.d.ts.map +1 -1
- package/dist/exporters/matrix/gates.js +3 -2
- package/dist/exporters/matrix/gates.js.map +1 -1
- package/dist/exporters/matrix/step-buckets.d.ts +13 -0
- package/dist/exporters/matrix/step-buckets.d.ts.map +1 -0
- package/dist/exporters/matrix/step-buckets.js +7 -0
- package/dist/exporters/matrix/step-buckets.js.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts +43 -0
- package/dist/exporters/qa-env-secret-denylist.d.ts.map +1 -0
- package/dist/exporters/qa-env-secret-denylist.js +143 -0
- package/dist/exporters/qa-env-secret-denylist.js.map +1 -0
- package/dist/exporters/scenario-merger.d.ts +20 -13
- package/dist/exporters/scenario-merger.d.ts.map +1 -1
- package/dist/exporters/scenario-merger.js +55 -30
- package/dist/exporters/scenario-merger.js.map +1 -1
- package/dist/exporters/step-formatter.d.ts +42 -33
- package/dist/exporters/step-formatter.d.ts.map +1 -1
- package/dist/exporters/step-formatter.js +68 -68
- package/dist/exporters/step-formatter.js.map +1 -1
- package/dist/exporters/types.d.ts +9 -8
- package/dist/exporters/types.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts +1 -0
- package/dist/generators/test-generator/adapters/adapter-interface.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts +1 -0
- package/dist/generators/test-generator/adapters/appium/appium-adapter.d.ts.map +1 -1
- package/dist/generators/test-generator/adapters/appium/appium-adapter.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -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/steps/actions/keyboard-global-action.hbs +3 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +70 -7
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +7 -1
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +6 -0
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts +1 -1
- package/dist/generators/test-generator/recipe-expander.d.ts.map +1 -1
- package/dist/generators/test-generator/recipe-expander.js +1 -1
- package/dist/generators/test-generator/recipe-expander.js.map +1 -1
- 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 +108 -17
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/template-engine.d.ts +1 -0
- package/dist/generators/test-generator/template-engine.d.ts.map +1 -1
- package/dist/generators/test-generator/template-engine.js +1 -1
- package/dist/generators/test-generator/template-engine.js.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts +7 -1
- package/dist/generators/test-generator/utils/data-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/data-resolver.js +8 -4
- package/dist/generators/test-generator/utils/data-resolver.js.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts +2 -2
- package/dist/generators/test-generator/utils/runtime-data-transformer.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/runtime-data-transformer.js +56 -11
- package/dist/generators/test-generator/utils/runtime-data-transformer.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +18 -2
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +29 -6
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +8 -7
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +2 -3
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/challenge.d.ts.map +1 -1
- package/dist/harness/challenge.js +1 -55
- package/dist/harness/challenge.js.map +1 -1
- package/dist/harness/data-driven-lint.d.ts.map +1 -1
- package/dist/harness/data-driven-lint.js +14 -9
- package/dist/harness/data-driven-lint.js.map +1 -1
- package/dist/harness/depth-lint.d.ts.map +1 -1
- package/dist/harness/depth-lint.js +1 -3
- package/dist/harness/depth-lint.js.map +1 -1
- package/dist/harness/flow-check.d.ts.map +1 -1
- package/dist/harness/flow-check.js +9 -7
- package/dist/harness/flow-check.js.map +1 -1
- package/dist/harness/flow-plan.d.ts.map +1 -1
- package/dist/harness/flow-plan.js +4 -3
- package/dist/harness/flow-plan.js.map +1 -1
- package/dist/harness/manifest.js +2 -2
- package/dist/harness/manifest.js.map +1 -1
- package/dist/harness/parse.d.ts +12 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +28 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/script-check.d.ts.map +1 -1
- package/dist/harness/script-check.js +52 -30
- package/dist/harness/script-check.js.map +1 -1
- package/dist/harness/secret-scan.d.ts +3 -1
- package/dist/harness/secret-scan.d.ts.map +1 -1
- package/dist/harness/secret-scan.js +14 -10
- package/dist/harness/secret-scan.js.map +1 -1
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +15 -4
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/trace.js +1 -1
- package/dist/harness/trace.js.map +1 -1
- package/dist/harness/unit-paths.d.ts +16 -0
- package/dist/harness/unit-paths.d.ts.map +1 -1
- package/dist/harness/unit-paths.js +39 -2
- package/dist/harness/unit-paths.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/screen-manager.d.ts.map +1 -1
- package/dist/orchestrator/screen-manager.js +8 -2
- package/dist/orchestrator/screen-manager.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/dist/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/dist/orchestrator/templates/playwright.config.ts +33 -2
- package/dist/orchestrator/templates/specs-mock.d.ts +66 -0
- package/dist/orchestrator/templates/specs-mock.d.ts.map +1 -0
- package/dist/orchestrator/templates/specs-mock.js +137 -0
- package/dist/orchestrator/templates/specs-mock.js.map +1 -0
- package/dist/orchestrator/templates/specs-mock.ts +179 -0
- package/dist/orchestrator/test-data-guide.d.ts.map +1 -1
- package/dist/orchestrator/test-data-guide.js +4 -0
- package/dist/orchestrator/test-data-guide.js.map +1 -1
- package/package.json +3 -3
- package/src/capabilities/discover.ts +4 -1
- package/src/cli/commands/add.ts +4 -2
- package/src/cli/commands/dashboard.ts +10 -1
- package/src/cli/commands/delivery.ts +19 -1
- package/src/cli/commands/generate.ts +4 -1
- package/src/dashboard/html-renderer.ts +8 -2
- package/src/dashboard/snapshot-builder.ts +3 -0
- package/src/dashboard/types.ts +4 -0
- package/src/exporters/api-environment.ts +3 -20
- package/src/exporters/api-testcase-formatter.ts +8 -2
- package/src/exporters/csv-exporter.ts +47 -8
- package/src/exporters/env-qa.ts +41 -0
- package/src/exporters/feature-parser.ts +17 -38
- package/src/exporters/json-exporter.ts +71 -24
- package/src/exporters/matrix/build.ts +6 -5
- package/src/exporters/matrix/gates.ts +3 -2
- package/src/exporters/matrix/step-buckets.ts +15 -0
- package/src/exporters/qa-env-secret-denylist.ts +118 -0
- package/src/exporters/scenario-merger.ts +80 -42
- package/src/exporters/step-formatter.ts +67 -66
- package/src/exporters/types.ts +9 -8
- package/src/generators/test-generator/adapters/adapter-interface.ts +1 -1
- package/src/generators/test-generator/adapters/appium/appium-adapter.ts +1 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/keyboard-global-action.hbs +3 -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/steps/actions/keyboard-global-action.hbs +3 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/storage-key-assertion.hbs +4 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/tab-order-assertion.hbs +30 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/navigation/route-assertion.hbs +1 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/dialog-root.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +10 -0
- package/src/generators/test-generator/code-generator.ts +82 -7
- package/src/generators/test-generator/diagnostics.ts +7 -1
- package/src/generators/test-generator/recipe-expander.ts +2 -2
- package/src/generators/test-generator/step-mapper.ts +106 -15
- package/src/generators/test-generator/template-engine.ts +2 -2
- package/src/generators/test-generator/utils/data-resolver.ts +8 -5
- package/src/generators/test-generator/utils/runtime-data-transformer.ts +57 -11
- package/src/generators/test-generator/utils/selector-resolver.ts +29 -5
- package/src/harness/audit.ts +10 -9
- package/src/harness/capability-plan.ts +2 -2
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/challenge.ts +2 -3
- package/src/harness/data-driven-lint.ts +13 -9
- package/src/harness/depth-lint.ts +2 -4
- package/src/harness/flow-check.ts +9 -7
- package/src/harness/flow-plan.ts +4 -3
- package/src/harness/manifest.ts +3 -3
- package/src/harness/parse.ts +28 -1
- package/src/harness/script-check.ts +52 -32
- package/src/harness/secret-scan.ts +11 -7
- package/src/harness/sensors.ts +15 -4
- package/src/harness/trace.ts +2 -2
- package/src/harness/unit-paths.ts +39 -2
- package/src/index.ts +1 -0
- package/src/orchestrator/screen-manager.ts +7 -2
- package/src/orchestrator/templates/ai-src/commands/dashboard.md +2 -1
- package/src/orchestrator/templates/ai-src/commands/run-test.md +1 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +2 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +51 -2
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +6 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +6 -0
- package/src/orchestrator/templates/playwright.config.ts +33 -2
- package/src/orchestrator/templates/specs-mock.ts +179 -0
- package/src/orchestrator/test-data-guide.ts +4 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mock = void 0;
|
|
4
|
+
/** Resource types the fail-on-unmocked guard judges; everything else passes through.
|
|
5
|
+
* NOTE: page.route() does NOT intercept WebSocket connections (that is
|
|
6
|
+
* page.routeWebSocket, a separate API) — the shipped posture is fetch/XHR only. */
|
|
7
|
+
const GUARDED_TYPES = new Set(['fetch', 'xhr']);
|
|
8
|
+
class MockRouter {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.state = new WeakMap();
|
|
11
|
+
}
|
|
12
|
+
stateFor(page) {
|
|
13
|
+
let st = this.state.get(page);
|
|
14
|
+
if (!st) {
|
|
15
|
+
st = { counts: new Map(), installed: new Set(), routes: [], guardInstalled: false, passthrough: false, violations: [], backgroundRouteCount: -1, backgroundNames: new Set() };
|
|
16
|
+
this.state.set(page, st);
|
|
17
|
+
}
|
|
18
|
+
return st;
|
|
19
|
+
}
|
|
20
|
+
/** `@mock:passthrough` — unmatched requests reach the real backend for this page/scenario.
|
|
21
|
+
* Also forgives violations already recorded (Background installs + goto run BEFORE the
|
|
22
|
+
* scenario body can set this flag — boot-time requests of an opted-out scenario must not
|
|
23
|
+
* fail its assertNoUnmocked verdict). The compiler additionally arms the flag inside
|
|
24
|
+
* beforeEach via test.info().tags, before the Background installs, so boot requests are
|
|
25
|
+
* not even aborted on the default path. */
|
|
26
|
+
setPassthrough(page) {
|
|
27
|
+
const st = this.stateFor(page);
|
|
28
|
+
st.passthrough = true;
|
|
29
|
+
st.violations.length = 0;
|
|
30
|
+
}
|
|
31
|
+
/** Install one catalog entry. The first install also arms the fail-on-unmocked guard. */
|
|
32
|
+
async install(page, cfg) {
|
|
33
|
+
const st = this.stateFor(page);
|
|
34
|
+
if (!st.guardInstalled) {
|
|
35
|
+
st.guardInstalled = true;
|
|
36
|
+
const guard = async (route) => {
|
|
37
|
+
const req = route.request();
|
|
38
|
+
if (!GUARDED_TYPES.has(req.resourceType()) || st.passthrough)
|
|
39
|
+
return route.fallback();
|
|
40
|
+
// Record + abort — NEVER throw here: a route handler runs outside any test
|
|
41
|
+
// stack, so a throw surfaces as an unhandled rejection (a hard process crash
|
|
42
|
+
// outside @playwright/test, flaky attribution inside it). The compiler emits
|
|
43
|
+
// `await mock.assertNoUnmocked(page)` at the end of every @mock scenario,
|
|
44
|
+
// which fails the test deterministically with the FULL violation list.
|
|
45
|
+
st.violations.push(`${req.method()} ${req.url()}`);
|
|
46
|
+
await route.abort('failed');
|
|
47
|
+
};
|
|
48
|
+
await page.route('**/*', guard); // FIRST → matched last → every named mock wins
|
|
49
|
+
st.routes.push({ pattern: '**/*', handler: guard });
|
|
50
|
+
}
|
|
51
|
+
const pattern = cfg.url.startsWith('re:') ? new RegExp(cfg.url.slice(3)) : cfg.url;
|
|
52
|
+
const handler = async (route) => {
|
|
53
|
+
if (cfg.method !== '*' && route.request().method() !== cfg.method)
|
|
54
|
+
return route.fallback();
|
|
55
|
+
st.counts.set(cfg.name, (st.counts.get(cfg.name) ?? 0) + 1);
|
|
56
|
+
if (cfg.delay_ms)
|
|
57
|
+
await new Promise((r) => setTimeout(r, cfg.delay_ms));
|
|
58
|
+
// A delayed fulfill can race a page close / reset at teardown — that race must
|
|
59
|
+
// not surface as an unhandled rejection from inside a route handler.
|
|
60
|
+
try {
|
|
61
|
+
await route.fulfill({ status: cfg.status, headers: cfg.headers, body: cfg.body });
|
|
62
|
+
}
|
|
63
|
+
catch { /* page closing */ }
|
|
64
|
+
};
|
|
65
|
+
await page.route(pattern, handler);
|
|
66
|
+
st.routes.push({ pattern, handler });
|
|
67
|
+
st.installed.add(cfg.name);
|
|
68
|
+
if (!st.counts.has(cfg.name))
|
|
69
|
+
st.counts.set(cfg.name, 0);
|
|
70
|
+
}
|
|
71
|
+
/** Handled-request count for a mock installed in THIS scenario — throws for a name
|
|
72
|
+
* that was never installed here (an exact 0 on an uninstalled mock must be a loud
|
|
73
|
+
* error, not a passing assertion). */
|
|
74
|
+
callCount(page, name) {
|
|
75
|
+
const st = this.state.get(page);
|
|
76
|
+
if (!st || !st.installed.has(name)) {
|
|
77
|
+
throw new Error(`mock "${name}" was never installed in this scenario — add \`Given [${name}] mock is active\``);
|
|
78
|
+
}
|
|
79
|
+
return st.counts.get(name) ?? 0;
|
|
80
|
+
}
|
|
81
|
+
/** Fail-on-unmocked verdict — emitted by the compiler as the LAST statement of every
|
|
82
|
+
* @mock scenario. Throws with the full list of aborted unmatched requests; a scenario
|
|
83
|
+
* whose app swallowed those failed fetches must not stay green for the wrong reason. */
|
|
84
|
+
async assertNoUnmocked(page) {
|
|
85
|
+
const st = this.state.get(page);
|
|
86
|
+
if (!st || st.violations.length === 0)
|
|
87
|
+
return;
|
|
88
|
+
const list = st.violations.slice();
|
|
89
|
+
st.violations.length = 0;
|
|
90
|
+
throw new Error(`${list.length} unmocked request(s) hit the fail-on-unmocked guard:\n - ${list.join('\n - ')}\n` +
|
|
91
|
+
'Add entries to mock/mocks.yaml, or opt out with @mock:passthrough.');
|
|
92
|
+
}
|
|
93
|
+
/** @serial only, emitted by the compiler at the END of the Background block (beforeAll):
|
|
94
|
+
* everything installed so far — Background mocks and the guard — persists across the
|
|
95
|
+
* feature's scenarios; reset() removes only what each scenario added on top. Without
|
|
96
|
+
* this, scenario 1's reset would kill the Background mocks and scenarios 2+ would run
|
|
97
|
+
* against the REAL backend with no guard — the silent-green class this driver exists
|
|
98
|
+
* to prevent. */
|
|
99
|
+
snapshotBackground(page) {
|
|
100
|
+
const st = this.stateFor(page);
|
|
101
|
+
st.backgroundRouteCount = st.routes.length;
|
|
102
|
+
st.backgroundNames = new Set(st.installed);
|
|
103
|
+
}
|
|
104
|
+
/** Scoped teardown for the @serial shared page: unroute exactly what THIS scenario
|
|
105
|
+
* installed (Background routes captured by snapshotBackground persist), zero every
|
|
106
|
+
* counter, clear flags/violations. Emitted at the end of each @mock scenario on the
|
|
107
|
+
* serial path; the default per-test path never needs it (page dies with the test). */
|
|
108
|
+
async reset(page) {
|
|
109
|
+
const st = this.state.get(page);
|
|
110
|
+
if (!st)
|
|
111
|
+
return;
|
|
112
|
+
if (st.backgroundRouteCount < 0) {
|
|
113
|
+
// No Background snapshot → drop everything (the pre-snapshot / default behavior).
|
|
114
|
+
for (const r of st.routes) {
|
|
115
|
+
try {
|
|
116
|
+
await page.unroute(r.pattern, r.handler);
|
|
117
|
+
}
|
|
118
|
+
catch { /* page may be closing */ }
|
|
119
|
+
}
|
|
120
|
+
this.state.delete(page);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
for (const r of st.routes.slice(st.backgroundRouteCount)) {
|
|
124
|
+
try {
|
|
125
|
+
await page.unroute(r.pattern, r.handler);
|
|
126
|
+
}
|
|
127
|
+
catch { /* page may be closing */ }
|
|
128
|
+
}
|
|
129
|
+
st.routes.length = st.backgroundRouteCount;
|
|
130
|
+
st.installed = new Set(st.backgroundNames);
|
|
131
|
+
st.counts = new Map();
|
|
132
|
+
st.violations.length = 0;
|
|
133
|
+
st.passthrough = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
exports.mock = new MockRouter();
|
|
137
|
+
//# sourceMappingURL=specs-mock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"specs-mock.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-mock.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
/**
|
|
3
|
+
* Sungen Mock Driver — runtime network-mocking helper (auto-generated into specs/mock.ts).
|
|
4
|
+
*
|
|
5
|
+
* One module singleton (same import shape as db/api); ALL state lives per-page in a
|
|
6
|
+
* WeakMap<Page, MockState>, so routes and call counters follow the page's lifetime on
|
|
7
|
+
* both isolation paths: a fresh page per test destroys them for free, and the @serial
|
|
8
|
+
* shared page is reset by the compiler-emitted mock.reset(page) at the end of each
|
|
9
|
+
* @mock scenario (scoped — only routes THIS router installed are unrouted; a blanket
|
|
10
|
+
* page.unrouteAll() would also kill Background-installed mocks and third-party routes).
|
|
11
|
+
*
|
|
12
|
+
* Blocking posture (fail-on-unmocked, the project default): the first install() also
|
|
13
|
+
* registers a catch-all route. Playwright matches routes last-registered-first, so
|
|
14
|
+
* registering the catch-all FIRST makes every named mock win over it. The catch-all
|
|
15
|
+
* only judges fetch/XHR/websocket traffic — documents/scripts/styles/images pass, the
|
|
16
|
+
* app shell must still load; what is being isolated is API traffic. An unmatched API
|
|
17
|
+
* request aborts and throws (fails the test) unless the scenario opted out via
|
|
18
|
+
* @mock:passthrough (compiled to mock.setPassthrough(page)).
|
|
19
|
+
*
|
|
20
|
+
* DO NOT EDIT — regenerated by `sungen generate`.
|
|
21
|
+
*/
|
|
22
|
+
import type { Page, Route } from '@playwright/test';
|
|
23
|
+
|
|
24
|
+
export interface MockConfig {
|
|
25
|
+
name: string;
|
|
26
|
+
method: string; // HTTP verb or '*'
|
|
27
|
+
url: string; // page.route glob, or 're:<pattern>' for a regex
|
|
28
|
+
status: number;
|
|
29
|
+
headers?: Record<string, string>;
|
|
30
|
+
body?: string; // pre-serialized by the compiler (JSON string)
|
|
31
|
+
delay_ms?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface InstalledRoute {
|
|
35
|
+
pattern: string | RegExp;
|
|
36
|
+
handler: (route: Route) => Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface MockState {
|
|
40
|
+
counts: Map<string, number>; // per-mock handled-request counters
|
|
41
|
+
installed: Set<string>; // names installed in this scenario (callCount guard)
|
|
42
|
+
routes: InstalledRoute[]; // exactly what WE routed (scoped reset)
|
|
43
|
+
guardInstalled: boolean;
|
|
44
|
+
passthrough: boolean;
|
|
45
|
+
violations: string[]; // unmocked requests the guard aborted (fail-on-unmocked)
|
|
46
|
+
/** @serial only: routes[0..N) / names installed by Background (beforeAll) persist across
|
|
47
|
+
* scenarios — reset() removes only what a scenario added on top. -1 = no snapshot
|
|
48
|
+
* (the default per-test path), where reset() drops the whole state. */
|
|
49
|
+
backgroundRouteCount: number;
|
|
50
|
+
backgroundNames: Set<string>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Resource types the fail-on-unmocked guard judges; everything else passes through.
|
|
54
|
+
* NOTE: page.route() does NOT intercept WebSocket connections (that is
|
|
55
|
+
* page.routeWebSocket, a separate API) — the shipped posture is fetch/XHR only. */
|
|
56
|
+
const GUARDED_TYPES = new Set(['fetch', 'xhr']);
|
|
57
|
+
|
|
58
|
+
class MockRouter {
|
|
59
|
+
private state = new WeakMap<Page, MockState>();
|
|
60
|
+
|
|
61
|
+
private stateFor(page: Page): MockState {
|
|
62
|
+
let st = this.state.get(page);
|
|
63
|
+
if (!st) {
|
|
64
|
+
st = { counts: new Map(), installed: new Set(), routes: [], guardInstalled: false, passthrough: false, violations: [], backgroundRouteCount: -1, backgroundNames: new Set() };
|
|
65
|
+
this.state.set(page, st);
|
|
66
|
+
}
|
|
67
|
+
return st;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** `@mock:passthrough` — unmatched requests reach the real backend for this page/scenario.
|
|
71
|
+
* Also forgives violations already recorded (Background installs + goto run BEFORE the
|
|
72
|
+
* scenario body can set this flag — boot-time requests of an opted-out scenario must not
|
|
73
|
+
* fail its assertNoUnmocked verdict). The compiler additionally arms the flag inside
|
|
74
|
+
* beforeEach via test.info().tags, before the Background installs, so boot requests are
|
|
75
|
+
* not even aborted on the default path. */
|
|
76
|
+
setPassthrough(page: Page): void {
|
|
77
|
+
const st = this.stateFor(page);
|
|
78
|
+
st.passthrough = true;
|
|
79
|
+
st.violations.length = 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Install one catalog entry. The first install also arms the fail-on-unmocked guard. */
|
|
83
|
+
async install(page: Page, cfg: MockConfig): Promise<void> {
|
|
84
|
+
const st = this.stateFor(page);
|
|
85
|
+
if (!st.guardInstalled) {
|
|
86
|
+
st.guardInstalled = true;
|
|
87
|
+
const guard = async (route: Route) => {
|
|
88
|
+
const req = route.request();
|
|
89
|
+
if (!GUARDED_TYPES.has(req.resourceType()) || st.passthrough) return route.fallback();
|
|
90
|
+
// Record + abort — NEVER throw here: a route handler runs outside any test
|
|
91
|
+
// stack, so a throw surfaces as an unhandled rejection (a hard process crash
|
|
92
|
+
// outside @playwright/test, flaky attribution inside it). The compiler emits
|
|
93
|
+
// `await mock.assertNoUnmocked(page)` at the end of every @mock scenario,
|
|
94
|
+
// which fails the test deterministically with the FULL violation list.
|
|
95
|
+
st.violations.push(`${req.method()} ${req.url()}`);
|
|
96
|
+
await route.abort('failed');
|
|
97
|
+
};
|
|
98
|
+
await page.route('**/*', guard); // FIRST → matched last → every named mock wins
|
|
99
|
+
st.routes.push({ pattern: '**/*', handler: guard });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const pattern = cfg.url.startsWith('re:') ? new RegExp(cfg.url.slice(3)) : cfg.url;
|
|
103
|
+
const handler = async (route: Route) => {
|
|
104
|
+
if (cfg.method !== '*' && route.request().method() !== cfg.method) return route.fallback();
|
|
105
|
+
st.counts.set(cfg.name, (st.counts.get(cfg.name) ?? 0) + 1);
|
|
106
|
+
if (cfg.delay_ms) await new Promise((r) => setTimeout(r, cfg.delay_ms));
|
|
107
|
+
// A delayed fulfill can race a page close / reset at teardown — that race must
|
|
108
|
+
// not surface as an unhandled rejection from inside a route handler.
|
|
109
|
+
try { await route.fulfill({ status: cfg.status, headers: cfg.headers, body: cfg.body }); } catch { /* page closing */ }
|
|
110
|
+
};
|
|
111
|
+
await page.route(pattern, handler);
|
|
112
|
+
st.routes.push({ pattern, handler });
|
|
113
|
+
st.installed.add(cfg.name);
|
|
114
|
+
if (!st.counts.has(cfg.name)) st.counts.set(cfg.name, 0);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Handled-request count for a mock installed in THIS scenario — throws for a name
|
|
118
|
+
* that was never installed here (an exact 0 on an uninstalled mock must be a loud
|
|
119
|
+
* error, not a passing assertion). */
|
|
120
|
+
callCount(page: Page, name: string): number {
|
|
121
|
+
const st = this.state.get(page);
|
|
122
|
+
if (!st || !st.installed.has(name)) {
|
|
123
|
+
throw new Error(`mock "${name}" was never installed in this scenario — add \`Given [${name}] mock is active\``);
|
|
124
|
+
}
|
|
125
|
+
return st.counts.get(name) ?? 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Fail-on-unmocked verdict — emitted by the compiler as the LAST statement of every
|
|
129
|
+
* @mock scenario. Throws with the full list of aborted unmatched requests; a scenario
|
|
130
|
+
* whose app swallowed those failed fetches must not stay green for the wrong reason. */
|
|
131
|
+
async assertNoUnmocked(page: Page): Promise<void> {
|
|
132
|
+
const st = this.state.get(page);
|
|
133
|
+
if (!st || st.violations.length === 0) return;
|
|
134
|
+
const list = st.violations.slice();
|
|
135
|
+
st.violations.length = 0;
|
|
136
|
+
throw new Error(
|
|
137
|
+
`${list.length} unmocked request(s) hit the fail-on-unmocked guard:\n - ${list.join('\n - ')}\n` +
|
|
138
|
+
'Add entries to mock/mocks.yaml, or opt out with @mock:passthrough.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** @serial only, emitted by the compiler at the END of the Background block (beforeAll):
|
|
142
|
+
* everything installed so far — Background mocks and the guard — persists across the
|
|
143
|
+
* feature's scenarios; reset() removes only what each scenario added on top. Without
|
|
144
|
+
* this, scenario 1's reset would kill the Background mocks and scenarios 2+ would run
|
|
145
|
+
* against the REAL backend with no guard — the silent-green class this driver exists
|
|
146
|
+
* to prevent. */
|
|
147
|
+
snapshotBackground(page: Page): void {
|
|
148
|
+
const st = this.stateFor(page);
|
|
149
|
+
st.backgroundRouteCount = st.routes.length;
|
|
150
|
+
st.backgroundNames = new Set(st.installed);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Scoped teardown for the @serial shared page: unroute exactly what THIS scenario
|
|
154
|
+
* installed (Background routes captured by snapshotBackground persist), zero every
|
|
155
|
+
* counter, clear flags/violations. Emitted at the end of each @mock scenario on the
|
|
156
|
+
* serial path; the default per-test path never needs it (page dies with the test). */
|
|
157
|
+
async reset(page: Page): Promise<void> {
|
|
158
|
+
const st = this.state.get(page);
|
|
159
|
+
if (!st) return;
|
|
160
|
+
if (st.backgroundRouteCount < 0) {
|
|
161
|
+
// No Background snapshot → drop everything (the pre-snapshot / default behavior).
|
|
162
|
+
for (const r of st.routes) {
|
|
163
|
+
try { await page.unroute(r.pattern, r.handler); } catch { /* page may be closing */ }
|
|
164
|
+
}
|
|
165
|
+
this.state.delete(page);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
for (const r of st.routes.slice(st.backgroundRouteCount)) {
|
|
169
|
+
try { await page.unroute(r.pattern, r.handler); } catch { /* page may be closing */ }
|
|
170
|
+
}
|
|
171
|
+
st.routes.length = st.backgroundRouteCount;
|
|
172
|
+
st.installed = new Set(st.backgroundNames);
|
|
173
|
+
st.counts = new Map();
|
|
174
|
+
st.violations.length = 0;
|
|
175
|
+
st.passthrough = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const mock = new MockRouter();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-data-guide.d.ts","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":"AAaA,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBvD;
|
|
1
|
+
{"version":3,"file":"test-data-guide.d.ts","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":"AAaA,oEAAoE;AACpE,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAgBvD;AA2GD;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAoB1D"}
|
|
@@ -163,6 +163,10 @@ const ENV_QA_EXAMPLE = `# Secret test data referenced as \${QA_*} in qa/**/test-
|
|
|
163
163
|
# Per-environment overrides: .env.qa.<env> + run with SUNGEN_ENV=<env>.
|
|
164
164
|
#
|
|
165
165
|
# QA_ADMIN_PASSWORD=
|
|
166
|
+
#
|
|
167
|
+
# Web baseURL for this environment — overrides the static value in
|
|
168
|
+
# playwright.config.ts (declare per env: .env.qa.dev, .env.qa.uat, ...).
|
|
169
|
+
# SUNGEN_BASE_URL=
|
|
166
170
|
`;
|
|
167
171
|
/**
|
|
168
172
|
* Idempotently create qa/TEST-DATA-GUIDE.md and .env.qa.example, and make sure
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-data-guide.js","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,wCAgBC;
|
|
1
|
+
{"version":3,"file":"test-data-guide.js","sourceRoot":"","sources":["../../src/orchestrator/test-data-guide.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAcA,wCAgBC;AA+GD,4DAoBC;AAjKD;;;;;;;;;GASG;AACH,uCAAyB;AACzB,2CAA6B;AAE7B,oEAAoE;AACpE,SAAgB,cAAc,CAAC,QAAgB;IAC7C,OAAO;QACL,KAAK,QAAQ,+DAA+D;QAC5E,+EAA+E;QAC/E,sBAAsB;QACtB,2EAA2E;QAC3E,4EAA4E;QAC5E,wEAAwE;QACxE,iCAAiC;QACjC,8EAA8E;QAC9E,yEAAyE;QACzE,8EAA8E;QAC9E,0EAA0E;QAC1E,+EAA+E;QAC/E,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,MAAM,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4FhB,CAAC;AAEF,MAAM,cAAc,GAAG;;;;;;;;;CAStB,CAAC;AAEF;;;GAGG;AACH,SAAgB,wBAAwB,CAAC,GAAW;IAClD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,EAAE,oBAAoB,CAAC,CAAC;IAC7D,IAAI,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QACrE,EAAE,CAAC,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjD,CAAC;IACD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,iBAAiB,CAAC,CAAC;IACtD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;QAChC,EAAE,CAAC,aAAa,CAAC,WAAW,EAAE,cAAc,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC;IACnD,IAAI,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,aAAa,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,6EAA6E;QAC7E,oEAAoE;QACpE,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1F,EAAE,CAAC,cAAc,CAAC,aAAa,EAAE,iFAAiF,CAAC,CAAC;QACtH,CAAC;IACH,CAAC;SAAM,CAAC;QACN,EAAE,CAAC,aAAa,CAAC,aAAa,EAAE,+EAA+E,EAAE,OAAO,CAAC,CAAC;IAC5H,CAAC;AACH,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.19",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -39,8 +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-data-factory": "3.2.
|
|
43
|
-
"@sungen/driver-ui": "3.2.
|
|
42
|
+
"@sungen/driver-data-factory": "3.2.19",
|
|
43
|
+
"@sungen/driver-ui": "3.2.19",
|
|
44
44
|
"chalk": "^5.6.2",
|
|
45
45
|
"commander": "^14.0.2",
|
|
46
46
|
"dotenv": "^17.2.3",
|
|
@@ -27,7 +27,10 @@ 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
|
-
|
|
30
|
+
// driver-mock appended at the END: ordering = registration = composition (tie-breaks
|
|
31
|
+
// between equal-priority patterns follow registration order) — inserting mid-list
|
|
32
|
+
// shifts them and shows up as golden diffs.
|
|
33
|
+
const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/driver-api', '@sungen/driver-mobile', '@sungen/driver-data-factory', '@sungen/driver-mock'];
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
36
|
* Bundled (non-opt-in) drivers: shipped as a core dependency (`drivers.yaml` `bundled: true`), so the
|
package/src/cli/commands/add.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
+
import { featureFilesFor } from '../../harness/unit-paths';
|
|
4
5
|
|
|
5
6
|
export function registerAddCommand(program: Command): void {
|
|
6
7
|
program
|
|
@@ -27,8 +28,9 @@ export function registerAddCommand(program: Command): void {
|
|
|
27
28
|
// When --figma is set on an existing screen, skip scaffolding and
|
|
28
29
|
// just refresh Figma-derived files (spec_figma.md, ui/*.png).
|
|
29
30
|
const screenDir = path.join(process.cwd(), 'qa', 'screens', options.screen);
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
// ANY `.feature` means the screen exists — its file may be named after the --path
|
|
32
|
+
// segment or be a `--feature` sub-feature, not `<screen>.feature`.
|
|
33
|
+
const screenAlreadyExists = featureFilesFor(screenDir, options.screen).some((f) => fs.existsSync(f));
|
|
32
34
|
|
|
33
35
|
if (!(options.figma && screenAlreadyExists)) {
|
|
34
36
|
const { ScreenManager } = require('../../orchestrator/screen-manager');
|
|
@@ -12,6 +12,7 @@ import * as path from 'path';
|
|
|
12
12
|
import * as fs from 'fs';
|
|
13
13
|
import { execSync } from 'child_process';
|
|
14
14
|
import { EnvironmentInfo } from '../../exporters/types';
|
|
15
|
+
import { resolveWebBaseUrl } from '../../exporters/env-qa';
|
|
15
16
|
import {
|
|
16
17
|
buildDashboardSnapshot,
|
|
17
18
|
DashboardTarget,
|
|
@@ -42,11 +43,14 @@ function getEnvironment(cwd: string): EnvironmentInfo {
|
|
|
42
43
|
const configPath = path.join(cwd, 'playwright.config.ts');
|
|
43
44
|
if (fs.existsSync(configPath)) {
|
|
44
45
|
const c = fs.readFileSync(configPath, 'utf-8');
|
|
45
|
-
|
|
46
|
+
// Both config shapes: legacy literal and env-aware resolveBaseUrl('x') wrapper.
|
|
47
|
+
const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
|
|
46
48
|
if (baseMatch) baseURL = baseMatch[1];
|
|
47
49
|
const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
|
|
48
50
|
if (projMatch) projectName = projMatch[1];
|
|
49
51
|
}
|
|
52
|
+
// Same SUNGEN_BASE_URL override as delivery — the two surfaces must agree.
|
|
53
|
+
baseURL = resolveWebBaseUrl(cwd, baseURL);
|
|
50
54
|
|
|
51
55
|
let executor = process.env.CI_USER || process.env.USER || '';
|
|
52
56
|
try {
|
|
@@ -61,6 +65,7 @@ interface DashboardOptions {
|
|
|
61
65
|
noHistory?: boolean;
|
|
62
66
|
open?: boolean;
|
|
63
67
|
maxHistory?: string;
|
|
68
|
+
env?: string;
|
|
64
69
|
}
|
|
65
70
|
|
|
66
71
|
export function registerDashboardCommand(program: Command): void {
|
|
@@ -71,9 +76,13 @@ export function registerDashboardCommand(program: Command): void {
|
|
|
71
76
|
.option('--no-history', 'Do not persist this run under qa/dashboard/history/')
|
|
72
77
|
.option('--max-history <n>', `Limit snapshots included in dashboard stats (default: ${DEFAULT_MAX_HISTORY}). Older files are kept on disk.`)
|
|
73
78
|
.option('--open', 'Open the rendered HTML in the default browser when done')
|
|
79
|
+
.option('--env <env>', 'Build the dashboard for one environment/locale (sets SUNGEN_ENV): picks that env\'s result files, and Trends/Compare only see history snapshots of the same env')
|
|
74
80
|
.action(async (names: string[], options: DashboardOptions) => {
|
|
75
81
|
try {
|
|
76
82
|
const cwd = process.cwd();
|
|
83
|
+
// Same convention as run-test/delivery: SUNGEN_ENV drives result-file
|
|
84
|
+
// resolution, locale pairing, and (new) history scoping downstream.
|
|
85
|
+
if (options.env) process.env.SUNGEN_ENV = options.env;
|
|
77
86
|
|
|
78
87
|
// 1. Scope detection
|
|
79
88
|
let targets: DashboardTarget[];
|
|
@@ -24,7 +24,9 @@ import {
|
|
|
24
24
|
import { renderXlsxMultiSheet, writeXlsx, buildApiDetailRows, addApiDetailSheet } from '../../exporters/xlsx-exporter';
|
|
25
25
|
import { loadApiCatalog } from '../../exporters/api-catalog-loader';
|
|
26
26
|
import { resolveApiEnvironment } from '../../exporters/api-environment';
|
|
27
|
+
import { resolveWebBaseUrl } from '../../exporters/env-qa';
|
|
27
28
|
import { resolvePlatformAppId } from '../../exporters/mobile-app-id';
|
|
29
|
+
import { scanTestDataSecrets } from '../../harness/secret-scan';
|
|
28
30
|
import { EnvironmentInfo, PreflightCheck, ScreenSummary, TestCaseRow, ApiCatalogEntry } from '../../exporters/types';
|
|
29
31
|
import { mapFilePath } from '../../exporters/matrix/map-loader';
|
|
30
32
|
import {
|
|
@@ -343,11 +345,16 @@ function getEnvironment(cwd: string): EnvironmentInfo {
|
|
|
343
345
|
const configPath = path.join(cwd, 'playwright.config.ts');
|
|
344
346
|
if (fs.existsSync(configPath)) {
|
|
345
347
|
const c = fs.readFileSync(configPath, 'utf-8');
|
|
346
|
-
|
|
348
|
+
// Both config shapes: the legacy literal `baseURL: 'x'` and the env-aware
|
|
349
|
+
// `baseURL: resolveBaseUrl('x')` (the quoted value is the static fallback).
|
|
350
|
+
const baseMatch = c.match(/baseURL:\s*(?:resolveBaseUrl\(\s*)?['"]([^'"]+)['"]/);
|
|
347
351
|
if (baseMatch) baseURL = baseMatch[1];
|
|
348
352
|
const projMatch = c.match(/name:\s*['"]([^'"]+)['"]/);
|
|
349
353
|
if (projMatch) projectName = projMatch[1];
|
|
350
354
|
}
|
|
355
|
+
// SUNGEN_BASE_URL (shell > .env.qa.<SUNGEN_ENV> > .env.qa) overrides the static
|
|
356
|
+
// value — the Test Environment column must show the host the run actually hit.
|
|
357
|
+
baseURL = resolveWebBaseUrl(cwd, baseURL);
|
|
351
358
|
|
|
352
359
|
let executor = process.env.CI_USER || process.env.USER || '';
|
|
353
360
|
try {
|
|
@@ -894,6 +901,17 @@ export function registerDeliveryCommand(program: Command): void {
|
|
|
894
901
|
}
|
|
895
902
|
}
|
|
896
903
|
|
|
904
|
+
// Security S0 — warn (never block) if test-data looks to hold a real secret.
|
|
905
|
+
// Deliverables are COMMITTED files; delivery is the last stop before a
|
|
906
|
+
// hardcoded credential lands in one. Same scan `generate` runs.
|
|
907
|
+
const scanDirs = [...new Set(toExport.map((t) => qaDir(cwd, t)))];
|
|
908
|
+
const secretHits = scanDirs.flatMap((d) => scanTestDataSecrets(d));
|
|
909
|
+
if (secretHits.length) {
|
|
910
|
+
log(`\n⚠️ Possible secret(s) in committed test-data (review — do NOT commit real credentials):`);
|
|
911
|
+
for (const h of secretHits.slice(0, 10)) log(` ${h.file}:${h.line} — ${h.reason}`);
|
|
912
|
+
log(` Move real secrets to an env overlay / CI secret; keep test-data placeholders only.\n`);
|
|
913
|
+
}
|
|
914
|
+
|
|
897
915
|
// 3. Export
|
|
898
916
|
const legacy = options.legacy || options.full;
|
|
899
917
|
if (!legacy) {
|
|
@@ -212,7 +212,10 @@ export function registerGenerateCommand(program: Command): void {
|
|
|
212
212
|
if (screenName) scanDirs.push(path.join(process.cwd(), 'qa', 'screens', screenName));
|
|
213
213
|
else if (flowName) scanDirs.push(path.join(process.cwd(), 'qa', 'flows', flowName));
|
|
214
214
|
else {
|
|
215
|
-
|
|
215
|
+
// qa/api units are yaml sinks too (apis.yaml headers, mock bodies once shared);
|
|
216
|
+
// api/flows nests one level deeper. A file entry in the list is harmless —
|
|
217
|
+
// scanTestDataSecrets only reads known subdirs of each entry.
|
|
218
|
+
for (const base of ['screens', 'flows', 'api', path.join('api', 'flows')]) {
|
|
216
219
|
const d = path.join(process.cwd(), 'qa', base);
|
|
217
220
|
if (fs.existsSync(d)) for (const n of fs.readdirSync(d)) scanDirs.push(path.join(d, n));
|
|
218
221
|
}
|
|
@@ -48,8 +48,14 @@ export function buildPayload(
|
|
|
48
48
|
historyLimit?: number,
|
|
49
49
|
): DashboardPayload {
|
|
50
50
|
const all = readHistory(cwd, historyLimit);
|
|
51
|
-
// Exclude `current` from history if it's been written there already
|
|
52
|
-
|
|
51
|
+
// Exclude `current` from history if it's been written there already, and keep
|
|
52
|
+
// ONLY snapshots of the SAME environment: qa/dashboard/history/ accumulates
|
|
53
|
+
// dev/uat/stg runs side by side, and blending them gave Trends one mixed
|
|
54
|
+
// pass-rate line and let Compare diff across environments. Legacy snapshots
|
|
55
|
+
// without the field count as base runs ('').
|
|
56
|
+
const currentEnv = current.environment?.sungenEnv || '';
|
|
57
|
+
const history = all.filter((s) =>
|
|
58
|
+
s.runId !== current.runId && (s.environment?.sungenEnv || '') === currentEnv);
|
|
53
59
|
return { current, history };
|
|
54
60
|
}
|
|
55
61
|
|
|
@@ -541,6 +541,9 @@ function collectEnvironment(cwd: string, env: EnvironmentInfo) {
|
|
|
541
541
|
sungenVersion: getPackageVersion(),
|
|
542
542
|
gitBranch,
|
|
543
543
|
gitSha,
|
|
544
|
+
// Which environment this snapshot belongs to — dev/uat/stg runs must never
|
|
545
|
+
// blend into one trend line ('' = base run).
|
|
546
|
+
sungenEnv: process.env.SUNGEN_ENV || '',
|
|
544
547
|
};
|
|
545
548
|
}
|
|
546
549
|
|
package/src/dashboard/types.ts
CHANGED
|
@@ -23,6 +23,10 @@ export interface SnapshotEnvironment {
|
|
|
23
23
|
sungenVersion: string;
|
|
24
24
|
gitBranch?: string;
|
|
25
25
|
gitSha?: string;
|
|
26
|
+
/** SUNGEN_ENV the snapshot was built under ('' = base run, absent on legacy
|
|
27
|
+
* snapshots). Trends/Compare must never mix runs of different environments,
|
|
28
|
+
* so the payload builder filters history by this field. */
|
|
29
|
+
sungenEnv?: string;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
32
|
export interface AggregateSummary {
|
|
@@ -19,6 +19,9 @@ import * as fs from 'fs';
|
|
|
19
19
|
import * as path from 'path';
|
|
20
20
|
import { parse as parseYaml } from 'yaml';
|
|
21
21
|
import { ApiCatalogEntry } from './types';
|
|
22
|
+
// .env.qa reading (runtime-precedence, local map) moved to ./env-qa so the web
|
|
23
|
+
// baseURL resolver shares the exact same rules.
|
|
24
|
+
import { buildEnvMap } from './env-qa';
|
|
22
25
|
|
|
23
26
|
interface ApiDatasourceConf {
|
|
24
27
|
kind?: string;
|
|
@@ -26,26 +29,6 @@ interface ApiDatasourceConf {
|
|
|
26
29
|
baseUrl?: string;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
/** Read KEY=value lines from one dotenv-style file (quotes stripped, comments ignored). */
|
|
30
|
-
function readEnvFile(file: string, into: Record<string, string>): void {
|
|
31
|
-
if (!fs.existsSync(file)) return;
|
|
32
|
-
for (const line of fs.readFileSync(file, 'utf8').split('\n')) {
|
|
33
|
-
const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
|
34
|
-
if (m && into[m[1]] === undefined) into[m[1]] = m[2].replace(/^["']|["']$/g, '');
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Shell env > .env.qa.<SUNGEN_ENV> > .env.qa — same precedence as the runtime's
|
|
39
|
-
* loadEnvQa, but built as a LOCAL map (the exporter must not mutate process.env). */
|
|
40
|
-
function buildEnvMap(cwd: string): Record<string, string> {
|
|
41
|
-
const map: Record<string, string> = {};
|
|
42
|
-
for (const name of [`.env.qa.${process.env.SUNGEN_ENV || ''}`, '.env.qa']) {
|
|
43
|
-
if (!name.endsWith('.')) readEnvFile(path.join(cwd, name), map);
|
|
44
|
-
}
|
|
45
|
-
for (const [k, v] of Object.entries(process.env)) if (v !== undefined) map[k] = v;
|
|
46
|
-
return map;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
32
|
function loadDatasources(cwd: string): Record<string, ApiDatasourceConf> {
|
|
50
33
|
const file = [path.join(cwd, 'datasources.yaml'), path.join(cwd, 'qa', 'datasources.yaml')]
|
|
51
34
|
.find((f) => fs.existsSync(f));
|
|
@@ -295,7 +295,7 @@ function formatBodyFile(bodyFile: unknown, args: Record<string, string>, localiz
|
|
|
295
295
|
* test-data keys — catches variants like confirmPassword / new_password /
|
|
296
296
|
* password_confirmation / access_token / client_secret / otp / passcode.
|
|
297
297
|
*/
|
|
298
|
-
const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization/i;
|
|
298
|
+
export const SENSITIVE_NAME = /pass(word|code)?|passwd|pwd|secret|token|otp|api[-_]?key|apikey|credential|authorization/i;
|
|
299
299
|
function isSensitiveField(key: string): boolean {
|
|
300
300
|
return SENSITIVE_NAME.test(key);
|
|
301
301
|
}
|
|
@@ -319,7 +319,13 @@ export function collectSecretValues(testData: Record<string, string>): string[]
|
|
|
319
319
|
export function redactSecrets(text: string, secrets: string[]): string {
|
|
320
320
|
if (secrets.length === 0 || !text) return text;
|
|
321
321
|
let out = text;
|
|
322
|
-
for (const s of secrets)
|
|
322
|
+
for (const s of secrets) {
|
|
323
|
+
// Floor lives HERE, not only in the collectors: an empty string would split('')
|
|
324
|
+
// and interleave `***` between every character; a 1-2 char value masks
|
|
325
|
+
// coincidental text. Callers building their own denylist must not bypass it.
|
|
326
|
+
if (!s || s.length < 3) continue;
|
|
327
|
+
out = out.split(s).join('***');
|
|
328
|
+
}
|
|
323
329
|
return out;
|
|
324
330
|
}
|
|
325
331
|
|