@sun-asterisk/sungen 3.2.27 → 3.2.29
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 +29 -0
- package/dist/capabilities/discover.d.ts.map +1 -1
- package/dist/capabilities/discover.js +144 -10
- package/dist/capabilities/discover.js.map +1 -1
- package/dist/capabilities/registry.d.ts +18 -0
- package/dist/capabilities/registry.d.ts.map +1 -1
- package/dist/capabilities/registry.js +25 -0
- package/dist/capabilities/registry.js.map +1 -1
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/audit.js +9 -0
- package/dist/cli/commands/audit.js.map +1 -1
- package/dist/cli/commands/capability.js +1 -1
- package/dist/cli/commands/capability.js.map +1 -1
- package/dist/dashboard/templates/index.html +1 -1
- package/dist/exporters/feature-parser.d.ts +1 -1
- package/dist/exporters/feature-parser.js +1 -1
- package/dist/exporters/matrix/build.d.ts.map +1 -1
- package/dist/exporters/matrix/build.js +14 -1
- package/dist/exporters/matrix/build.js.map +1 -1
- package/dist/exporters/matrix/item-labels.d.ts +35 -0
- package/dist/exporters/matrix/item-labels.d.ts.map +1 -0
- package/dist/exporters/matrix/item-labels.js +90 -0
- package/dist/exporters/matrix/item-labels.js.map +1 -0
- package/dist/exporters/matrix/render-coverage-sheet.d.ts +14 -0
- package/dist/exporters/matrix/render-coverage-sheet.d.ts.map +1 -0
- package/dist/exporters/matrix/render-coverage-sheet.js +160 -0
- package/dist/exporters/matrix/render-coverage-sheet.js.map +1 -0
- package/dist/exporters/matrix/render-overview-sheet.d.ts +20 -0
- package/dist/exporters/matrix/render-overview-sheet.d.ts.map +1 -0
- package/dist/exporters/matrix/render-overview-sheet.js +36 -0
- package/dist/exporters/matrix/render-overview-sheet.js.map +1 -0
- package/dist/exporters/matrix/render-testcase-sheet.d.ts +30 -0
- package/dist/exporters/matrix/render-testcase-sheet.d.ts.map +1 -0
- package/dist/exporters/matrix/render-testcase-sheet.js +156 -0
- package/dist/exporters/matrix/render-testcase-sheet.js.map +1 -0
- package/dist/exporters/matrix/render-xlsx.d.ts +41 -31
- package/dist/exporters/matrix/render-xlsx.d.ts.map +1 -1
- package/dist/exporters/matrix/render-xlsx.js +72 -425
- package/dist/exporters/matrix/render-xlsx.js.map +1 -1
- package/dist/exporters/matrix/testcase-summary-band.d.ts +30 -0
- package/dist/exporters/matrix/testcase-summary-band.d.ts.map +1 -0
- package/dist/exporters/matrix/testcase-summary-band.js +75 -0
- package/dist/exporters/matrix/testcase-summary-band.js.map +1 -0
- package/dist/exporters/matrix/wording.d.ts.map +1 -1
- package/dist/exporters/matrix/wording.js +13 -0
- package/dist/exporters/matrix/wording.js.map +1 -1
- package/dist/exporters/matrix/xlsx-cells.d.ts +38 -0
- package/dist/exporters/matrix/xlsx-cells.d.ts.map +1 -0
- package/dist/exporters/matrix/xlsx-cells.js +52 -0
- package/dist/exporters/matrix/xlsx-cells.js.map +1 -0
- package/dist/exporters/xlsx-report-builder.d.ts +12 -1
- package/dist/exporters/xlsx-report-builder.d.ts.map +1 -1
- package/dist/exporters/xlsx-report-builder.js +10 -2
- package/dist/exporters/xlsx-report-builder.js.map +1 -1
- package/dist/generators/gherkin-parser/index.d.ts +7 -0
- package/dist/generators/gherkin-parser/index.d.ts.map +1 -1
- package/dist/generators/gherkin-parser/index.js +3 -3
- package/dist/generators/gherkin-parser/index.js.map +1 -1
- package/dist/generators/test-generator/adapters/appium/os-support.d.ts +53 -0
- package/dist/generators/test-generator/adapters/appium/os-support.d.ts.map +1 -0
- package/dist/generators/test-generator/adapters/appium/os-support.js +200 -0
- package/dist/generators/test-generator/adapters/appium/os-support.js.map +1 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +9 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +24 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +6 -2
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +4 -3
- package/dist/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +9 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +34 -1
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +14 -6
- package/dist/generators/test-generator/adapters/appium/templates/steps/assertions/page-assertion.hbs +6 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/go-back-action.hbs +21 -0
- package/dist/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +16 -0
- package/dist/generators/test-generator/adapters/appium/templates/test-file.hbs +8 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +2 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +7 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +7 -0
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +4 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +12 -4
- package/dist/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +14 -2
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +8 -8
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -1
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +8 -8
- package/dist/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +6 -2
- package/dist/generators/test-generator/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +6 -2
- package/dist/generators/test-generator/code-generator.js.map +1 -1
- package/dist/generators/test-generator/diagnostics.d.ts +38 -2
- package/dist/generators/test-generator/diagnostics.d.ts.map +1 -1
- package/dist/generators/test-generator/diagnostics.js +37 -1
- package/dist/generators/test-generator/diagnostics.js.map +1 -1
- package/dist/generators/test-generator/patterns/index.d.ts +33 -6
- package/dist/generators/test-generator/patterns/index.d.ts.map +1 -1
- package/dist/generators/test-generator/patterns/index.js +71 -7
- package/dist/generators/test-generator/patterns/index.js.map +1 -1
- package/dist/generators/test-generator/patterns/types.d.ts +8 -0
- package/dist/generators/test-generator/patterns/types.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.d.ts +40 -1
- package/dist/generators/test-generator/step-mapper.d.ts.map +1 -1
- package/dist/generators/test-generator/step-mapper.js +104 -21
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +24 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +82 -12
- package/dist/generators/test-generator/utils/selector-resolver.js.map +1 -1
- package/dist/harness/audit.d.ts +3 -1
- package/dist/harness/audit.d.ts.map +1 -1
- package/dist/harness/audit.js +52 -29
- package/dist/harness/audit.js.map +1 -1
- package/dist/harness/capability-plan.d.ts +4 -0
- package/dist/harness/capability-plan.d.ts.map +1 -1
- package/dist/harness/capability-plan.js +32 -2
- package/dist/harness/capability-plan.js.map +1 -1
- package/dist/harness/catalog/drivers.yaml +1 -1
- package/dist/harness/catalog/universal-viewpoints.yaml +145 -0
- package/dist/harness/next-step.d.ts.map +1 -1
- package/dist/harness/next-step.js +1 -0
- package/dist/harness/next-step.js.map +1 -1
- package/dist/harness/parse.d.ts +6 -0
- package/dist/harness/parse.d.ts.map +1 -1
- package/dist/harness/parse.js +1 -1
- package/dist/harness/parse.js.map +1 -1
- package/dist/harness/repair.d.ts.map +1 -1
- package/dist/harness/repair.js +8 -1
- package/dist/harness/repair.js.map +1 -1
- package/dist/harness/sensors.d.ts +49 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +51 -1
- package/dist/harness/sensors.js.map +1 -1
- package/dist/harness/viewpoint-ledger.d.ts +10 -0
- package/dist/harness/viewpoint-ledger.d.ts.map +1 -1
- package/dist/harness/viewpoint-ledger.js +23 -2
- package/dist/harness/viewpoint-ledger.js.map +1 -1
- package/dist/orchestrator/templates/ai-src/commands/delivery.md +4 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -5
- package/dist/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +48 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +39 -6
- package/dist/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +16 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +11 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +89 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +58 -1
- package/dist/orchestrator/templates/ai-src/skills/sungen-test-design-techniques/SKILL.md +13 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +59 -10
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-d-display.md +1 -0
- package/dist/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-f-mobile-native.md +419 -0
- package/dist/orchestrator/templates/readme.md +1 -1
- package/dist/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts +31 -0
- package/dist/orchestrator/templates/specs-url-assert.d.ts.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.js +43 -0
- package/dist/orchestrator/templates/specs-url-assert.js.map +1 -1
- package/dist/orchestrator/templates/specs-url-assert.ts +54 -0
- package/dist/orchestrator/templates/wdio.conf.ts +105 -1
- package/package.json +3 -3
- package/src/capabilities/discover.ts +146 -10
- package/src/capabilities/registry.ts +30 -0
- package/src/cli/commands/audit.ts +9 -0
- package/src/cli/commands/capability.ts +1 -1
- package/src/dashboard/templates/index.html +1 -1
- package/src/exporters/feature-parser.ts +1 -1
- package/src/exporters/matrix/build.ts +13 -1
- package/src/exporters/matrix/item-labels.ts +78 -0
- package/src/exporters/matrix/render-coverage-sheet.ts +168 -0
- package/src/exporters/matrix/render-overview-sheet.ts +56 -0
- package/src/exporters/matrix/render-testcase-sheet.ts +178 -0
- package/src/exporters/matrix/render-xlsx.ts +67 -448
- package/src/exporters/matrix/testcase-summary-band.ts +79 -0
- package/src/exporters/matrix/wording.ts +13 -0
- package/src/exporters/matrix/xlsx-cells.ts +56 -0
- package/src/exporters/xlsx-report-builder.ts +16 -2
- package/src/generators/gherkin-parser/index.ts +1 -1
- package/src/generators/test-generator/adapters/appium/os-support.ts +176 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/alert-fill-action.hbs +9 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/capture-variable.hbs +24 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-action.hbs +6 -2
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/hover-element-with-text.hbs +4 -3
- package/src/generators/test-generator/adapters/appium/templates/steps/actions/unknown-element-action.hbs +9 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/have-value-assertion.hbs +34 -1
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/label-value-assertion.hbs +14 -6
- package/src/generators/test-generator/adapters/appium/templates/steps/assertions/page-assertion.hbs +6 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/go-back-action.hbs +21 -0
- package/src/generators/test-generator/adapters/appium/templates/steps/gestures/set-geolocation-action.hbs +16 -0
- package/src/generators/test-generator/adapters/appium/templates/test-file.hbs +8 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row-nth.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/actions/table-action-in-row.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/page-assertion.hbs +2 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/selected-assertion.hbs +7 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/state-with-filter-assertion.hbs +7 -0
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-empty.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-match-data.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-count.hbs +4 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-exists.hbs +12 -4
- package/src/generators/test-generator/adapters/playwright/templates/steps/assertions/table-row-not-exists.hbs +14 -2
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-base.hbs +8 -8
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/id.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator-strategies/testid.hbs +1 -1
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/locator.hbs +8 -8
- package/src/generators/test-generator/adapters/playwright/templates/steps/partials/url-assertion.hbs +6 -2
- package/src/generators/test-generator/code-generator.ts +6 -2
- package/src/generators/test-generator/diagnostics.ts +39 -2
- package/src/generators/test-generator/patterns/index.ts +78 -8
- package/src/generators/test-generator/patterns/types.ts +8 -0
- package/src/generators/test-generator/step-mapper.ts +109 -19
- package/src/generators/test-generator/utils/selector-resolver.ts +98 -12
- package/src/harness/audit.ts +56 -31
- package/src/harness/capability-plan.ts +33 -1
- package/src/harness/catalog/drivers.yaml +1 -1
- package/src/harness/catalog/universal-viewpoints.yaml +145 -0
- package/src/harness/next-step.ts +1 -0
- package/src/harness/parse.ts +7 -1
- package/src/harness/repair.ts +8 -1
- package/src/harness/sensors.ts +100 -3
- package/src/harness/viewpoint-ledger.ts +26 -5
- package/src/orchestrator/templates/ai-src/commands/delivery.md +4 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-delivery/SKILL.md +14 -5
- package/src/orchestrator/templates/ai-src/skills/sungen-error-mapping/SKILL.md +48 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-gherkin-syntax/SKILL.md +39 -6
- package/src/orchestrator/templates/ai-src/skills/sungen-harness-audit/SKILL.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-mobile-gestures/SKILL.md +16 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-fix/SKILL.md +11 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-selector-keys/SKILL.md +14 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +89 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-review/SKILL.md +58 -1
- package/src/orchestrator/templates/ai-src/skills/sungen-test-design-techniques/SKILL.md +13 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/SKILL.md +59 -10
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-d-display.md +1 -0
- package/src/orchestrator/templates/ai-src/skills/sungen-viewpoint/group-f-mobile-native.md +419 -0
- package/src/orchestrator/templates/readme.md +1 -1
- package/src/orchestrator/templates/specs-pw-shape-reporter.ts +0 -0
- package/src/orchestrator/templates/specs-url-assert.ts +54 -0
- package/src/orchestrator/templates/wdio.conf.ts +105 -1
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
22
|
exports.joinPath = joinPath;
|
|
23
23
|
exports.urlMatches = urlMatches;
|
|
24
|
+
exports.expectPage = expectPage;
|
|
25
|
+
const test_1 = require("@playwright/test");
|
|
24
26
|
/**
|
|
25
27
|
* Join a page's URL with the `with {{v}}` value for a NAVIGATION step
|
|
26
28
|
* (`Given User is on [X] page with {{v}}`).
|
|
@@ -118,4 +120,45 @@ function urlMatches({ pathname, declared, dataRef }) {
|
|
|
118
120
|
return queryOk && hashOk;
|
|
119
121
|
};
|
|
120
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* `toHaveURL` is a polling assertion: it passes on the FIRST sample that matches. A client-side
|
|
125
|
+
* router pushes the destination URL before the route guard or the API has answered, so a user
|
|
126
|
+
* with no permission is "on" `/items/detail` for a few hundred milliseconds — long enough for
|
|
127
|
+
* the assertion to sample it, pass, and end the test green — and is then bounced to `/forbidden`
|
|
128
|
+
* with no assertion left to see it. A real project shipped a permission matrix on that oracle;
|
|
129
|
+
* the 403s were found by hand (#662).
|
|
130
|
+
*
|
|
131
|
+
* `expectPage` is the page oracle `see [X] page` compiles to now. Arriving is not enough: the URL
|
|
132
|
+
* has to be STILL matching once the page has settled.
|
|
133
|
+
* 1. arrive — the same predicate `toHaveURL` always used;
|
|
134
|
+
* 2. settle — wait for network to go quiet (bounded: an app that polls forever must not hang
|
|
135
|
+
* the test, so a timeout here is not a failure);
|
|
136
|
+
* 3. hold — sample the URL over a settle window; the moment it stops matching, fail and
|
|
137
|
+
* NAME where it went, because that destination is the finding;
|
|
138
|
+
* 4. re-assert — the arrival predicate, once more, on the settled page.
|
|
139
|
+
*
|
|
140
|
+
* `SUNGEN_URL_SETTLE_MS` (default 1000) is the hold window; `SUNGEN_URL_NETWORKIDLE_MS` (default
|
|
141
|
+
* 3000) bounds step 2. Both are runtime knobs, so a slow environment is a config change, not a
|
|
142
|
+
* regenerate.
|
|
143
|
+
*/
|
|
144
|
+
async function expectPage(page, expectation, opts = {}) {
|
|
145
|
+
const settleMs = opts.settleMs ?? Number(process.env.SUNGEN_URL_SETTLE_MS ?? 1000);
|
|
146
|
+
const networkIdleMs = opts.networkIdleMs ?? Number(process.env.SUNGEN_URL_NETWORKIDLE_MS ?? 3000);
|
|
147
|
+
const matches = urlMatches(expectation);
|
|
148
|
+
const assertUrl = opts.assertUrl ?? (async (m) => { await (0, test_1.expect)(page).toHaveURL(m); });
|
|
149
|
+
await assertUrl(matches);
|
|
150
|
+
const arrivedAt = page.url();
|
|
151
|
+
await page.waitForLoadState('networkidle', { timeout: networkIdleMs }).catch(() => undefined);
|
|
152
|
+
const started = Date.now();
|
|
153
|
+
while (Date.now() - started < settleMs) {
|
|
154
|
+
const now = page.url();
|
|
155
|
+
if (!matches(new URL(now))) {
|
|
156
|
+
throw new Error(`page assertion: reached ${arrivedAt} and then left for ${now} after ${Date.now() - started}ms — `
|
|
157
|
+
+ 'a route guard, an error boundary or a redirect moved the user away after the URL had already '
|
|
158
|
+
+ 'matched. The page the test asserted is not the page the user ended on.');
|
|
159
|
+
}
|
|
160
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
161
|
+
}
|
|
162
|
+
await assertUrl(matches);
|
|
163
|
+
}
|
|
121
164
|
//# sourceMappingURL=specs-url-assert.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"specs-url-assert.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-url-assert.ts"],"names":[],"mappings":";AAAA,oBAAoB;AACpB;;;;;;;;;;;;;;;;;GAiBG;;
|
|
1
|
+
{"version":3,"file":"specs-url-assert.js","sourceRoot":"","sources":["../../../src/orchestrator/templates/specs-url-assert.ts"],"names":[],"mappings":"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"}
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* URL, and needs no regex escaping of runtime values, which is what forced the split.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { expect, type Page } from '@playwright/test';
|
|
22
|
+
|
|
21
23
|
export interface UrlExpectation {
|
|
22
24
|
/**
|
|
23
25
|
* Anchored pathname pattern, compiled by the generator from the page selector's `value`
|
|
@@ -132,3 +134,55 @@ export function urlMatches({ pathname, declared, dataRef }: UrlExpectation): (u:
|
|
|
132
134
|
return queryOk && hashOk;
|
|
133
135
|
};
|
|
134
136
|
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `toHaveURL` is a polling assertion: it passes on the FIRST sample that matches. A client-side
|
|
140
|
+
* router pushes the destination URL before the route guard or the API has answered, so a user
|
|
141
|
+
* with no permission is "on" `/items/detail` for a few hundred milliseconds — long enough for
|
|
142
|
+
* the assertion to sample it, pass, and end the test green — and is then bounced to `/forbidden`
|
|
143
|
+
* with no assertion left to see it. A real project shipped a permission matrix on that oracle;
|
|
144
|
+
* the 403s were found by hand (#662).
|
|
145
|
+
*
|
|
146
|
+
* `expectPage` is the page oracle `see [X] page` compiles to now. Arriving is not enough: the URL
|
|
147
|
+
* has to be STILL matching once the page has settled.
|
|
148
|
+
* 1. arrive — the same predicate `toHaveURL` always used;
|
|
149
|
+
* 2. settle — wait for network to go quiet (bounded: an app that polls forever must not hang
|
|
150
|
+
* the test, so a timeout here is not a failure);
|
|
151
|
+
* 3. hold — sample the URL over a settle window; the moment it stops matching, fail and
|
|
152
|
+
* NAME where it went, because that destination is the finding;
|
|
153
|
+
* 4. re-assert — the arrival predicate, once more, on the settled page.
|
|
154
|
+
*
|
|
155
|
+
* `SUNGEN_URL_SETTLE_MS` (default 1000) is the hold window; `SUNGEN_URL_NETWORKIDLE_MS` (default
|
|
156
|
+
* 3000) bounds step 2. Both are runtime knobs, so a slow environment is a config change, not a
|
|
157
|
+
* regenerate.
|
|
158
|
+
*/
|
|
159
|
+
export async function expectPage(
|
|
160
|
+
page: { url(): string; waitForLoadState(state: 'networkidle', opts?: { timeout?: number }): Promise<void> },
|
|
161
|
+
expectation: UrlExpectation,
|
|
162
|
+
opts: { settleMs?: number; networkIdleMs?: number; assertUrl?: (matches: (u: URL) => boolean) => Promise<void> } = {},
|
|
163
|
+
): Promise<void> {
|
|
164
|
+
const settleMs = opts.settleMs ?? Number(process.env.SUNGEN_URL_SETTLE_MS ?? 1000);
|
|
165
|
+
const networkIdleMs = opts.networkIdleMs ?? Number(process.env.SUNGEN_URL_NETWORKIDLE_MS ?? 3000);
|
|
166
|
+
const matches = urlMatches(expectation);
|
|
167
|
+
const assertUrl = opts.assertUrl ?? (async (m: (u: URL) => boolean) => { await expect(page as unknown as Page).toHaveURL(m); });
|
|
168
|
+
|
|
169
|
+
await assertUrl(matches);
|
|
170
|
+
const arrivedAt = page.url();
|
|
171
|
+
|
|
172
|
+
await page.waitForLoadState('networkidle', { timeout: networkIdleMs }).catch(() => undefined);
|
|
173
|
+
|
|
174
|
+
const started = Date.now();
|
|
175
|
+
while (Date.now() - started < settleMs) {
|
|
176
|
+
const now = page.url();
|
|
177
|
+
if (!matches(new URL(now))) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`page assertion: reached ${arrivedAt} and then left for ${now} after ${Date.now() - started}ms — `
|
|
180
|
+
+ 'a route guard, an error boundary or a redirect moved the user away after the URL had already '
|
|
181
|
+
+ 'matched. The page the test asserted is not the page the user ended on.',
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await assertUrl(matches);
|
|
188
|
+
}
|
|
@@ -28,6 +28,26 @@ import * as path from 'node:path';
|
|
|
28
28
|
import { execSync } from 'node:child_process';
|
|
29
29
|
import PwShapeReporter from './specs/reporters/pw-shape-reporter';
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Failure-evidence output. `test-results/` is already gitignored — screenshots and page sources
|
|
33
|
+
* of a real app routinely contain live data (a signed-in email, a token rendered on screen), so
|
|
34
|
+
* this directory must never become committable.
|
|
35
|
+
*/
|
|
36
|
+
const EVIDENCE_ROOT = path.join('test-results', 'mobile');
|
|
37
|
+
/** A full logcat is unbounded; only the tail is ever diagnostic. */
|
|
38
|
+
const EVIDENCE_LOG_TAIL_LINES = 500;
|
|
39
|
+
/** ~2 MB of accessibility tree is already far more than anyone reads. */
|
|
40
|
+
const EVIDENCE_PAGE_SOURCE_MAX = 2_000_000;
|
|
41
|
+
|
|
42
|
+
/** Filesystem-safe path segment from a feature/test title. */
|
|
43
|
+
function evidenceSlug(s: string): string {
|
|
44
|
+
return String(s)
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
47
|
+
.replace(/^-+|-+$/g, '')
|
|
48
|
+
.slice(0, 80) || 'unnamed';
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
/**
|
|
32
52
|
* Auto-select MOBILE specs and classify each by its target OS (dual-target routing).
|
|
33
53
|
*
|
|
@@ -404,9 +424,93 @@ export const config: WebdriverIO.Config = {
|
|
|
404
424
|
// Connectivity self-heal (#615): onPrepare heals a device wedged by a previous KILLED run
|
|
405
425
|
// (its onComplete never fired) before the session starts; onComplete restores after this run,
|
|
406
426
|
// pass or fail. Both run in the launcher process, so they fire even when the worker/session died.
|
|
407
|
-
onPrepare: () => { restoreConnectivity('onPrepare'); setShowTouches('onPrepare'); },
|
|
427
|
+
onPrepare: () => { restoreConnectivity('onPrepare'); setShowTouches('onPrepare'); fs.mkdirSync(EVIDENCE_ROOT, { recursive: true }); },
|
|
408
428
|
onComplete: () => { restoreConnectivity('onComplete'); setShowTouches('onComplete'); },
|
|
409
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Failure evidence. A mobile failure used to reach the report as a single error string — no
|
|
432
|
+
* image, no tree, no device log — which makes triaging a native UI almost impossible.
|
|
433
|
+
*
|
|
434
|
+
* Only runs on failure, so a passing suite pays nothing. Each capture is wrapped SEPARATELY:
|
|
435
|
+
* one unavailable command (a Flutter page source that blows the buffer, a driver with no log
|
|
436
|
+
* endpoint) must not cost us the other two, and nothing in here may convert an already-failing
|
|
437
|
+
* test into a different error.
|
|
438
|
+
*/
|
|
439
|
+
afterTest: async (
|
|
440
|
+
test: { title?: string; parent?: string },
|
|
441
|
+
_context: unknown,
|
|
442
|
+
result: { passed?: boolean },
|
|
443
|
+
): Promise<void> => {
|
|
444
|
+
if (result?.passed) return;
|
|
445
|
+
|
|
446
|
+
// Opt-out for constrained runners (a device farm billing per artifact, a CI box with no disk).
|
|
447
|
+
// A feature tagged @screenshot:on-failure declares its evidence essential and overrides it.
|
|
448
|
+
const forced = (globalThis as Record<string, unknown>).__sungenForceEvidence === true;
|
|
449
|
+
if (!forced && process.env.SUNGEN_MOBILE_EVIDENCE === 'off') return;
|
|
450
|
+
|
|
451
|
+
const feature = test?.parent || 'Tests';
|
|
452
|
+
const title = test?.title || 'test';
|
|
453
|
+
const dir = path.join(EVIDENCE_ROOT, evidenceSlug(feature), evidenceSlug(title));
|
|
454
|
+
const files: Array<{ name: string; path: string; contentType: string }> = [];
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
458
|
+
} catch (e) {
|
|
459
|
+
console.warn(`[sungen] could not create evidence dir ${dir}: ${(e as Error).message}`);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 1. Screenshot — the single most useful artifact for a UI failure.
|
|
464
|
+
try {
|
|
465
|
+
const shot = path.join(dir, 'screenshot.png');
|
|
466
|
+
await driver.saveScreenshot(shot);
|
|
467
|
+
files.push({ name: 'screenshot', path: shot, contentType: 'image/png' });
|
|
468
|
+
} catch (e) {
|
|
469
|
+
console.warn(`[sungen] screenshot capture failed: ${(e as Error).message}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 2. Page source — the accessibility tree, i.e. WHY a selector did not match. Capped: a
|
|
473
|
+
// Flutter/RecyclerView tree can run to tens of MB and there is no value in writing that.
|
|
474
|
+
try {
|
|
475
|
+
const src = await driver.getPageSource();
|
|
476
|
+
const capped = src.length > EVIDENCE_PAGE_SOURCE_MAX
|
|
477
|
+
? `${src.slice(0, EVIDENCE_PAGE_SOURCE_MAX)}\n<!-- [sungen] truncated at ${EVIDENCE_PAGE_SOURCE_MAX} of ${src.length} chars -->`
|
|
478
|
+
: src;
|
|
479
|
+
if (src.length > EVIDENCE_PAGE_SOURCE_MAX) {
|
|
480
|
+
console.warn(`[sungen] page source ${src.length} chars — truncated to ${EVIDENCE_PAGE_SOURCE_MAX}`);
|
|
481
|
+
}
|
|
482
|
+
const out = path.join(dir, 'page-source.xml');
|
|
483
|
+
fs.writeFileSync(out, capped, 'utf-8');
|
|
484
|
+
files.push({ name: 'page-source', path: out, contentType: 'application/xml' });
|
|
485
|
+
} catch (e) {
|
|
486
|
+
console.warn(`[sungen] page source capture failed: ${(e as Error).message}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// 3. Device log — logcat on Android, syslog on iOS. Tail only: a full logcat is unbounded.
|
|
490
|
+
try {
|
|
491
|
+
const logType = driver.isIOS ? 'syslog' : 'logcat';
|
|
492
|
+
const entries = (await driver.getLogs(logType)) as Array<{ message?: string } | string>;
|
|
493
|
+
const lines = entries.map((l) => (typeof l === 'string' ? l : l?.message || '')).filter(Boolean);
|
|
494
|
+
const tail = lines.slice(-EVIDENCE_LOG_TAIL_LINES).join('\n');
|
|
495
|
+
const out = path.join(dir, 'device.log');
|
|
496
|
+
fs.writeFileSync(out, tail, 'utf-8');
|
|
497
|
+
files.push({ name: 'device-log', path: out, contentType: 'text/plain' });
|
|
498
|
+
} catch (e) {
|
|
499
|
+
console.warn(`[sungen] device log capture failed: ${(e as Error).message}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Hand the paths to the reporter so they land in the result's `attachments`. Resolved off
|
|
503
|
+
// globalThis, not imported — see the note in specs/reporters/pw-shape-reporter.ts.
|
|
504
|
+
try {
|
|
505
|
+
const attach = (globalThis as Record<string, unknown>).__sungenAttachEvidence;
|
|
506
|
+
if (typeof attach === 'function') {
|
|
507
|
+
(attach as (f: string, t: string, files: typeof files) => void)(feature, title, files);
|
|
508
|
+
}
|
|
509
|
+
} catch (e) {
|
|
510
|
+
console.warn(`[sungen] could not register evidence with the reporter: ${(e as Error).message}`);
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
|
|
410
514
|
// iOS + locale: XCUITest's `noReset:false` does NOT actually clear the app's data container
|
|
411
515
|
// (verified live 2026-06-11) — a Flutter app that persisted its language on a previous launch keeps
|
|
412
516
|
// it and ignores the new device locale, so the i18n run asserts the wrong language. Wipe the app
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sun-asterisk/sungen",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.29",
|
|
4
4
|
"description": "Deterministic E2E Test Compiler - Gherkin + Selectors → Playwright tests",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -40,8 +40,8 @@
|
|
|
40
40
|
"@babel/types": "^7.28.5",
|
|
41
41
|
"@cucumber/gherkin": "^37.0.0",
|
|
42
42
|
"@cucumber/messages": "^31.0.0",
|
|
43
|
-
"@sungen/driver-data-factory": "3.2.
|
|
44
|
-
"@sungen/driver-ui": "3.2.
|
|
43
|
+
"@sungen/driver-data-factory": "3.2.29",
|
|
44
|
+
"@sungen/driver-ui": "3.2.29",
|
|
45
45
|
"chalk": "^5.6.2",
|
|
46
46
|
"commander": "^14.0.2",
|
|
47
47
|
"dotenv": "^17.2.3",
|
|
@@ -18,9 +18,12 @@
|
|
|
18
18
|
* (`--version`, `capability remove`) even when a driver is missing/broken.
|
|
19
19
|
*/
|
|
20
20
|
import { createRequire } from 'module';
|
|
21
|
+
import * as fs from 'fs';
|
|
21
22
|
import * as path from 'path';
|
|
23
|
+
import { coreVersion } from '../orchestrator/lockstep-version';
|
|
22
24
|
import { capabilityRegistry } from './registry';
|
|
23
25
|
import { LOCAL_DRIVERS, registerCoreCapability } from './builtins';
|
|
26
|
+
import { loadDriverCatalog, readCapabilities } from '../harness/capability';
|
|
24
27
|
|
|
25
28
|
/**
|
|
26
29
|
* Driver packages relocated out of core, loaded before the in-core `LOCAL_DRIVERS`. As `db`/`api`
|
|
@@ -44,18 +47,116 @@ const EXTERNAL_DRIVERS = ['@sungen/driver-ui', '@sungen/driver-db', '@sungen/dri
|
|
|
44
47
|
// test-data available out of the box (no `capability add` needed); inert until a field-map/recipe exists.
|
|
45
48
|
const BUNDLED_DRIVERS = new Set(['@sungen/driver-ui', '@sungen/driver-data-factory']);
|
|
46
49
|
|
|
50
|
+
/** One installed copy of a driver package: where it was found from, and which version lives there. */
|
|
51
|
+
export interface DriverCopy {
|
|
52
|
+
/** The anchor the lookup was made from — the project's package.json, or core's own file. */
|
|
53
|
+
anchor: 'project' | 'core';
|
|
54
|
+
resolved: string;
|
|
55
|
+
version: string | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `3.2.28-beta.2` and `3.2.28` sit on the same lockstep line; `3.2.20` does not. */
|
|
59
|
+
export function sameVersionLine(a: string | undefined, b: string | undefined): boolean {
|
|
60
|
+
if (!a || !b || a === 'latest' || b === 'latest') return false;
|
|
61
|
+
const line = (v: string) => v.split('-')[0];
|
|
62
|
+
return line(a) === line(b);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Pick which installed copy of a driver to load.
|
|
67
|
+
*
|
|
68
|
+
* Project-first is right for an OPT-IN driver: `sungen capability add db` installs into the
|
|
69
|
+
* project's node_modules, and a globally-installed core would never see it from its own anchor.
|
|
70
|
+
*
|
|
71
|
+
* It was wrong for a BUNDLED driver. Core pins `@sungen/driver-ui` to its exact version, but the
|
|
72
|
+
* project may hold an older copy — a `^` range from an earlier scaffold, a lock file nobody
|
|
73
|
+
* refreshed — and that copy loaded first, unconditionally. Core 3.2.25 started gating every state
|
|
74
|
+
* clause on `pattern.states` (SG-E021); driver-ui 3.2.20 predates the field; so
|
|
75
|
+
* `see [Filter] button is hidden` threw on the very pattern (`is-hidden`) the message told the
|
|
76
|
+
* author to use. Every `is <state>` step in every project holding a stale driver, on Gherkin that
|
|
77
|
+
* was correct — and the message blamed the Gherkin.
|
|
78
|
+
*
|
|
79
|
+
* So for a bundled driver the copy on core's own version line wins wherever it lives. Only when no
|
|
80
|
+
* copy is on that line does the first one load — and the caller then warns, because that skew is
|
|
81
|
+
* exactly the breakage the user is about to see. An unknown core version (`latest`) keeps the old
|
|
82
|
+
* project-first order: there is no line to match against.
|
|
83
|
+
*/
|
|
84
|
+
export function pickDriverCopy(copies: readonly DriverCopy[], core: string, bundled: boolean): DriverCopy | undefined {
|
|
85
|
+
if (!copies.length) return undefined;
|
|
86
|
+
if (bundled && core !== 'latest') {
|
|
87
|
+
const lockstep = copies.find((c) => sameVersionLine(c.version, core));
|
|
88
|
+
if (lockstep) return lockstep;
|
|
89
|
+
}
|
|
90
|
+
return copies[0];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function readDriverVersion(req: NodeRequire, name: string, resolved: string): string | undefined {
|
|
94
|
+
// `<pkg>/package.json` is not always exported; fall back to walking up from the entry file.
|
|
95
|
+
try { return req(`${name}/package.json`)?.version; } catch { /* try the walk */ }
|
|
96
|
+
let dir = path.dirname(resolved);
|
|
97
|
+
for (let i = 0; i < 6; i++) {
|
|
98
|
+
try {
|
|
99
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
|
|
100
|
+
if (pkg?.name === name) return pkg.version;
|
|
101
|
+
} catch { /* keep walking */ }
|
|
102
|
+
const parent = path.dirname(dir);
|
|
103
|
+
if (parent === dir) break;
|
|
104
|
+
dir = parent;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The lockstep contract, in the user's terms, with the one command that restores it. */
|
|
110
|
+
function versionSkewWarning(name: string, loaded: string | undefined, core: string, bypassed?: DriverCopy): string {
|
|
111
|
+
const fix = ` Fix: run \`sungen update\` in the project (or \`npm i ${name}@${core}\`) so every @sungen/driver-* matches sungen ${core}.`;
|
|
112
|
+
if (bypassed) {
|
|
113
|
+
return (
|
|
114
|
+
`⚠ sungen: the project holds "${name}" ${bypassed.version ?? '(unknown version)'} but sungen is ${core} — ` +
|
|
115
|
+
`loading core's own ${loaded ?? core} copy instead. The two are published in lockstep; a driver behind core misses ` +
|
|
116
|
+
`metadata core now requires (for the UI driver: every "is hidden"/"is disabled" clause fails SG-E021 on the pattern that should assert it).\n` + fix
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
`⚠ sungen: "${name}" ${loaded ?? '(unknown version)'} is loaded against sungen ${core} — the two are published in lockstep, ` +
|
|
121
|
+
`and a driver on another version line misses metadata core requires (for the UI driver: every "is hidden"/"is disabled" ` +
|
|
122
|
+
`clause fails SG-E021 on the pattern that should assert it).\n` + fix
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
47
126
|
function loadExternalDriver(name: string): void {
|
|
48
|
-
|
|
49
|
-
//
|
|
50
|
-
// core (`npm i -g`) wouldn't
|
|
51
|
-
//
|
|
52
|
-
|
|
127
|
+
const bundled = BUNDLED_DRIVERS.has(name);
|
|
128
|
+
// Look from the user's PROJECT and from core's own location. Opt-in drivers (`sungen capability add
|
|
129
|
+
// db|api`) install into the PROJECT's node_modules — a globally-installed core (`npm i -g`) wouldn't
|
|
130
|
+
// see them via a core-anchored `require`. The bundled @sungen/driver-ui is a core dependency
|
|
131
|
+
// (co-located with core), so the second anchor always has a copy on core's version line.
|
|
132
|
+
const anchors: Array<[DriverCopy['anchor'], string]> = [
|
|
133
|
+
['project', path.join(process.cwd(), 'package.json')],
|
|
134
|
+
['core', __filename],
|
|
135
|
+
];
|
|
136
|
+
const copies: DriverCopy[] = [];
|
|
137
|
+
const requires = new Map<string, NodeRequire>();
|
|
138
|
+
for (const [anchor, file] of anchors) {
|
|
53
139
|
let req: NodeRequire;
|
|
54
|
-
try { req = createRequire(
|
|
140
|
+
try { req = createRequire(file); } catch { continue; }
|
|
55
141
|
let resolved: string;
|
|
56
142
|
try { resolved = req.resolve(name); } catch { continue; } // not installed at this anchor → try next
|
|
143
|
+
if (copies.some((c) => c.resolved === resolved)) continue; // one install seen from both anchors
|
|
144
|
+
requires.set(resolved, req);
|
|
145
|
+
copies.push({ anchor, resolved, version: readDriverVersion(req, name, resolved) });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const core = coreVersion();
|
|
149
|
+
const chosen = pickDriverCopy(copies, core, bundled);
|
|
150
|
+
if (chosen) {
|
|
151
|
+
if (core !== 'latest' && chosen.version && !sameVersionLine(chosen.version, core)) {
|
|
152
|
+
console.warn(versionSkewWarning(name, chosen.version, core));
|
|
153
|
+
} else if (bundled && chosen.anchor === 'core' && copies[0] !== chosen) {
|
|
154
|
+
// A stale project copy was bypassed. Loading the right one already fixed the compile; the line
|
|
155
|
+
// is there so the stale dependency gets cleaned up rather than lingering under every future run.
|
|
156
|
+
console.warn(versionSkewWarning(name, chosen.version, core, copies[0]));
|
|
157
|
+
}
|
|
57
158
|
try {
|
|
58
|
-
const mod =
|
|
159
|
+
const mod = requires.get(chosen.resolved)!(chosen.resolved);
|
|
59
160
|
const register: ((r: typeof capabilityRegistry) => void) | undefined = mod?.sungenDriver?.register ?? mod?.register;
|
|
60
161
|
if (typeof register === 'function') register(capabilityRegistry);
|
|
61
162
|
} catch (e) {
|
|
@@ -68,10 +169,10 @@ function loadExternalDriver(name: string): void {
|
|
|
68
169
|
` Fix: reinstall it (\`sungen capability add ${name.replace(/^@sungen\/driver-/, '')}\`) or remove it.`,
|
|
69
170
|
);
|
|
70
171
|
}
|
|
71
|
-
return;
|
|
172
|
+
return;
|
|
72
173
|
}
|
|
73
174
|
// Not resolvable at ANY anchor.
|
|
74
|
-
if (
|
|
175
|
+
if (bundled) {
|
|
75
176
|
// A bundled default is missing → broken install, not a declined opt-in. Warn loudly so the user
|
|
76
177
|
// sees the real cause instead of a wall of "Unrecognized step pattern".
|
|
77
178
|
console.warn(
|
|
@@ -86,9 +187,44 @@ function loadExternalDriver(name: string): void {
|
|
|
86
187
|
// Opt-in driver not installed — capabilities are opt-in, so skip silently.
|
|
87
188
|
}
|
|
88
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Should this driver package load for the project's declared platform?
|
|
192
|
+
*
|
|
193
|
+
* Only an **opt-in platform** driver is gated. Three exclusions, each load-bearing:
|
|
194
|
+
* - `kind: 'capability'` (api/db/mock/data-factory) is gated by its ANNOTATION TAG, not by
|
|
195
|
+
* platform. Gating it here would break `@query`/`@api` on a project that legitimately has both.
|
|
196
|
+
* - `bundled: true` (driver-ui) is the BASELINE vocabulary, not a platform choice. Mobile reuses
|
|
197
|
+
* the whole web pattern set — click, fill, visibility — on the appium adapter, so skipping
|
|
198
|
+
* driver-ui on a `platform: mobile` project would delete almost every step the suite uses.
|
|
199
|
+
* - No `platform:` in `qa/capabilities.yaml` → load everything, exactly as before. A project that
|
|
200
|
+
* predates the profile, and every `--framework`-override path, must keep working.
|
|
201
|
+
*
|
|
202
|
+
* A package the catalog does not know stays ungated: an unknown driver is not evidence of a
|
|
203
|
+
* platform mismatch, and silently dropping it would be the hardest failure here to diagnose.
|
|
204
|
+
*/
|
|
205
|
+
function servesPlatform(pkg: string, platform: string | undefined): boolean {
|
|
206
|
+
if (!platform) return true;
|
|
207
|
+
const meta = Object.values(loadDriverCatalog()).find((m) => m.package === pkg);
|
|
208
|
+
if (!meta || meta.kind !== 'platform' || meta.bundled) return true;
|
|
209
|
+
if (meta.id === platform) return true;
|
|
210
|
+
// Record what the gate dropped. SG-W001 needs it: without the driver's patterns in the registry a
|
|
211
|
+
// mobile-only step reads as "sungen has no step form for it", which is untrue — the form exists in
|
|
212
|
+
// a vocabulary this project declined. Recording the SKIP is the honest half; naming the step would
|
|
213
|
+
// need a static list that rots on the next pattern added.
|
|
214
|
+
capabilityRegistry.noteGatedDriver(pkg, meta.id, platform);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
89
218
|
export function discoverAndRegisterCapabilities(): void {
|
|
90
219
|
if (capabilityRegistry.isPopulated()) return;
|
|
91
|
-
|
|
220
|
+
// CG-1b — discovery never read the project's platform, so on a machine holding both drivers the
|
|
221
|
+
// mobile vocabulary was live on web projects and vice versa. `platforms:` on the patterns is what
|
|
222
|
+
// makes a cross-platform step FAIL WELL; this is what keeps it from being reachable at all.
|
|
223
|
+
const { platform } = readCapabilities(process.cwd());
|
|
224
|
+
for (const name of EXTERNAL_DRIVERS) {
|
|
225
|
+
if (!servesPlatform(name, platform)) continue;
|
|
226
|
+
loadExternalDriver(name); // ui (external) — first, default capability
|
|
227
|
+
}
|
|
92
228
|
for (const driver of LOCAL_DRIVERS) driver.register(capabilityRegistry); // db, api (in-core for now)
|
|
93
229
|
registerCoreCapability(capabilityRegistry); // generic layer, always present, registered last
|
|
94
230
|
}
|
|
@@ -76,6 +76,10 @@ export interface CapabilityDescriptor {
|
|
|
76
76
|
|
|
77
77
|
export class CapabilityRegistry {
|
|
78
78
|
private caps = new Map<string, CapabilityDescriptor>();
|
|
79
|
+
/** package → the platform it serves, for drivers the platform gate skipped. See `noteGatedDriver`. */
|
|
80
|
+
private gated = new Map<string, string>();
|
|
81
|
+
/** The project platform those skips were made FOR — the same value for every entry in `gated`. */
|
|
82
|
+
private gatedFor: string | undefined;
|
|
79
83
|
|
|
80
84
|
/** Register (or replace, by id) a capability descriptor. Idempotent by id. */
|
|
81
85
|
register(descriptor: CapabilityDescriptor): void {
|
|
@@ -116,9 +120,35 @@ export class CapabilityRegistry {
|
|
|
116
120
|
return kind ? all.filter((s) => s.kind === kind) : all;
|
|
117
121
|
}
|
|
118
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Record a driver discovery SKIPPED because it does not serve the project's platform.
|
|
125
|
+
*
|
|
126
|
+
* Gating removes the driver's patterns from the registry, which is the point — but it also means
|
|
127
|
+
* a step belonging to that driver reaches SG-W001 ("sungen has no step form for it") instead of
|
|
128
|
+
* SG-W020 ("this step is mobile-only"). SG-W001's sentence is then false. This is the only fact
|
|
129
|
+
* available to correct it: patterns were never loaded, so what was skipped is knowable and which
|
|
130
|
+
* step it owned is not.
|
|
131
|
+
*/
|
|
132
|
+
noteGatedDriver(pkg: string, serves: string, projectPlatform: string): void {
|
|
133
|
+
this.gated.set(pkg, serves);
|
|
134
|
+
this.gatedFor = projectPlatform;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Drivers skipped by the platform gate, as `[package, platform it serves]`. Empty when nothing was gated. */
|
|
138
|
+
gatedDrivers(): Array<[string, string]> {
|
|
139
|
+
return [...this.gated.entries()];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The project platform the gate applied, or undefined when nothing was gated. */
|
|
143
|
+
gatedForPlatform(): string | undefined {
|
|
144
|
+
return this.gatedFor;
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
/** Test seam: drop all registrations. */
|
|
120
148
|
_reset(): void {
|
|
121
149
|
this.caps.clear();
|
|
150
|
+
this.gated.clear();
|
|
151
|
+
this.gatedFor = undefined;
|
|
122
152
|
}
|
|
123
153
|
}
|
|
124
154
|
|
|
@@ -92,6 +92,15 @@ function render(r: AuditReport): void {
|
|
|
92
92
|
for (const c of r.duplicates.clusters.slice(0, 3)) {
|
|
93
93
|
L(` ${c.sameDataLikely ? '✗ exact' : '○ EP/data family'} (${c.scenarios.length}): ${c.scenarios.slice(0, 3).join(' | ')}${c.scenarios.length > 3 ? ' …' : ''}`);
|
|
94
94
|
}
|
|
95
|
+
// #694 — printed under the duplicate axis because that is where a reader looks for it, but it
|
|
96
|
+
// is a DIFFERENT signal: ⑤ clusters identical steps, this catches one id on distinct scenarios.
|
|
97
|
+
// Marked BLOCKING inline so a red build states its cause without scrolling to the findings.
|
|
98
|
+
if (r.reusedScenarioIds.length > 0) {
|
|
99
|
+
L(` ✗ BLOCKING — ${r.reusedScenarioIds.length} reused scenario id(s): a shared id ships as several deliverable rows under ONE testcase id`);
|
|
100
|
+
for (const d of r.reusedScenarioIds.slice(0, 3)) {
|
|
101
|
+
L(` ${d.id} ×${d.count}${d.file ? ` in ${d.file}` : ''}: ${d.titles.slice(0, 2).join(' | ')}${d.titles.length > 2 ? ' …' : ''}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
95
104
|
L('');
|
|
96
105
|
L(` ⑥ Traceability — ${(r.trace.mappedRatio * 100).toFixed(0)}% scenarios linked to viewpoint-overview`);
|
|
97
106
|
L(` ${r.trace.note}`);
|
|
@@ -98,7 +98,7 @@ export function registerCapabilityCommand(program: Command): void {
|
|
|
98
98
|
// ── plan (Planner — recommend-only, no install) ──────────────────────────
|
|
99
99
|
cap
|
|
100
100
|
.command('plan')
|
|
101
|
-
.description('Capability Planner: classify execution modes + @manual reasons (M1–
|
|
101
|
+
.description('Capability Planner: classify execution modes + @manual reasons (M1–M11) + recommend drivers (no install)')
|
|
102
102
|
.requiredOption('-s, --screen <name>', 'Screen or flow name')
|
|
103
103
|
.option('--json', 'Output the raw plan JSON')
|
|
104
104
|
.action((o: { screen: string; json?: boolean }) => {
|