Skip to content

Commit 78d9441

Browse files
committed
test(contracts): enumerate the macOS emitter's real vocabulary
Review found the table claimed to pin "the vocabulary each backend actually emits" while omitting most of it. `normalizedSnapshotType` has three output classes and only two were represented: 1. thirteen roles mapped to fixed short names — six were missing (StaticText, TextField, TextArea, MenuBarItem, Menu, MenuItem); 2. AXWindow, whose output is the SUBROLE unless it is AXStandardWindow; 3. the `default:` arm, `subrole ?? role`, emitting the raw AX-prefixed value for every unmapped role. All three are now enumerated, and the table asserts its own completeness against the emitter's fixed-output set — a role added to that switch without being added here fails, which is the emitter-drift protection the docblock was promising but not delivering. Re-measuring over the complete tables also corrected the header's own numbers. The claim was "71 of 73 agree, 2 disagree"; over 75 names it is 71 agree and FOUR disagree, because AXSystemDialog and AXUnknown were absent from the old table. Those two are the behavioral delta of this PR — an AXWindow whose subrole is emitted as the type, invisible to the six type-only spellings and named exactly by `role` — so the incomplete table had been hiding the very rows that justify reading role/subrole. The other two (AXFloatingWindow, AXSystemFloatingWindow) remain inert: only the `===` spelling misses them and its platform union is `android | ios`.
1 parent 0da567c commit 78d9441

1 file changed

Lines changed: 85 additions & 19 deletions

File tree

