Skip to content

Commit b3c2e10

Browse files
committed
fix: preserve published inspector sources
1 parent 9bc9604 commit b3c2e10

7 files changed

Lines changed: 299 additions & 25 deletions

File tree

client/src/app/AppShell.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1330,6 +1330,7 @@ export function AppShell({
13301330
accessibilityPreferredSource,
13311331
accessibilitySource,
13321332
snapshot.source,
1333+
availableSources,
13331334
roots.length,
13341335
accessibilityRootsRef.current.length,
13351336
);
@@ -1371,6 +1372,7 @@ export function AppShell({
13711372
accessibilityPreferredSource,
13721373
accessibilitySource,
13731374
"native-ax",
1375+
accessibilityAvailableSources,
13741376
0,
13751377
accessibilityRootsRef.current.length,
13761378
);

client/src/app/uiState.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ describe("uiState", () => {
7676
"auto",
7777
"flutter",
7878
"flutter",
79+
["flutter", "native-ax"],
7980
0,
8081
3,
8182
),
@@ -85,6 +86,7 @@ describe("uiState", () => {
8586
"flutter",
8687
"flutter",
8788
"native-ax",
89+
["flutter", "native-ax"],
8890
12,
8991
3,
9092
),
@@ -94,6 +96,7 @@ describe("uiState", () => {
9496
"auto",
9597
"flutter",
9698
"flutter",
99+
["flutter", "native-ax"],
97100
4,
98101
3,
99102
),
@@ -103,6 +106,7 @@ describe("uiState", () => {
103106
"native-ax",
104107
"native-ax",
105108
"flutter",
109+
["flutter", "native-ax"],
106110
4,
107111
3,
108112
),
@@ -112,6 +116,7 @@ describe("uiState", () => {
112116
"flutter",
113117
"native-ax",
114118
"native-ax",
119+
["native-ax"],
115120
4,
116121
3,
117122
),
@@ -124,6 +129,7 @@ describe("uiState", () => {
124129
"auto",
125130
"nativescript",
126131
"nativescript",
132+
["nativescript", "native-ax"],
127133
0,
128134
3,
129135
),
@@ -133,6 +139,7 @@ describe("uiState", () => {
133139
"nativescript",
134140
"nativescript",
135141
"native-ax",
142+
["nativescript", "native-ax"],
136143
12,
137144
3,
138145
),
@@ -142,6 +149,20 @@ describe("uiState", () => {
142149
"auto",
143150
"nativescript",
144151
"nativescript",
152+
["nativescript", "native-ax"],
153+
4,
154+
3,
155+
),
156+
).toBe(false);
157+
});
158+
159+
it("does not retain a rich tree after that source disappears", () => {
160+
expect(
161+
shouldRetainAccessibilityTreeDuringRefresh(
162+
"react-native",
163+
"react-native",
164+
"nativescript",
165+
["nativescript", "native-ax"],
145166
4,
146167
3,
147168
),

client/src/app/uiState.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ export function shouldRetainAccessibilityTreeDuringRefresh(
165165
currentPreference: AccessibilitySourcePreference,
166166
currentSource: AccessibilitySource | "",
167167
snapshotSource: AccessibilitySource,
168+
availableSources: AccessibilitySource[],
168169
nextRootCount: number,
169170
currentRootCount: number,
170171
): boolean {
@@ -179,6 +180,9 @@ export function shouldRetainAccessibilityTreeDuringRefresh(
179180
) {
180181
return false;
181182
}
183+
if (!availableSources.includes(retainedSource)) {
184+
return false;
185+
}
182186
if (currentSource !== retainedSource) {
183187
return false;
184188
}

client/src/features/accessibility/AccessibilityOverlay.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,52 @@ describe("AccessibilityOverlay", () => {
5555
expect(markup).toContain('aria-label="SimDeck accessibility element');
5656
expect(markup).not.toContain(" title=");
5757
});
58+
59+
it("marks overlay nodes as app representations without bare disabled wording", () => {
60+
const markup = renderToStaticMarkup(
61+
createElement(AccessibilityOverlay, {
62+
hoveredId: null,
63+
roots: [
64+
{
65+
frame: { height: 844, width: 390, x: 0, y: 0 },
66+
role: "application",
67+
children: [
68+
{
69+
AXLabel: "Upgrade",
70+
enabled: false,
71+
frame: { height: 48, width: 180, x: 105, y: 720 },
72+
nativeScript: {
73+
testID: "upgrade-button",
74+
type: "Button",
75+
},
76+
source: "nativescript",
77+
sourceLocation: {
78+
file: "/Users/dj/Developer/app/src/app.component.ts",
79+
line: 12,
80+
column: 8,
81+
},
82+
type: "Button",
83+
},
84+
],
85+
},
86+
],
87+
selectedId: "",
88+
}),
89+
);
90+
91+
expect(markup).toContain(
92+
'data-simdeck-overlay-node="accessibility-representation"',
93+
);
94+
expect(markup).toContain(
95+
"SimDeck overlay node representing a simulator app element",
96+
);
97+
expect(markup).toContain('data-test-id="upgrade-button"');
98+
expect(markup).toContain(
99+
'data-simdeck-accessibility-source-location="/Users/dj/Developer/app/src/app.component.ts:12:8"',
100+
);
101+
expect(markup).toContain("simulator accessibility state enabled=false");
102+
expect(markup).not.toContain(">disabled<");
103+
expect(markup).not.toContain("; disabled");
104+
expect(markup).not.toContain(" title=");
105+
});
58106
});

client/src/features/accessibility/AccessibilityOverlay.tsx

Lines changed: 95 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -146,30 +146,42 @@ function AccessibilityDomNode({
146146
const kind = accessibilityKind(node);
147147
const role = accessibilityDomRole(kind);
148148
const tagName = accessibilityDomTagName(node);
149+
const description = accessibilityDomDescription(node, metadata);
149150

150151
return createElement(tagName, {
151152
"aria-checked":
152153
role === "checkbox" || role === "switch"
153154
? (node.checked ?? undefined)
154155
: undefined,
155156
"aria-label": label,
157+
"aria-description": description,
156158
"aria-level": depth + 1,
157159
"aria-selected": node.selected ?? undefined,
158160
className: "accessibility-dom-node",
161+
"data-test-id": metadata.testId,
159162
"data-testid": `simdeck-accessibility-${id}`,
160163
"data-simdeck-accessibility-id": id,
161164
"data-simdeck-accessibility-component": kind,
165+
"data-simdeck-accessibility-description": description,
162166
"data-simdeck-accessibility-identifier":
163167
accessibilityIdentifier(node) || undefined,
164168
"data-simdeck-accessibility-kind": kind,
165169
"data-simdeck-accessibility-label": primaryAccessibilityText(node),
166170
"data-simdeck-accessibility-image": metadata.imageName,
171+
"data-simdeck-accessibility-source-location": metadata.sourceLocation,
167172
"data-simdeck-accessibility-source-file": metadata.sourceFile,
168173
"data-simdeck-accessibility-source-line": metadata.sourceLine,
169174
"data-simdeck-accessibility-source-column": metadata.sourceColumn,
170175
"data-simdeck-accessibility-source": node.source || undefined,
176+
"data-simdeck-accessibility-test-id": metadata.testId,
177+
"data-simdeck-accessibility-native-id": metadata.nativeId,
178+
"data-simdeck-accessibility-role": role,
171179
"data-simdeck-accessibility-state": metadata.state,
172180
"data-simdeck-accessibility-value": metadata.value,
181+
"data-simdeck-framework": metadata.framework,
182+
"data-simdeck-framework-id": metadata.frameworkId,
183+
"data-simdeck-framework-type": metadata.frameworkType,
184+
"data-simdeck-overlay-node": "accessibility-representation",
173185
"data-simdeck-inspector-id": node.inspectorId || undefined,
174186
"data-simdeck-uikit-id": node.uikitId || undefined,
175187
role,
@@ -222,6 +234,23 @@ function accessibilityDomLabel(node: AccessibilityNode): string {
222234
return parts.join("; ");
223235
}
224236

237+
function accessibilityDomDescription(
238+
node: AccessibilityNode,
239+
metadata: ReturnType<typeof accessibilityDomMetadata>,
240+
): string | undefined {
241+
const parts = [
242+
"SimDeck overlay node representing a simulator app element, not the browser page control",
243+
metadata.framework ? `framework ${metadata.framework}` : "",
244+
metadata.frameworkType ? `component ${metadata.frameworkType}` : "",
245+
metadata.testId ? `test id ${metadata.testId}` : "",
246+
metadata.nativeId ? `native id ${metadata.nativeId}` : "",
247+
metadata.sourceLocation ? `source ${metadata.sourceLocation}` : "",
248+
metadata.state ? `state ${metadata.state}` : "",
249+
node.help ? `hint ${node.help}` : "",
250+
].filter(Boolean);
251+
return parts.length > 0 ? parts.join("; ") : undefined;
252+
}
253+
225254
function accessibilityDomRole(kind: string): AriaRole {
226255
const normalized = kind.toLowerCase();
227256
if (normalized.includes("button")) {
@@ -275,8 +304,13 @@ function cleanTagPart(value: string | null | undefined): string | null {
275304

276305
function accessibilityDomMetadata(node: AccessibilityNode, id?: string) {
277306
const sourceLocation = primarySourceLocation(node);
307+
const framework = frameworkMetadata(node);
278308
return {
309+
framework: framework.framework,
310+
frameworkId: framework.id,
311+
frameworkType: framework.type,
279312
imageName: cleanAccessibilityText(node.imageName),
313+
nativeId: framework.nativeId,
280314
placeholder: cleanAccessibilityText(node.placeholder),
281315
sourceFile: sourceLocation.file || undefined,
282316
sourceColumn:
@@ -289,10 +323,60 @@ function accessibilityDomMetadata(node: AccessibilityNode, id?: string) {
289323
: undefined,
290324
sourceLocation: formatSourceLocation(sourceLocation),
291325
state: accessibilityStateSummary(node, id),
326+
testId: framework.testId,
292327
value: cleanAccessibilityText(node.AXValue),
293328
};
294329
}
295330

331+
function frameworkMetadata(node: AccessibilityNode): {
332+
framework: string | undefined;
333+
id: string | undefined;
334+
nativeId: string | undefined;
335+
testId: string | undefined;
336+
type: string | undefined;
337+
} {
338+
const nativeScript = node.nativeScript ?? {};
339+
const reactNative = node.reactNative ?? {};
340+
const flutter = node.flutter ?? {};
341+
const semantics = node.semantics ?? {};
342+
return {
343+
framework: node.source || undefined,
344+
id:
345+
stringRecordValue(nativeScript, "id") ??
346+
stringRecordValue(reactNative, "tag") ??
347+
stringRecordValue(flutter, "key") ??
348+
undefined,
349+
nativeId:
350+
stringRecordValue(reactNative, "nativeID") ??
351+
stringRecordValue(nativeScript, "id") ??
352+
stringRecordValue(semantics, "identifier") ??
353+
undefined,
354+
testId:
355+
stringRecordValue(nativeScript, "testID") ??
356+
stringRecordValue(reactNative, "testID") ??
357+
undefined,
358+
type:
359+
stringRecordValue(nativeScript, "type") ??
360+
stringRecordValue(reactNative, "displayName") ??
361+
stringRecordValue(flutter, "widgetType") ??
362+
undefined,
363+
};
364+
}
365+
366+
function stringRecordValue(
367+
record: Record<string, unknown>,
368+
key: string,
369+
): string | undefined {
370+
const value = record[key];
371+
if (typeof value === "string") {
372+
return value.trim() || undefined;
373+
}
374+
if (typeof value === "number" || typeof value === "boolean") {
375+
return String(value);
376+
}
377+
return undefined;
378+
}
379+
296380
function primarySourceLocation(node: AccessibilityNode): {
297381
column: number | null;
298382
file: string;
@@ -342,13 +426,17 @@ function accessibilityStateSummary(
342426
): string {
343427
const state = [
344428
id ? `tree id ${id}` : "",
345-
node.enabled === false ? "disabled" : "",
346-
node.focused === true ? "focused" : "",
347-
node.selected === true ? "selected" : "",
348-
node.checked === true ? "checked" : "",
349-
node.checked === false ? "unchecked" : "",
350-
node.clickable === true ? "clickable" : "",
351-
node.scrollable === true ? "scrollable" : "",
429+
node.enabled === false ? "simulator accessibility state enabled=false" : "",
430+
node.focused === true ? "simulator accessibility state focused=true" : "",
431+
node.selected === true ? "simulator accessibility state selected=true" : "",
432+
node.checked === true ? "simulator accessibility state checked=true" : "",
433+
node.checked === false ? "simulator accessibility state checked=false" : "",
434+
node.clickable === true
435+
? "simulator accessibility action clickable=true"
436+
: "",
437+
node.scrollable === true
438+
? "simulator accessibility action scrollable=true"
439+
: "",
352440
].filter(Boolean);
353441
return state.join(", ");
354442
}

0 commit comments

Comments
 (0)