Skip to content

Commit fcb1812

Browse files
committed
feat: add absolute source paths and file links
1 parent f54a23e commit fcb1812

19 files changed

Lines changed: 1538 additions & 129 deletions

File tree

client/src/app/AppShell.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ import { isMoveControlMessage } from "./controlMessages";
114114
const ACCESSIBILITY_REFRESH_MS = 1500;
115115
const REACT_NATIVE_ACCESSIBILITY_REFRESH_MS = 500;
116116
const FLUTTER_ACCESSIBILITY_REFRESH_MS = 1000;
117+
const ACCESSIBILITY_BACKGROUND_REFRESH_MS = 3000;
117118
const ANDROID_METADATA_REFRESH_MS = 1000;
118119
const DEFAULT_ACCESSIBILITY_MAX_DEPTH = 10;
119120
const LOGICAL_INSPECTOR_MAX_DEPTH = 80;
@@ -1429,18 +1430,16 @@ export function AppShell({
14291430
);
14301431

14311432
useEffect(() => {
1432-
if (!hierarchyVisible) {
1433-
return;
1434-
}
1435-
14361433
const refreshMs =
1437-
accessibilityPreferredSource === "react-native" ||
1438-
accessibilitySource === "react-native"
1439-
? REACT_NATIVE_ACCESSIBILITY_REFRESH_MS
1440-
: accessibilityPreferredSource === "flutter" ||
1441-
accessibilitySource === "flutter"
1442-
? FLUTTER_ACCESSIBILITY_REFRESH_MS
1443-
: ACCESSIBILITY_REFRESH_MS;
1434+
hierarchyVisible
1435+
? accessibilityPreferredSource === "react-native" ||
1436+
accessibilitySource === "react-native"
1437+
? REACT_NATIVE_ACCESSIBILITY_REFRESH_MS
1438+
: accessibilityPreferredSource === "flutter" ||
1439+
accessibilitySource === "flutter"
1440+
? FLUTTER_ACCESSIBILITY_REFRESH_MS
1441+
: ACCESSIBILITY_REFRESH_MS
1442+
: ACCESSIBILITY_BACKGROUND_REFRESH_MS;
14441443
let disposed = false;
14451444
let timeout: number | null = null;
14461445
const refreshLoop = async () => {

client/src/app/uiState.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,36 @@ describe("uiState", () => {
118118
).toBe(false);
119119
});
120120

121+
it("retains the current NativeScript tree during empty or fallback refreshes", () => {
122+
expect(
123+
shouldRetainAccessibilityTreeDuringRefresh(
124+
"auto",
125+
"nativescript",
126+
"nativescript",
127+
0,
128+
3,
129+
),
130+
).toBe(true);
131+
expect(
132+
shouldRetainAccessibilityTreeDuringRefresh(
133+
"nativescript",
134+
"nativescript",
135+
"native-ax",
136+
12,
137+
3,
138+
),
139+
).toBe(true);
140+
expect(
141+
shouldRetainAccessibilityTreeDuringRefresh(
142+
"auto",
143+
"nativescript",
144+
"nativescript",
145+
4,
146+
3,
147+
),
148+
).toBe(false);
149+
});
150+
121151
it("sanitizes persisted viewport state and falls back to defaults", () => {
122152
const sanitized = sanitizePersistedUiState({
123153
bundleIDValue: 123 as unknown as string,

client/src/app/uiState.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,10 @@ export function shouldRetainAccessibilityTreeDuringRefresh(
173173
}
174174
const retainedSource =
175175
currentPreference === "auto" ? currentSource : currentPreference;
176-
if (retainedSource !== "flutter" && retainedSource !== "react-native") {
176+
if (
177+
!isAccessibilitySource(retainedSource) ||
178+
!retainableRichAccessibilitySources.has(retainedSource)
179+
) {
177180
return false;
178181
}
179182
if (currentSource !== retainedSource) {
@@ -182,6 +185,13 @@ export function shouldRetainAccessibilityTreeDuringRefresh(
182185
return snapshotSource !== retainedSource || nextRootCount === 0;
183186
}
184187

188+
const retainableRichAccessibilitySources = new Set<AccessibilitySource>([
189+
"nativescript",
190+
"react-native",
191+
"flutter",
192+
"swiftui",
193+
]);
194+
185195
export function isAccessibilitySource(
186196
value: unknown,
187197
): value is AccessibilitySource {

client/src/features/accessibility/AccessibilityInspector.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useRef, useState } from "react";
2-
import type { CSSProperties, FormEvent } from "react";
2+
import type { CSSProperties, FormEvent, ReactNode } from "react";
33

44
import { sendInspectorRequest } from "../../api/simulators";
55
import type {
@@ -42,6 +42,7 @@ interface AccessibilityInspectorProps {
4242
}
4343

4444
type InspectorTab = "console" | "inspector" | "performance";
45+
type DetailValue = ReactNode;
4546

4647
export function AccessibilityInspector({
4748
availableSources,
@@ -317,6 +318,7 @@ export function AccessibilityInspector({
317318
const kind = accessibilityKind(item.node);
318319
const label = hierarchyNodeLabel(item.node, kind);
319320
const sourceBadge = sourceLocationBadgeText(item.node);
321+
const sourceHref = sourceLocationHref(item.node);
320322
const sourceTitle = sourceLocationText(item.node);
321323
const chainBadge = compactedChainBadgeText(item.chain.length);
322324
const chainPath = compactedChainPathText(item);
@@ -377,15 +379,25 @@ export function AccessibilityInspector({
377379
{label ? (
378380
<span className="hierarchy-node-text">{label}</span>
379381
) : null}
380-
{sourceBadge ? (
382+
</button>
383+
{sourceBadge ? (
384+
sourceHref ? (
385+
<a
386+
className="hierarchy-node-source"
387+
href={sourceHref}
388+
title={sourceTitle}
389+
>
390+
{sourceBadge}
391+
</a>
392+
) : (
381393
<span
382394
className="hierarchy-node-source"
383395
title={sourceTitle}
384396
>
385397
{sourceBadge}
386398
</span>
387-
) : null}
388-
</button>
399+
)
400+
) : null}
389401
</div>
390402
);
391403
})
@@ -500,10 +512,21 @@ function NodeDetails({
500512
selectedSimulator: SimulatorMetadata | null;
501513
}) {
502514
const isAndroid = isAndroidSimulator(selectedSimulator);
503-
const details = [
515+
const sourceText = sourceLocationText(node);
516+
const sourceHref = sourceLocationHref(node);
517+
const details = ([
504518
["Type", accessibilityKind(node)],
505519
["Label", primaryAccessibilityText(node)],
506-
["Source", sourceLocationText(node)],
520+
[
521+
"Source",
522+
sourceHref ? (
523+
<a className="hierarchy-detail-link" href={sourceHref}>
524+
{sourceText}
525+
</a>
526+
) : (
527+
sourceText
528+
),
529+
],
507530
[
508531
isAndroid ? "Resource ID" : "Identifier",
509532
isAndroid
@@ -539,7 +562,7 @@ function NodeDetails({
539562
["PID", node.pid == null ? "" : String(node.pid)],
540563
["Actions", node.custom_actions?.join(", ") ?? ""],
541564
["Help", node.help ?? ""],
542-
].filter(([, value]) => value);
565+
] as Array<[string, DetailValue]>).filter(([, value]) => value);
543566

544567
return (
545568
<div className="hierarchy-details">
@@ -673,6 +696,24 @@ function sourceLocationText(node: AccessibilityNode): string {
673696
return `${location.file}:${line}:${column}`;
674697
}
675698

699+
function sourceLocationHref(node: AccessibilityNode): string {
700+
const location = primarySourceLocation(node);
701+
return location?.file ? fileUrl(location.file) : "";
702+
}
703+
704+
function fileUrl(file: string): string {
705+
if (file.startsWith("file://")) {
706+
return file;
707+
}
708+
if (!file.startsWith("/")) {
709+
return "";
710+
}
711+
return `file://${file
712+
.split("/")
713+
.map((part, index) => (index === 0 ? "" : encodeURIComponent(part)))
714+
.join("/")}`;
715+
}
716+
676717
function sourceLocationBadgeText(node: AccessibilityNode): string {
677718
const location = primarySourceLocation(node);
678719
if (!location?.file) {
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from "vitest";
2+
3+
import { accessibilityDomTagName } from "./AccessibilityOverlay";
4+
5+
describe("accessibilityDomTagName", () => {
6+
it("uses source and component names for annotator-friendly custom tags", () => {
7+
expect(
8+
accessibilityDomTagName({
9+
source: "nativescript",
10+
type: "TabItem",
11+
}),
12+
).toBe("simdeck-tab-item");
13+
expect(
14+
accessibilityDomTagName({
15+
source: "nativescript",
16+
type: "Label",
17+
}),
18+
).toBe("simdeck-label");
19+
expect(
20+
accessibilityDomTagName({
21+
source: "react-native",
22+
type: "RangeAndFilterBar",
23+
}),
24+
).toBe("simdeck-range-and-filter-bar");
25+
});
26+
});

0 commit comments

Comments
 (0)