@sun-asterisk/sungen 3.2.28 → 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/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/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/code-generator.d.ts.map +1 -1
- package/dist/generators/test-generator/code-generator.js +5 -1
- 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 +100 -17
- package/dist/generators/test-generator/step-mapper.js.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts +23 -1
- package/dist/generators/test-generator/utils/selector-resolver.d.ts.map +1 -1
- package/dist/generators/test-generator/utils/selector-resolver.js +78 -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 +51 -28
- 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/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 +48 -0
- package/dist/harness/sensors.d.ts.map +1 -1
- package/dist/harness/sensors.js +26 -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 +18 -3
- 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 +10 -3
- package/dist/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +88 -2
- 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-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/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/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/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/code-generator.ts +5 -1
- 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 +107 -17
- package/src/generators/test-generator/utils/selector-resolver.ts +88 -12
- package/src/harness/audit.ts +55 -30
- 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/parse.ts +7 -1
- package/src/harness/repair.ts +8 -1
- package/src/harness/sensors.ts +75 -1
- 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 +18 -3
- 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 +10 -3
- package/src/orchestrator/templates/ai-src/skills/sungen-tc-generation/SKILL.md +88 -2
- 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-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/wdio.conf.ts +105 -1
|
@@ -46,6 +46,12 @@ interface SelectorEntry {
|
|
|
46
46
|
// === v2: Table ===
|
|
47
47
|
columns?: Record<string, { index: number; header: string; elements?: Record<string, SelectorEntry> }>;
|
|
48
48
|
pagination?: Record<string, string>;
|
|
49
|
+
// What EVERY row of this table matches, relative to the table locator (issue #688). The table
|
|
50
|
+
// templates assume an ARIA table — `getByRole('row')`, `tbody` — and on a list built from plain
|
|
51
|
+
// <div>s that universe is 0 elements, so `is hidden` / `is empty` pass without checking anything.
|
|
52
|
+
// `rows: 'div.job-row'` replaces the whole row chain. Distinct from a ROW LABEL's own `type: locator`
|
|
53
|
+
// entry, which positions ONE row (`tbody tr:first-child`) — a shape is a property of the table.
|
|
54
|
+
rows?: string;
|
|
49
55
|
|
|
50
56
|
// === v2: Assertion Helpers ===
|
|
51
57
|
attribute?: string; // Attribute to check (for "has" assertions)
|
|
@@ -130,6 +136,7 @@ export interface ResolvedSelector {
|
|
|
130
136
|
contenteditable?: boolean; // Rich text editor
|
|
131
137
|
columns?: Record<string, { index: number; header: string; elements?: Record<string, any> }>;
|
|
132
138
|
pagination?: Record<string, string>;
|
|
139
|
+
rows?: string; // Row shape, relative to the table (issue #688) — see SelectorEntry.rows
|
|
133
140
|
attribute?: string; // Attribute to check
|
|
134
141
|
pattern?: string; // Regex for attribute value
|
|
135
142
|
expanded?: { class?: string; attribute?: string; state?: 'none' }; // Expand/collapse state signal
|
|
@@ -150,10 +157,35 @@ export class SelectorResolver {
|
|
|
150
157
|
private diagnostics?: DiagnosticsCollector;
|
|
151
158
|
private contentNamedRoles: string[] = DEFAULT_CONTENT_NAMED_ROLES;
|
|
152
159
|
private warnedContentNamed = new Set<string>();
|
|
160
|
+
/** The adapter's target platform — gates the list-member auto-infer (#665). Defaults to web. */
|
|
161
|
+
private platform: 'web' | 'mobile' = 'web';
|
|
153
162
|
|
|
154
|
-
constructor(selectorsDir?: string, screenName?: string) {
|
|
163
|
+
constructor(selectorsDir?: string, screenName?: string, platform: 'web' | 'mobile' = 'web') {
|
|
155
164
|
this.selectorsDir = selectorsDir || path.join(process.cwd(), 'qa', 'selectors', 'screens');
|
|
156
165
|
this.screenName = screenName;
|
|
166
|
+
this.platform = platform;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A role that resolved with NO accessible name, on the mobile adapter — a dead selector (#665).
|
|
171
|
+
*
|
|
172
|
+
* `appium-selector.hbs` renders a role as `~{{name || value}}`, so with no name it emits the role
|
|
173
|
+
* TOKEN itself: `~listitem`, `~status`. A native a11y tree has no role concept and contains no
|
|
174
|
+
* such string, so the locator can never match — yet it compiled clean and only failed at run time
|
|
175
|
+
* with `element with selector "~listitem" wasn't found`.
|
|
176
|
+
*
|
|
177
|
+
* The invariant is "role with no name", NOT "role in LIST_MEMBER_ROLES". Both are reachable and
|
|
178
|
+
* both are dead:
|
|
179
|
+
* · auto-infer — only the three LIST_MEMBER_ROLES are nameless (the other 22 default the label)
|
|
180
|
+
* · an entry — a list-member role withholds the name by design, and `name: null` is the
|
|
181
|
+
* documented opt-out for a content-named role, so `type: role` reaches it too
|
|
182
|
+
* One predicate covers every door; the CALLERS choose the remedy, because it differs: with no
|
|
183
|
+
* entry the fix is "add one", with an entry the fix is "change its type".
|
|
184
|
+
*
|
|
185
|
+
* Web is unaffected — there the role IS the locator (see LIST_MEMBER_ROLES).
|
|
186
|
+
*/
|
|
187
|
+
private isUnusableMobileRole(r: ResolvedSelector | null | undefined): boolean {
|
|
188
|
+
return this.platform === 'mobile' && !!r && r.strategy === 'role' && !r.name;
|
|
157
189
|
}
|
|
158
190
|
|
|
159
191
|
/** Attach the compile-diagnostics collector (SG-W003) */
|
|
@@ -452,11 +484,28 @@ export class SelectorResolver {
|
|
|
452
484
|
// Auto-infer locator from elementType when no exact YAML entry exists
|
|
453
485
|
if (elementType) {
|
|
454
486
|
const inferred = SelectorResolver.inferFromElementType(elementType, displayLabel);
|
|
455
|
-
|
|
487
|
+
|
|
488
|
+
// #665 — auto-infer is an ARIA concept. It survives on mobile only because a label-named
|
|
489
|
+
// role hands the appium template a `name` to render as an accessibility-id (`[Submit]
|
|
490
|
+
// button` → `~Submit`, a plausible guess). A nameless role has nothing to hand it, and the
|
|
491
|
+
// template's `~{{name || value}}` fell through to the role TOKEN — `[Job List] item` →
|
|
492
|
+
// `~listitem`, a string no Android/iOS a11y tree contains. Clean compile, guaranteed miss.
|
|
493
|
+
//
|
|
494
|
+
// Declining rather than warning is what repairs the workflow. `sungen-selector-fix` Phase 0
|
|
495
|
+
// decides whether to scan the live page by asking whether the ref "can't be auto-inferred";
|
|
496
|
+
// while auto-infer answered yes, Phase 0 skipped the scan, so no real selector was ever
|
|
497
|
+
// written and a warning would have reported the breakage with the repair path still switched
|
|
498
|
+
// off. Declining makes Phase 0 scan, and if it was skipped the not-found throw below names
|
|
499
|
+
// the exact key to add — the remedy here is "add an entry", which is what that error says.
|
|
500
|
+
//
|
|
501
|
+
// Web is untouched: `getByRole('listitem')` with no name is a real, working locator there.
|
|
502
|
+
if (inferred && !this.isUnusableMobileRole(inferred)) {
|
|
456
503
|
// Same SG-W003 class as the explicit-entry path: an auto-inferred name on a
|
|
457
504
|
// name-from-content role ([Loading] spinner → getByRole('status', { name: 'Loading' }))
|
|
458
505
|
// can never match. Add a YAML entry with name: null to opt out.
|
|
459
|
-
|
|
506
|
+
// WEB ONLY — see the note at the entry-path emission (#665).
|
|
507
|
+
if (this.platform !== 'mobile'
|
|
508
|
+
&& inferred.strategy === 'role' && inferred.name && inferred.role && this.contentNamedRoles.includes(inferred.role)) {
|
|
460
509
|
const warnKey = `${inferred.role}:${displayLabel}`;
|
|
461
510
|
if (!this.warnedContentNamed.has(warnKey)) {
|
|
462
511
|
this.warnedContentNamed.add(warnKey);
|
|
@@ -584,6 +633,7 @@ export class SelectorResolver {
|
|
|
584
633
|
if (entry.contenteditable) v2Fields.contenteditable = true;
|
|
585
634
|
if (entry.columns) v2Fields.columns = entry.columns;
|
|
586
635
|
if (entry.pagination) v2Fields.pagination = entry.pagination;
|
|
636
|
+
if (typeof entry.rows === 'string' && entry.rows.trim()) v2Fields.rows = entry.rows.trim();
|
|
587
637
|
if (entry.attribute) v2Fields.attribute = entry.attribute;
|
|
588
638
|
if (entry.pattern) v2Fields.pattern = entry.pattern;
|
|
589
639
|
if (entry.expanded) v2Fields.expanded = entry.expanded;
|
|
@@ -658,7 +708,16 @@ export class SelectorResolver {
|
|
|
658
708
|
// reference text. For name-from-content roles (status/alert/…) that filter can
|
|
659
709
|
// never match the live element (its accessible name is computed from its content,
|
|
660
710
|
// e.g. a toast's message text) — issue #467. Opt out with `name: null`.
|
|
661
|
-
|
|
711
|
+
//
|
|
712
|
+
// WEB ONLY. "The accessible name is computed from content" is an ARIA rule with no mobile
|
|
713
|
+
// counterpart: the appium template turns a role into an accessibility-id lookup, so
|
|
714
|
+
// `[Loading] spinner` → `~Loading` is exactly as legitimate a guess as `[Submit] button`
|
|
715
|
+
// → `~Submit`, which this compiler emits happily. On mobile the warning was therefore a
|
|
716
|
+
// false positive — and worse, its remedy made things strictly worse: `name: null` leaves
|
|
717
|
+
// the role nameless, which now renders as the role TOKEN and is refused outright (#665).
|
|
718
|
+
// A wrong selector fails loudly at run time; advice that manufactures a dead one does not.
|
|
719
|
+
if (this.platform !== 'mobile'
|
|
720
|
+
&& entry.name === undefined && name && this.contentNamedRoles.includes(value)) {
|
|
662
721
|
const warnKey = `${value}:${originalLabel}`;
|
|
663
722
|
if (!this.warnedContentNamed.has(warnKey)) {
|
|
664
723
|
this.warnedContentNamed.add(warnKey);
|
|
@@ -669,14 +728,31 @@ export class SelectorResolver {
|
|
|
669
728
|
);
|
|
670
729
|
}
|
|
671
730
|
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
731
|
+
{
|
|
732
|
+
const resolvedRole = withExtras({
|
|
733
|
+
strategy: 'role',
|
|
734
|
+
role: value,
|
|
735
|
+
name: name, // Use the name field for accessible name
|
|
736
|
+
value,
|
|
737
|
+
nth,
|
|
738
|
+
exact,
|
|
739
|
+
});
|
|
740
|
+
// #665 — the same dead selector as the auto-infer path, reached through an ENTRY: a
|
|
741
|
+
// list-member role withholds its name by design, and `name: null` is the documented
|
|
742
|
+
// opt-out for a content-named role. Either way a mobile role with no name renders as
|
|
743
|
+
// the role token. Different remedy from the auto-infer case, so a different message:
|
|
744
|
+
// the entry exists, it is the TYPE that cannot work here.
|
|
745
|
+
if (this.isUnusableMobileRole(resolvedRole)) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
`Selector "${originalLabel}" is \`type: role, value: ${value}\`${name === null ? ' with `name: null`' : ''}, ` +
|
|
748
|
+
`which resolves to a role with no accessible name. A native app has no role concept, so this compiles to ` +
|
|
749
|
+
`the accessibility-id "~${value}" — a string no Android/iOS a11y tree contains, and the step can never match. ` +
|
|
750
|
+
`Give this entry a locator the device actually exposes: \`type: accessibility-id\` (a content-desc / ` +
|
|
751
|
+
`accessibilityIdentifier), \`type: xpath\`, or \`type: id\`.`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
return resolvedRole;
|
|
755
|
+
}
|
|
680
756
|
|
|
681
757
|
case 'testid':
|
|
682
758
|
return withExtras({
|
package/src/harness/audit.ts
CHANGED
|
@@ -13,20 +13,20 @@ import { checkViewpointBaseline, ViewpointBaseline } from './viewpoint-baseline'
|
|
|
13
13
|
import { flowQuality, statefulDepthFor, continuityGaps, flowInventory, misfiledPhases, FlowQualityResult, InventoryResult } from './flow-contract';
|
|
14
14
|
import { featureFilesFor } from './unit-paths';
|
|
15
15
|
import {
|
|
16
|
-
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, traceability, claimProof, taxonomyLint,
|
|
16
|
+
loadCatalog, viewpointGate, assertionDepth, dataThemesFor, depthThresholdFor, coverageBalance, duplicateClusters, scenarioIdReuse, ReusedScenarioId, traceability, claimProof, taxonomyLint,
|
|
17
17
|
automatableManual, flowCoveredThemes, flowRegressionDepth, oracleStrength, declaredPageType,
|
|
18
18
|
numericClaimGaps, priorityMismatches,
|
|
19
19
|
GateResult, DepthResult, BalanceResult, DuplicateResult, TraceResult, ClaimProofResult, TaxonomyResult, Catalog, AutomatableManualResult, FlowDepthResult, OracleStrengthResult,
|
|
20
20
|
} from './sensors';
|
|
21
21
|
import { loadFlowScenarios } from './flow-check';
|
|
22
|
-
import { manualReasonMismatches, MANUAL_REASONS, buildPlan } from './capability-plan';
|
|
22
|
+
import { manualReasonMismatches, MANUAL_REASONS, buildPlan, KEEP_REASON_CODES, MANUAL_REASON_RANGE } from './capability-plan';
|
|
23
23
|
import { readCapabilities, verificationScopeFindings } from './capability';
|
|
24
24
|
import { readIntent, projectRootFromScreenDir, IntentProfile } from './intent';
|
|
25
25
|
import { getProvenance, Provenance } from './provenance';
|
|
26
26
|
import { specCoverage, SpecCoverageResult, parseSpecClauses, restatedRequirementSources } from './spec-coverage';
|
|
27
27
|
import { downstreamScope, manualOracle, readText, DownstreamResult, ManualOracleResult,
|
|
28
28
|
negativeSideEffect, sourceBacked, crossArtifactOwnership, isolationRisk, serialCascadeRisk } from './quality-gates';
|
|
29
|
-
import { viewpointLedger, parseViewpointItems, browserGestureSubstitutions, viewpointIntegrity, LedgerResult } from './viewpoint-ledger';
|
|
29
|
+
import { viewpointLedger, parseViewpointItems, browserGestureSubstitutions, substitutionRemedy, viewpointIntegrity, LedgerResult } from './viewpoint-ledger';
|
|
30
30
|
import { specBranches, unenumeratedBranches, sameOracleClusters, permissionPairGaps, awaitingSeedData, unconsideredRisks, MULTI_SCREEN_RISKS } from './spec-branches';
|
|
31
31
|
const MULTI_SCREEN_RISK_COUNT = MULTI_SCREEN_RISKS.length;
|
|
32
32
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
@@ -45,6 +45,8 @@ export interface AuditReport {
|
|
|
45
45
|
duplicates: DuplicateResult;
|
|
46
46
|
trace: TraceResult;
|
|
47
47
|
taxonomyMismatch: boolean; // scenarios use IDs not in the project's test-viewpoint.md
|
|
48
|
+
/** #694 — two different scenarios in one .feature carrying the same VP id. Gate-blocking. */
|
|
49
|
+
reusedScenarioIds: ReusedScenarioId[];
|
|
48
50
|
downstream: DownstreamResult; // downstream screens referenced but under-covered
|
|
49
51
|
manualOracle: ManualOracleResult; // @manual scenarios lacking setup/action/oracle
|
|
50
52
|
automatableManual: AutomatableManualResult; // @manual that is actually automatable (deferred, not judgment) — TQ-2
|
|
@@ -460,30 +462,15 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
460
462
|
if (!flowQ.contract!.minimalGuarantee) {
|
|
461
463
|
findings.push('FLOW-GUARANTEE-MISSING: the contract declares no `minimalGuarantee:` — the postcondition that must hold in EVERY flow, including failure ("no second account for the same email", "no verification mail on a rejected submit"). Exception/guard scenarios have nothing to assert against without it; add it (and `successGuarantee:` for the goal-reached side).');
|
|
462
464
|
}
|
|
463
|
-
// #599 — integrity of the DECLARATION itself: three ways it can be internally wrong while
|
|
464
|
-
// every coverage number still reads well.
|
|
465
|
-
{
|
|
466
|
-
const testDataDir = path.join(screenDir, 'test-data');
|
|
467
|
-
const extra: Array<{ label: string; text: string }> = [];
|
|
468
|
-
try {
|
|
469
|
-
for (const f of fs.readdirSync(testDataDir).filter((x) => x.endsWith('.yaml'))) {
|
|
470
|
-
extra.push({ label: `test-data/${f}`, text: readTextFile(path.join(testDataDir, f)) ?? '' });
|
|
471
|
-
}
|
|
472
|
-
} catch { /* api units and fresh scaffolds carry none */ }
|
|
473
|
-
const vi = viewpointIntegrity(viewpointPath, scenarios.map((sc) => sc.vpId ?? '').filter(Boolean), extra);
|
|
474
|
-
for (const d of vi.duplicateIds.slice(0, 4)) {
|
|
475
|
-
findings.push(`VIEWPOINT-ID-REUSED: "${d.id}" labels ${d.count} different claims (${d.texts.map((t) => `"${t.slice(0, 44)}…"`).join(' · ')}). The ledger then counts ${d.count} items while traceability sees ONE id, so both the coverage arithmetic and "which test proves this?" go wrong — and neither number looks suspicious. Give each atomic claim its own id; several ids may share a flow.`);
|
|
476
|
-
}
|
|
477
|
-
for (const d of vi.danglingRefs.slice(0, 5)) {
|
|
478
|
-
findings.push(`TRACE-REF-DANGLING: ${d.where} cites "${d.ref}", which no scenario carries. Nothing breaks at runtime, so a wrong pointer survives every refactor and quietly misleads the next maintainer. Fix the reference, or the scenario id it was meant to name.`);
|
|
479
|
-
}
|
|
480
|
-
if (vi.uniformPriority) {
|
|
481
|
-
findings.push(`VIEWPOINT-PRIORITY-UNIFORM: all ${vi.uniformPriority.rows} rows of the Priority Viewpoints table are "${vi.uniformPriority.levels[0]}". That is a formality, not a priority: nothing can be selected for a smoke run and the release conversation has no input. Rank by business impact and risk — the basic flow and the data-integrity guards are not the same bet as an exit path a screen suite already covers.`);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
465
|
// #595 — a viewpoint item naming a browser-level gesture, answered with an in-app control.
|
|
485
466
|
for (const g of browserGestureSubstitutions(viewpointPath, scenarios, platform).slice(0, 4)) {
|
|
486
|
-
|
|
467
|
+
// The remedy is not the same on both platforms, and getting it wrong is the difference between
|
|
468
|
+
// useful advice and a lie: web still has no reload/back step (#593), so `@manual` is the honest
|
|
469
|
+
// reply — but every mobile gesture in this set now compiles, so telling a mobile author to defer
|
|
470
|
+
// would send them to a manual procedure for work the compiler does. Read from the ledger entry
|
|
471
|
+
// (`substitutionRemedy`), never re-derived here, so a step landing for web needs no edit here.
|
|
472
|
+
const remedy = substitutionRemedy(g);
|
|
473
|
+
findings.push(`VIEWPOINT-GESTURE-SUBSTITUTED: the viewpoint asks for "${g.gesture}" ("${g.item.slice(0, 80)}…") but the suite exercises an in-app control instead. They are different mechanisms — an app button runs the screen's own re-hydrate logic and the gesture does not, so a defect that only shows on the gesture's own path lives in exactly that gap. ${remedy}`);
|
|
487
474
|
}
|
|
488
475
|
// #595 — a flow whose requirement list is a hand restatement of the screen specs it
|
|
489
476
|
// traverses. `specFR` read 100% over two restated FRs while the guard clause the flow most
|
|
@@ -523,6 +510,41 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
523
510
|
findings.push(`FLOW-HANDOFF-SHALLOW: only ${flowQ.handoffs.asserted}/${flowQ.handoffs.total} cross-screen transitions are followed by an assertion in the new screen — carried state (the thing a flow exists to verify) is unproven at the handoff. businessDepth is capped until asserted.`);
|
|
524
511
|
}
|
|
525
512
|
}
|
|
513
|
+
// #694 — two DIFFERENT scenarios wearing the same VP id. BLOCKING (it joins the gate
|
|
514
|
+
// conjunction below) where the declaration-integrity findings that follow are advisory: a
|
|
515
|
+
// reused id does not stop here, it reaches the customer. `generateTcId` builds the delivered
|
|
516
|
+
// testcase id straight out of the vpId, so the collision ships as two rows carrying ONE id in
|
|
517
|
+
// the deliverable — and the result-merge fallback has already attached one sibling's run
|
|
518
|
+
// result to another, delivering 7 never-executed testcases as "Passed" (see
|
|
519
|
+
// `exporters/scenario-merger.ts`). `matrix/gates.ts` already grades this `severity: 'error'`,
|
|
520
|
+
// but only at delivery; blocking at audit is what makes it cheap to fix (renumber, regenerate).
|
|
521
|
+
const reusedIds = scenarioIdReuse(scenarios);
|
|
522
|
+
for (const r of reusedIds.slice(0, 6)) {
|
|
523
|
+
findings.push(`SCENARIO-ID-REUSED: ${r.count} different scenarios in ${r.file || 'this feature'} carry the id "${r.id}" (${r.titles.map((t) => `"${t.slice(0, 52)}…"`).join(' · ')}). Every axis stays green — the duplicate sensor clusters identical STEPS, so scenarios that merely share an id sail past it — while the deliverable gets ${r.count} rows under one testcase id and the run-result merge can attach the wrong scenario's result. Give each scenario its own sequence number within its category; the ledger item that now reads UNCOVERED is the sibling that lost its number.`);
|
|
524
|
+
}
|
|
525
|
+
// #599 — integrity of the DECLARATION itself: three ways it can be internally wrong while
|
|
526
|
+
// every coverage number still reads well. Advisory. This used to sit inside the
|
|
527
|
+
// `flowQ.hasContract` branch above, so a SCREEN unit never reached it at all — the checks ran
|
|
528
|
+
// on flows only, silently, for every release since they landed (#694).
|
|
529
|
+
{
|
|
530
|
+
const testDataDir = path.join(screenDir, 'test-data');
|
|
531
|
+
const extra: Array<{ label: string; text: string }> = [];
|
|
532
|
+
try {
|
|
533
|
+
for (const f of fs.readdirSync(testDataDir).filter((x) => x.endsWith('.yaml'))) {
|
|
534
|
+
extra.push({ label: `test-data/${f}`, text: readTextFile(path.join(testDataDir, f)) ?? '' });
|
|
535
|
+
}
|
|
536
|
+
} catch { /* api units and fresh scaffolds carry none */ }
|
|
537
|
+
const vi = viewpointIntegrity(viewpointPath, scenarios.map((sc) => sc.vpId ?? '').filter(Boolean), extra);
|
|
538
|
+
for (const d of vi.duplicateIds.slice(0, 4)) {
|
|
539
|
+
findings.push(`VIEWPOINT-ID-REUSED: "${d.id}" labels ${d.count} different claims (${d.texts.map((t) => `"${t.slice(0, 44)}…"`).join(' · ')}). The ledger then counts ${d.count} items while traceability sees ONE id, so both the coverage arithmetic and "which test proves this?" go wrong — and neither number looks suspicious. Give each atomic claim its own id; several ids may share a flow.`);
|
|
540
|
+
}
|
|
541
|
+
for (const d of vi.danglingRefs.slice(0, 5)) {
|
|
542
|
+
findings.push(`TRACE-REF-DANGLING: ${d.where} cites "${d.ref}", which no scenario carries. Nothing breaks at runtime, so a wrong pointer survives every refactor and quietly misleads the next maintainer. Fix the reference, or the scenario id it was meant to name.`);
|
|
543
|
+
}
|
|
544
|
+
if (vi.uniformPriority) {
|
|
545
|
+
findings.push(`VIEWPOINT-PRIORITY-UNIFORM: all ${vi.uniformPriority.rows} rows of the Priority Viewpoints table are "${vi.uniformPriority.levels[0]}". That is a formality, not a priority: nothing can be selected for a smoke run and the release conversation has no input. Rank by business impact and risk — the basic flow and the data-integrity guards are not the same bet as an exit path a screen suite already covers.`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
526
548
|
// #592 W5 — a continuity mechanism proved on one side only.
|
|
527
549
|
if (isUiFlow) {
|
|
528
550
|
const scenarioTexts = scenarios.map((s) => `${s.name} ${s.stepsText}`);
|
|
@@ -536,7 +558,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
536
558
|
// manual, most of them M5-inbox — invisible while tagged bare).
|
|
537
559
|
const bareManual = (featureText.match(/@manual(?![:\w])/g) || []).length;
|
|
538
560
|
if (bareManual > 0) {
|
|
539
|
-
findings.push(`MANUAL-CODE-MISSING: ${bareManual} @manual tag(s) carry no reason code — tag @manual
|
|
561
|
+
findings.push(`MANUAL-CODE-MISSING: ${bareManual} @manual tag(s) carry no reason code — tag @manual:${MANUAL_REASON_RANGE} (M5 = external dependency e.g. inbox, M3 = mock/network, M7 = environment, M10 = device/OS capability, M11 = real-device only) so the capability planner can route what a driver would automate.`);
|
|
540
562
|
}
|
|
541
563
|
}
|
|
542
564
|
for (const c of flowCredits) {
|
|
@@ -671,7 +693,7 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
671
693
|
const plan = buildPlan(screenDir, screenName, catalogScreenName, platform);
|
|
672
694
|
if (plan.recommendations.length) {
|
|
673
695
|
const recs = plan.recommendations.map((r) => `\`sungen capability add ${r.driver}\` (automates ${r.count})`).join(' · ');
|
|
674
|
-
findings.push(`CAPABILITY-SUGGESTION: ${plan.capabilityManual} @manual scenario(s) are capability-manual (a driver could automate them) — ${recs}. Recommend-only: nothing is installed automatically; the ${plan.judgmentManual} judgment-manual (
|
|
696
|
+
findings.push(`CAPABILITY-SUGGESTION: ${plan.capabilityManual} @manual scenario(s) are capability-manual (a driver could automate them) — ${recs}. Recommend-only: nothing is installed automatically; the ${plan.judgmentManual} judgment-manual (${KEEP_REASON_CODES.join('/')}) correctly stay manual.`);
|
|
675
697
|
}
|
|
676
698
|
// Named, not dropped: without this the scenarios read as "nothing could help", and the
|
|
677
699
|
// operator's only clue was an install that dead-ends (#597).
|
|
@@ -835,15 +857,18 @@ export function runAudit(screenDir: string, screenName: string): AuditReport {
|
|
|
835
857
|
const calibration = { axes, weakest, inflated, notApplicable, cappedAt: capped ? cap : undefined };
|
|
836
858
|
|
|
837
859
|
// Gate spans coverage (viewpoint themes), depth, claim-proof, spec-clause coverage,
|
|
838
|
-
//
|
|
860
|
+
// taxonomy-match (scenarios must use the project's viewpoint IDs when defined),
|
|
861
|
+
// AND scenario-id uniqueness — a reused id reaches the customer's deliverable as two rows
|
|
862
|
+
// under one testcase id, and has already shipped a never-executed testcase as "Passed" (#694).
|
|
839
863
|
const gateStatus: 'PASS' | 'FAIL' =
|
|
840
|
-
gate.gaps.length === 0 && depth.verdict !== 'fail' && claim.verdict !== 'fail' && spec.verdict !== 'fail' && !taxonomyMismatch && !gateSensorError
|
|
864
|
+
gate.gaps.length === 0 && depth.verdict !== 'fail' && claim.verdict !== 'fail' && spec.verdict !== 'fail' && !taxonomyMismatch && !gateSensorError
|
|
865
|
+
&& reusedIds.length === 0 ? 'PASS' : 'FAIL';
|
|
841
866
|
|
|
842
867
|
return {
|
|
843
868
|
screen: screenName,
|
|
844
869
|
scenarioCount: scenarios.length,
|
|
845
870
|
gate, depth, claim, taxonomy, balance, duplicates, trace, spec,
|
|
846
|
-
taxonomyMismatch, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
|
|
871
|
+
taxonomyMismatch, reusedScenarioIds: reusedIds, downstream, manualOracle: manualOracleResult, automatableManual: autoManual, flowDepth, oracle, ledger, viewpointBaseline, calibration,
|
|
847
872
|
...(isUiFlow ? { flowQuality: flowQ, flowInventory: inventory ?? undefined } : {}),
|
|
848
873
|
score: {
|
|
849
874
|
overall: Math.round(overall * 10) / 10,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Capability Planner (Phase 2b) — deterministic, recommend-only.
|
|
3
3
|
*
|
|
4
4
|
* Classifies each scenario's execution mode + each @manual case by reason code
|
|
5
|
-
* (M1–
|
|
5
|
+
* (M1–M11), maps capability-reasons to drivers, and emits the manual-reason KPI.
|
|
6
6
|
* Never installs anything (that's `sungen capability add`). See
|
|
7
7
|
* docs/spec/sungen_phase2b_spec.md.
|
|
8
8
|
*/
|
|
@@ -22,13 +22,37 @@ export const MANUAL_REASONS: Record<string, ReasonDef> = {
|
|
|
22
22
|
M4: { code: 'M4', label: 'Missing stable selector/test-id', cls: 'capability', drivers: [] }, // locator contract, no driver
|
|
23
23
|
M5: { code: 'M5', label: 'External dependency', cls: 'capability', drivers: ['mail-file', 'contract'] },
|
|
24
24
|
M6: { code: 'M6', label: 'Visual / UX judgment', cls: 'keep', drivers: ['specialized'] },
|
|
25
|
+
// `capability` with no driver is deliberate and matches M4: the case IS automatable once the
|
|
26
|
+
// environment exists, it just is not an `sungen capability add` away. Reclassifying it as `keep`
|
|
27
|
+
// would claim the opposite — that no environment could ever run it — and would silently move
|
|
28
|
+
// every existing M7 scenario from the capability-manual count to judgment-manual.
|
|
25
29
|
M7: { code: 'M7', label: 'Environment limitation', cls: 'capability', drivers: [] }, // testability, no driver
|
|
26
30
|
M8: { code: 'M8', label: 'Not worth automating', cls: 'keep', drivers: [] },
|
|
27
31
|
M9: { code: 'M9', label: 'True human judgment', cls: 'keep', drivers: [] },
|
|
32
|
+
// Mobile-specific (M10/M11). Both are `keep` with no driver, and that is the honest answer:
|
|
33
|
+
// nothing installable automates them, so the planner must not imply otherwise. Before these
|
|
34
|
+
// existed every mobile-specific reason collapsed into M7, which made "needs a real device" and
|
|
35
|
+
// "waiting on staging infra" indistinguishable in the report.
|
|
36
|
+
M10: { code: 'M10', label: 'Device/OS capability', cls: 'keep', drivers: [] },
|
|
37
|
+
M11: { code: 'M11', label: 'Real-device only', cls: 'keep', drivers: [] },
|
|
28
38
|
// Not a single-screen driver gap: automatable cross-screen via a flow (/sungen:add-flow).
|
|
29
39
|
XS: { code: 'XS', label: 'Cross-screen (automate via flow)', cls: 'flow', drivers: [] },
|
|
30
40
|
};
|
|
31
41
|
|
|
42
|
+
/** Codes that stay manual whatever is installed — derived, so a new `keep` code joins by itself. */
|
|
43
|
+
export const KEEP_REASON_CODES: string[] = Object.values(MANUAL_REASONS)
|
|
44
|
+
.filter((r) => r.cls === 'keep')
|
|
45
|
+
.map((r) => r.code);
|
|
46
|
+
|
|
47
|
+
/** The documented reason-code range, e.g. "M1..M11". Derived so guidance can never go stale. */
|
|
48
|
+
export const MANUAL_REASON_RANGE: string = (() => {
|
|
49
|
+
const ns = Object.keys(MANUAL_REASONS)
|
|
50
|
+
.filter((c) => /^M\d+$/.test(c))
|
|
51
|
+
.map((c) => Number(c.slice(1)))
|
|
52
|
+
.sort((a, b) => a - b);
|
|
53
|
+
return ns.length ? `M${ns[0]}..M${ns[ns.length - 1]}` : '';
|
|
54
|
+
})();
|
|
55
|
+
|
|
32
56
|
// Keyword inference (best-effort retrofit). Order matters — first match wins.
|
|
33
57
|
// Cross-screen "deferred to a flow" is checked FIRST: it is not a driver gap on
|
|
34
58
|
// this screen, it is handled by a flow — so it must not recommend a driver.
|
|
@@ -41,6 +65,14 @@ const INFER: { code: string; re: RegExp }[] = [
|
|
|
41
65
|
// "email" alone is too greedy (every subscription test mentions it) → require a real external-mail
|
|
42
66
|
// signal (verification mail / OTP / inbox), so an API/DB-oracle test isn't misread as M5.
|
|
43
67
|
{ code: 'M5', re: /\b(external|third[-\s]?party|sandbox|payment gateway|invoice|download|verification (e?mail|link)|otp|mailbox|inbox|e?mail link)\b/i },
|
|
68
|
+
// Mobile-specific, and they MUST sit ahead of M6/M7 or they never fire: M6 matches
|
|
69
|
+
// /keyboard|accessibilit|responsive/ and M7 matches /environment/, either of which would take a
|
|
70
|
+
// mobile reason first. They stay BEHIND M1–M5 on purpose, so a mobile phrasing of an ordinary
|
|
71
|
+
// driver gap ("assert via API", "needs a stable test-id") still routes to the driver that solves
|
|
72
|
+
// it. One known consequence of that order: "IAP sandbox" hits M5's `sandbox` before M10 — the
|
|
73
|
+
// external-payment reading is the more useful one, and the order test pins it either way.
|
|
74
|
+
{ code: 'M11', re: /\b(real device|máy thật|physical device|nfc|biometric|fingerprint|face id|touch id|camera thật|real camera|push token)\b/i },
|
|
75
|
+
{ code: 'M10', re: /\b(notification shade|app switcher|recents|in-app purchase|iap|interruption|incoming call|permission variance|os version)\b/i },
|
|
44
76
|
{ code: 'M6', re: /\b(visual|responsive|layout|accessibilit|a11y|keyboard|screen reader|ux|breakpoint)\b/i },
|
|
45
77
|
{ code: 'M7', re: /\b(environment|staging[-\s]?only|infra|env limitation)\b/i },
|
|
46
78
|
{ code: 'M8', re: /\b(not worth|exploratory|one[-\s]?off)\b/i },
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
# status: shipped → published as an npm package (R5); planned → not built yet.
|
|
11
11
|
# bundled: true → installed automatically (a dependency of @sun-asterisk/sungen),
|
|
12
12
|
# so `capability add` is unnecessary.
|
|
13
|
-
# unblocks: manual-reason codes (M1–
|
|
13
|
+
# unblocks: manual-reason codes (M1–M11) this driver resolves (Phase 2b taxonomy).
|
|
14
14
|
#
|
|
15
15
|
# R5 status: the three real capabilities ship as packages — @sungen/driver-ui (web UI,
|
|
16
16
|
# bundled as the default), @sungen/driver-db, @sungen/driver-api. The web *platform*
|
|
@@ -106,6 +106,125 @@ page_types:
|
|
|
106
106
|
- theme: navigation-correctness
|
|
107
107
|
keywords: [tap, navigate, "leaves home", "is hidden", opens, navigation]
|
|
108
108
|
|
|
109
|
+
# ---- mobile page-types -------------------------------------------------------------------
|
|
110
|
+
# All `mobile-*`, so `sensors.ts` never offers them to a web target. Two rules shaped the
|
|
111
|
+
# keywords below:
|
|
112
|
+
# 1. Detection needs 2 distinct hits AND a 1-hit lead (MIN_HITS/MIN_MARGIN), so keywords must
|
|
113
|
+
# DISCRIMINATE, not merely occur. Generic mobile words ("tap", "screen") are absent.
|
|
114
|
+
# 2. A mobile target still considers the web types, and that is correct: a mobile commerce
|
|
115
|
+
# screen SHOULD select ecommerce-list, and a mobile form leaning on generic form
|
|
116
|
+
# vocabulary (field/submit/validation) SHOULD select `form`. These types are for what a
|
|
117
|
+
# web type has no concept of, so each keyword set is built from natively-distinctive nouns.
|
|
118
|
+
# Themes carry `depth:` wherever the honest oracle is a value rather than a sighting — that is
|
|
119
|
+
# what separates a suite that scrolls and asserts a value from one that only asserts presence.
|
|
120
|
+
# Every theme is expressible in the SHIPPED mobile vocabulary (scroll/swipe/pull-to-refresh/
|
|
121
|
+
# rotate/background/restart/grant-permission/set-connectivity/deep-link), so none of them forces
|
|
122
|
+
# an @manual. OS-back themes remain absent, but the REASON has changed: the `go back` step now
|
|
123
|
+
# exists (per-OS KEYCODE_BACK / WDA navigation-back), so the blocker is no longer vocabulary. What
|
|
124
|
+
# stops it now is blast radius — a new `must_cover` theme is a new gap for every existing mobile
|
|
125
|
+
# suite, which is a scoring decision for an owner, not a drive-by. `viewpoint-ledger` already
|
|
126
|
+
# covers the dangerous half by refusing `tap [Back]` as an answer to an OS-back viewpoint item.
|
|
127
|
+
mobile-list:
|
|
128
|
+
detect_keywords: [feed, "pull to refresh", "infinite scroll", "load more", "list item", "list row"]
|
|
129
|
+
must_cover:
|
|
130
|
+
- theme: list-data
|
|
131
|
+
keywords: [list, feed, row, "list item", displayed]
|
|
132
|
+
depth:
|
|
133
|
+
requires: data-assertion
|
|
134
|
+
cross_screen: false
|
|
135
|
+
keywords: ["row contains", "item contains", "every row", "all items", "each row", "list shows"]
|
|
136
|
+
template: "User see all [List Item] text contains {{expected_label}}"
|
|
137
|
+
- theme: refresh-correctness
|
|
138
|
+
keywords: ["pull to refresh", refresh, reload, refreshed]
|
|
139
|
+
depth:
|
|
140
|
+
requires: data-assertion
|
|
141
|
+
cross_screen: false
|
|
142
|
+
keywords: ["after refresh", "refreshed list", "new item", "updated list", "after pull to refresh"]
|
|
143
|
+
template: "User pull to refresh [List] … User see all [List Item] text contains {{expected_label}}"
|
|
144
|
+
- theme: scroll-reachability
|
|
145
|
+
keywords: [scroll, swipe, "load more", "infinite scroll", "below the fold"]
|
|
146
|
+
depth:
|
|
147
|
+
requires: data-assertion
|
|
148
|
+
cross_screen: false
|
|
149
|
+
keywords: ["after scrolling", "scrolled to", "last item", "load more", "next page"]
|
|
150
|
+
template: "User scroll to [Last List Item] … User see [Last List Item] text contains {{expected_last}}"
|
|
151
|
+
|
|
152
|
+
mobile-detail:
|
|
153
|
+
detect_keywords: ["detail screen", "detail view", "detail header", "item detail", "detail pane"]
|
|
154
|
+
must_cover:
|
|
155
|
+
- theme: detail-data
|
|
156
|
+
keywords: [detail, header, displayed, field]
|
|
157
|
+
depth:
|
|
158
|
+
requires: data-assertion
|
|
159
|
+
cross_screen: false
|
|
160
|
+
keywords: ["detail shows", "detail contains", "detail header", "detail value", "detail field"]
|
|
161
|
+
template: "User see [Detail Title] text contains {{expected_title}}"
|
|
162
|
+
- theme: list-detail-consistency
|
|
163
|
+
keywords: [consistent, consistency, match, "same item", "correct item"]
|
|
164
|
+
depth:
|
|
165
|
+
requires: data-assertion
|
|
166
|
+
cross_screen: true
|
|
167
|
+
keywords: ["same item name", "matching name", "same value", "consistency", "same title"]
|
|
168
|
+
template: "User remember [List Item] text as {{v}} … User see [Detail Title] text contains {{v}}"
|
|
169
|
+
|
|
170
|
+
mobile-form:
|
|
171
|
+
detect_keywords: ["soft keyboard", "keyboard overlap", "hide keyboard", "on-screen keyboard", "keyboard covers"]
|
|
172
|
+
must_cover:
|
|
173
|
+
- theme: field-validation
|
|
174
|
+
keywords: [validation, invalid, required, error]
|
|
175
|
+
depth:
|
|
176
|
+
requires: data-assertion
|
|
177
|
+
cross_screen: false
|
|
178
|
+
keywords: ["error message", "validation message", "invalid input", "required message"]
|
|
179
|
+
template: "User see [Field Error] message contains {{error_text}}"
|
|
180
|
+
# No `depth:` on purpose: the claim IS a sighting — after the keyboard is hidden the field
|
|
181
|
+
# and its action are visible again. Forcing a value assertion here would invent an oracle.
|
|
182
|
+
- theme: keyboard-overlap
|
|
183
|
+
keywords: ["soft keyboard", "keyboard overlap", "hide keyboard", "keyboard covers", "keyboard hides"]
|
|
184
|
+
- theme: submit-correctness
|
|
185
|
+
keywords: [submit, save, submitted, saved]
|
|
186
|
+
depth:
|
|
187
|
+
requires: data-assertion
|
|
188
|
+
cross_screen: false
|
|
189
|
+
keywords: ["after submit", "saved value", "success message", "after saving"]
|
|
190
|
+
template: "User see [Success Message] text contains {{success_text}}"
|
|
191
|
+
|
|
192
|
+
mobile-auth:
|
|
193
|
+
detect_keywords: [biometric, fingerprint, "face id", "touch id", "app lock"]
|
|
194
|
+
must_cover:
|
|
195
|
+
- theme: credential-validation
|
|
196
|
+
keywords: [invalid, "wrong password", error, incorrect]
|
|
197
|
+
depth:
|
|
198
|
+
requires: data-assertion
|
|
199
|
+
cross_screen: false
|
|
200
|
+
keywords: ["error message", "invalid credential", "wrong password", "incorrect password"]
|
|
201
|
+
template: "User see [Login Error] message contains {{error_text}}"
|
|
202
|
+
- theme: session-persistence
|
|
203
|
+
keywords: [session, restart, persist, "stays signed in"]
|
|
204
|
+
depth:
|
|
205
|
+
requires: data-assertion
|
|
206
|
+
cross_screen: false
|
|
207
|
+
keywords: ["after restart", "still signed in", "session persists", "remains signed in"]
|
|
208
|
+
template: "User restart the app … User see [Account Name] text contains {{user_name}}"
|
|
209
|
+
|
|
210
|
+
mobile-settings:
|
|
211
|
+
detect_keywords: ["settings screen", toggle, preference, "settings list", "settings item"]
|
|
212
|
+
must_cover:
|
|
213
|
+
- theme: toggle-state-correctness
|
|
214
|
+
keywords: [toggle, switch, enabled, disabled]
|
|
215
|
+
depth:
|
|
216
|
+
requires: data-assertion
|
|
217
|
+
cross_screen: false
|
|
218
|
+
keywords: ["toggle state", "switch is on", "enabled state", "reflects the", "shows enabled"]
|
|
219
|
+
template: "User see [Notifications Toggle] text contains {{expected_state}}"
|
|
220
|
+
- theme: setting-persistence
|
|
221
|
+
keywords: [persist, restart, saved, "after reopening"]
|
|
222
|
+
depth:
|
|
223
|
+
requires: data-assertion
|
|
224
|
+
cross_screen: false
|
|
225
|
+
keywords: ["after restart", "setting persists", "still enabled", "saved setting", "after reopening"]
|
|
226
|
+
template: "User restart the app … User see [Notifications Toggle] text contains {{expected_state}}"
|
|
227
|
+
|
|
109
228
|
auth:
|
|
110
229
|
detect_keywords: [login, logout, password, signin, "sign in", credential]
|
|
111
230
|
must_cover:
|
|
@@ -127,3 +246,29 @@ universal:
|
|
|
127
246
|
keywords: ["empty", "no data", "no result", "failed", "error state"]
|
|
128
247
|
- theme: accessibility
|
|
129
248
|
keywords: ["keyboard", "tab order", "accessible", "aria", "focus"]
|
|
249
|
+
|
|
250
|
+
# Same role as `universal`, but only offered to MOBILE targets — `universal` applies to web too,
|
|
251
|
+
# where "rotate to landscape" or "airplane mode" is meaningless.
|
|
252
|
+
#
|
|
253
|
+
# READ THIS BEFORE ADDING TEETH HERE: like `universal`, these themes are REMINDERS. They surface
|
|
254
|
+
# through `universalGaps`, which reaches the audit narrative and the capability sensors but is NOT
|
|
255
|
+
# an input to the score. A theme placed here can never move a number.
|
|
256
|
+
#
|
|
257
|
+
# That is deliberate, not an oversight. These concerns are real on every mobile screen, but not
|
|
258
|
+
# every mobile screen owes every one of them — a settings screen has no deep link, a read-only list
|
|
259
|
+
# has no permission prompt. Scoring them uniformly would penalise honest suites for gaps that are
|
|
260
|
+
# not gaps. The themes that DO carry weight sit in each page-type's `must_cover` above, where they
|
|
261
|
+
# apply to a screen that genuinely owes them.
|
|
262
|
+
mobile_universal:
|
|
263
|
+
- theme: orientation-stability
|
|
264
|
+
keywords: [rotate, landscape, portrait, orientation]
|
|
265
|
+
- theme: connectivity-degraded
|
|
266
|
+
keywords: ["airplane mode", offline, "wifi", "mobile data", "no connection"]
|
|
267
|
+
- theme: app-lifecycle-resume
|
|
268
|
+
keywords: [background, resume, foreground, "returns to the app", restart]
|
|
269
|
+
- theme: permission-prompt
|
|
270
|
+
keywords: [permission, grant, denied, allow]
|
|
271
|
+
- theme: gesture-reachability
|
|
272
|
+
keywords: [scroll, swipe, "off screen", "below the fold"]
|
|
273
|
+
- theme: deep-link-entry
|
|
274
|
+
keywords: ["deep link", deeplink, "universal link"]
|
package/src/harness/parse.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface ScenarioInfo {
|
|
|
41
41
|
ownedByFlow?: string; // @owned-by:<flow> — the flow that owns this deferred scenario (H6)
|
|
42
42
|
/** Ordered steps with their resolved bucket (And/But inherit) — flow handoff analysis (#569). */
|
|
43
43
|
steps?: Array<{ bucket: 'given' | 'when' | 'then'; text: string }>;
|
|
44
|
+
/**
|
|
45
|
+
* The `.feature` this scenario was read from. A unit may hold several (`add --feature`), and
|
|
46
|
+
* `scenarioIdReuse` scopes its uniqueness check to ONE file — so the sensor needs to know
|
|
47
|
+
* which. Set by `loadScenarios`; absent on hand-built fixtures, which then count as one file.
|
|
48
|
+
*/
|
|
49
|
+
sourceFile?: string;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/** Format-tolerant: is this token an ID (project's scheme), not a prose word?
|
|
@@ -270,7 +276,7 @@ export function loadScenarios(featurePath: string): ScenarioInfo[] {
|
|
|
270
276
|
const feature = parser.parseFeatureFile(featurePath);
|
|
271
277
|
return (feature.scenarios || [])
|
|
272
278
|
.filter((s) => !s.stepsName && !s.hookType) // skip @steps/@hook blocks
|
|
273
|
-
.map((s) => classifyScenario(withBackground(s, feature.background)));
|
|
279
|
+
.map((s) => ({ ...classifyScenario(withBackground(s, feature.background)), sourceFile: featurePath }));
|
|
274
280
|
}
|
|
275
281
|
|
|
276
282
|
/**
|
package/src/harness/repair.ts
CHANGED
|
@@ -10,6 +10,7 @@ import * as path from 'path';
|
|
|
10
10
|
import { capabilityRegistry } from '../capabilities/registry';
|
|
11
11
|
import { discoverAndRegisterCapabilities } from '../capabilities/discover';
|
|
12
12
|
import { scoringCapabilityFor } from './audit';
|
|
13
|
+
import { readCapabilities } from './capability';
|
|
13
14
|
|
|
14
15
|
export interface RepairProposal { source: 'audit' | 'runtime'; signal: string; ruleId: string; fix: string }
|
|
15
16
|
export interface RepairPlan {
|
|
@@ -50,7 +51,13 @@ function failuresFromResult(file: string): string[] {
|
|
|
50
51
|
*/
|
|
51
52
|
export function planRepair(unitId: string, reportName: string, cwd: string, generatedDir?: string): RepairPlan {
|
|
52
53
|
discoverAndRegisterCapabilities();
|
|
53
|
-
|
|
54
|
+
// The ACTIVE PLATFORM is part of capability resolution, and leaving it out silently sent every
|
|
55
|
+
// mobile unit to the web catalog: `scoringCapabilityFor`'s third parameter is what lets `mobile`
|
|
56
|
+
// beat the default `ui`, `runAudit` passes it, and this call did not. A bare screen on a
|
|
57
|
+
// `platform: mobile` project therefore resolved to `ui` — whose repair rules are the WEB ones —
|
|
58
|
+
// so registering a mobile catalog would have changed nothing here. `cwd` is the project root
|
|
59
|
+
// (the CLI passes `process.cwd()`), which is where `qa/capabilities.yaml` lives.
|
|
60
|
+
const capId = scoringCapabilityFor(unitId, capabilityRegistry.defaultCapabilityId(), readCapabilities(cwd).platform);
|
|
54
61
|
const rules = (capId ? capabilityRegistry.get(capId)?.repair?.rules : undefined) ?? [];
|
|
55
62
|
|
|
56
63
|
const signals: { source: 'audit' | 'runtime'; text: string }[] = [];
|