‎packages/contracts/src/snapshot-viewport-root.test.ts‎

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,30 @@ import { isViewportRootNode } from './snapshot-visibility.ts';
66
* into `node.type`/`role`/`subrole` — not over fixture strings.
77
*
88
* Nine hand-rolled spellings of this predicate existed before #1592's method was
9-
* applied here: three normalizing `type|role|subrole`, five lowercasing `type`
10-
* alone, and one comparing the normalized type for EQUALITY. Measured over these
11-
* tables they agreed on 71 of 73 emitted names, so collapsing them onto one
12-
* definition was a no-op everywhere except the two macOS window subroles the
13-
* type-only spellings could not see. Grounding the comparison in emitted
14-
* vocabulary is what made that provable; a fixture-string comparison would have
15-
* reported a divergence that no backend can produce (#1592, second commit).
9+
* applied here: three normalizing `type|role|subrole`, five lowercasing or
10+
* normalizing `type` alone, and one comparing the normalized type for EQUALITY.
11+
* Measured over the 75 names in the three tables below they agree on 71. All
12+
* four disagreements are macOS windows, and they split two ways:
1613
*
17-
* Keep these tables honest against their emitters. If a backend grows a type
18-
* name, add it here — a predicate that silently stops matching the root is
19-
* invisible in every other test, because the root node is scenery in all of them.
14+
* - `AXFloatingWindow` / `AXSystemFloatingWindow`: only the `===` spelling
15+
* (maestro) misses them, and its platform union is `android | ios`, so it
16+
* can never see a macOS node. Inert — the same shape #1592 found for its
17+
* own role/subrole arm.
18+
* - `AXSystemDialog` / `AXUnknown`: an `AXWindow` whose subrole is emitted AS
19+
* the type, so the six type-only spellings cannot see they are windows while
20+
* `role` names it exactly. These two are the entire behavioral delta of the
21+
* consolidation, and they are windows by role.
22+
*
23+
* Grounding this in emitted vocabulary is what made the collapse provable; a
24+
* fixture-string comparison would have reported divergences no backend can
25+
* produce (#1592, second commit).
26+
*
27+
* Keep these tables honest against their emitters — a predicate that silently
28+
* stops matching the root is invisible in every other test, because the root
29+
* node is scenery in all of them. The macOS table asserts its own completeness
30+
* against the emitter's fixed-output set below; iOS and Android do not, so a
31+
* new `elementTypeName` case or a new Android container class still has to be
32+
* added here by hand.
2033
*/
2134

2235
// apple/runner/.../RunnerTests+Snapshot.swift `elementTypeName` — a closed set.
@@ -78,22 +91,66 @@ const ANDROID_EMITTED_TYPES = [
7891
'androidx.core.widget.NestedScrollView',
7992
] as const;
8093

81-
// apple/macos-helper/.../SnapshotTraversal.swift `normalizedSnapshotType`: mapped
82-
// short names, else the raw subrole, else the raw role — so unmapped forms keep
83-
// their `AX` prefix, and only this backend populates `role`/`subrole` at all.
94+
// apple/macos-helper/.../SnapshotTraversal.swift `normalizedSnapshotType`. Three
95+
// output classes, all represented below:
96+
// 1. every one of the 13 roles the switch maps to a fixed short name;
97+
// 2. `AXWindow`, whose output is the SUBROLE unless that subrole is
98+
// `AXStandardWindow` — so a window's `type` can be any subrole string;
99+
// 3. the `default:` arm, `subrole ?? role`, which emits the raw `AX`-prefixed
100+
// value for every unmapped role.
101+
// This backend is the only one that populates `role`/`subrole` at all.
84102
const MACOS_EMITTED_NODES = [
103+
// 1. the complete fixed-output set.
85104
{ type: 'Application', role: 'AXApplication' },
86-
{ type: 'Window', role: 'AXWindow', subrole: 'AXStandardWindow' },
87-
{ type: 'AXFloatingWindow', role: 'AXWindow', subrole: 'AXFloatingWindow' },
88-
{ type: 'AXSystemDialog', role: 'AXWindow', subrole: 'AXSystemDialog' },
89-
{ type: 'AXUnknown', role: 'AXWindow', subrole: 'AXUnknown' },
90105
{ type: 'Sheet', role: 'AXSheet' },
91106
{ type: 'Dialog', role: 'AXDialog' },
92107
{ type: 'Button', role: 'AXButton' },
93-
{ type: 'Group', role: 'AXGroup' },
108+
{ type: 'StaticText', role: 'AXStaticText' },
109+
{ type: 'TextField', role: 'AXTextField' },
110+
{ type: 'TextArea', role: 'AXTextArea' },
94111
{ type: 'ScrollArea', role: 'AXScrollArea' },
95-
{ type: 'AXWebArea', role: 'AXWebArea' },
112+
{ type: 'Group', role: 'AXGroup' },
96113
{ type: 'MenuBar', role: 'AXMenuBar' },
114+
{ type: 'MenuBarItem', role: 'AXMenuBarItem' },
115+
{ type: 'Menu', role: 'AXMenu' },
116+
{ type: 'MenuItem', role: 'AXMenuItem' },
117+
// 2. AXWindow: standard subrole collapses to "Window", every other subrole
118+
// is emitted verbatim as the type.
119+
{ type: 'Window', role: 'AXWindow', subrole: 'AXStandardWindow' },
120+
{ type: 'AXFloatingWindow', role: 'AXWindow', subrole: 'AXFloatingWindow' },
121+
{ type: 'AXSystemFloatingWindow', role: 'AXWindow', subrole: 'AXSystemFloatingWindow' },
122+
{ type: 'AXSystemDialog', role: 'AXWindow', subrole: 'AXSystemDialog' },
123+
{ type: 'AXUnknown', role: 'AXWindow', subrole: 'AXUnknown' },
124+
// 3. the raw `subrole ?? role` fallback for unmapped roles.
125+
{ type: 'AXWebArea', role: 'AXWebArea' },
126+
{ type: 'AXList', role: 'AXList' },
127+
{ type: 'AXTable', role: 'AXTable' },
128+
{ type: 'AXOutline', role: 'AXOutline' },
129+
{ type: 'AXScrollBar', role: 'AXScrollBar' },
130+
{ type: 'AXSplitGroup', role: 'AXSplitGroup' },
131+
{ type: 'AXToolbar', role: 'AXToolbar' },
132+
{ type: 'AXSearchField', role: 'AXTextField', subrole: 'AXSearchField' },
133+
] as const;
134+
135+
/**
136+
* The 13 fixed outputs above are the complete set `normalizedSnapshotType`'s
137+
* switch can return, so a role added to that switch without being added here
138+
* is the drift this table exists to catch. Asserted rather than trusted.
139+
*/
140+
const MACOS_FIXED_OUTPUTS = [
141+
'Application',
142+
'Sheet',
143+
'Dialog',
144+
'Button',
145+
'StaticText',
146+
'TextField',
147+
'TextArea',
148+
'ScrollArea',
149+
'Group',
150+
'MenuBar',
151+
'MenuBarItem',
152+
'Menu',
153+
'MenuItem',
97154
] as const;
98155

99156
describe('isViewportRootNode over emitted backend vocabulary', () => {
@@ -123,11 +180,20 @@ describe('isViewportRootNode over emitted backend vocabulary', () => {
123180
'Application',
124181
'Window',
125182
'AXFloatingWindow',
183+
'AXSystemFloatingWindow',
126184
'AXSystemDialog',
127185
'AXUnknown',
128186
]);
129187
});
130188

189+
// The table's own completeness claim, asserted rather than trusted: every
190+
// fixed output the emitter's switch can return appears above, so adding a
191+
// role to that switch without adding it here fails.
192+
test('macOS: the table covers every fixed output normalizedSnapshotType returns', () => {
193+
const covered = new Set(MACOS_EMITTED_NODES.map((node) => node.type));
194+
expect(MACOS_FIXED_OUTPUTS.filter((name) => !covered.has(name))).toEqual([]);
195+
});
196+
131197
// Why the canonical predicate reads role/subrole and not `type` alone: these
132198
// two are windows that no type-only spelling could see.
133199
test('macOS: role rescues the two window subroles `type` alone cannot name', () => {

0 commit comments

Comments
 (0)