diff --git a/CONTEXT.md b/CONTEXT.md index 6956b48169..fd586484ad 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -76,6 +76,7 @@ - Settled observation: opt-in (`--settle`) post-action payload on press/click/fill/longpress — the quiet-window stable loop re-captures until the UI settles, and the response carries the diff vs the pre-action tree (changed lines only, added lines with fresh refs, `refsGeneration` when the settled tree was stored). Best-effort: never fails the action; `settled: false` plus a hint on never-quiet content. - Snapshot capture plan: per-strategy ordered chain of iOS snapshot capture backends (recursive tree, query sweep, private AX) run by one plan runner under a shared wall-clock budget; recovery ordering is declared data, never a per-call-site branch. - Snapshot quality verdict: structured outcome (state, backend, reason code, effective depth, collapsed leaves) computed once by the plan runner and shipped with every planned snapshot payload; the daemon and CLI render it instead of re-deriving degradation from node shapes. +- iOS WebView semantic presentation: the interactive snapshot projection that recognizes XCTest's typed `WebView` root and WebKit's `Other -> StaticText` wrapper pairs. It keeps raw diagnostics unchanged, presents ordinary wrapper text as `StaticText`, and presents wrappers carrying WebKit's numeric HTML heading level as `Heading`. - AX-unavailable target invalidation: iOS/macOS runner behavior where a root accessibility snapshot failure such as `kAXErrorIllegalArgument` marks the cached `XCUIApplication` target handle suspect. The runner fails closed for degraded interactive snapshots, clears the cached target, and lets the next command reacquire the app through normal activation. - Resolution disclosure (ADR 0012 decision 2): additive `resolution` field on press/click/fill/longpress responses discloses how the acting path resolved its target — `runtime`/`unique` or `runtime`/`disambiguated` (with `matchCount`/`winnerDiagnostic`/`tiebreak`/up-to-5 `alternatives`) on the daemon tree, `ref`/`exact` for a resolved `@ref` (runtime-ref and native-ref), `ref`/`label-fallback` when runtime-ref recovered a stale `@ref` via its recorded trailing label, or `direct-ios`/`not-observed` on the XCTest fast path; absent entirely on the coordinate path and on dispatches whose runner actually executed the maestro non-hittable coordinate fallback (permission alone keeps the direct path's `not-observed`). Pre-action diagnostics only: `winnerDiagnostic`/`alternatives` entries carry an opaque, non-`@` `diagnosticRef` that is never ref-issued, never MCP-pinned, and cannot be reused as an `@ref` target — a fresh snapshot/find is required before acting on an alternative. diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift index 25c70805cc..efac1b7a19 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift @@ -57,12 +57,16 @@ extension RunnerTests { case .collectionView: return "CollectionView" case .table: return "Table" case .scrollView: return "ScrollView" + case .toolbar: return "Toolbar" case .searchField: return "SearchField" case .segmentedControl: return "SegmentedControl" case .stepper: return "Stepper" case .picker: return "Picker" + case .activityIndicator: return "ActivityIndicator" + case .progressIndicator: return "ProgressIndicator" case .checkBox: return "CheckBox" case .menuItem: return "MenuItem" + case .webView: return "WebView" case .other: return "Other" default: switch type.rawValue { @@ -70,8 +74,6 @@ extension RunnerTests { return "Keyboard" case 20: return "Key" - case 24: - return "SearchField" default: return "Element(\(type.rawValue))" } diff --git a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift index 0fbd6e63d1..5a60f0efb3 100644 --- a/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +++ b/apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift @@ -131,7 +131,8 @@ final class RunnerTests: XCTestCase { .tabBar, .textField, .secureTextField, - .textView + .textView, + .webView ] // Keep blocker actions narrow to avoid false positives from generic hittable containers. let actionableTypes: Set = [ diff --git a/examples/test-app/README.md b/examples/test-app/README.md index bea995d094..b1d7623432 100644 --- a/examples/test-app/README.md +++ b/examples/test-app/README.md @@ -17,9 +17,17 @@ It is intentionally small, but each surface is dense with durable accessibility - `Product detail`: back navigation, quantity stepper, multiline notes, save action - `Checkout form`: required-field validation, fill vs type, checkbox state, choice groups, keyboard dismiss, success summary - `Settings`: switch rows, accordion content, loading and error states, retry flow, destructive-confirm modal +- `WebView accessibility`: a deterministic semantic fixture plus live websites with varied HTML for native accessibility snapshot verification Navigation uses Expo Router native bottom tabs, so the tab bar itself is also part of the test surface. +The deterministic WebView fixture is the stable accessibility oracle. On iOS, an interactive +snapshot should expose its root as `webview`, both titles as `heading`, paragraph and label content +as `text`, and the form controls as `text-field`, `switch`, and `button`. The live-site buttons are +exploratory smoke coverage for real-world WebKit trees, not stable assertion targets. +Use an unscoped snapshot for this oracle: XCTest can detach a scoped WebKit document subtree from +its `WebView` ancestor, leaving insufficient evidence for safe semantic projection. + ## Coverage map These are the main case families this app can support without adding more screens: diff --git a/examples/test-app/app/(tabs)/settings.tsx b/examples/test-app/app/(tabs)/settings.tsx index 5d2112ac08..6047017e7b 100644 --- a/examples/test-app/app/(tabs)/settings.tsx +++ b/examples/test-app/app/(tabs)/settings.tsx @@ -17,6 +17,7 @@ export default function SettingsRoute() { notificationsEnabled={state.notificationsEnabled} onOpenAccessorySetup={() => router.push('/accessory-setup')} onOpenInertSurface={() => router.push('/inert')} + onOpenWebViewLab={() => router.push('/webview')} onConfirmReset={state.resetLabState} onLoadDiagnostics={state.loadDiagnostics} onRetryDiagnostics={state.retryDiagnostics} diff --git a/examples/test-app/app/_layout.tsx b/examples/test-app/app/_layout.tsx index 7f6b88f7cf..cfd43e091d 100644 --- a/examples/test-app/app/_layout.tsx +++ b/examples/test-app/app/_layout.tsx @@ -26,6 +26,7 @@ function RootLayoutContent() { + {toastMessage ? : null} diff --git a/examples/test-app/app/webview.tsx b/examples/test-app/app/webview.tsx new file mode 100644 index 0000000000..8ca901a9a3 --- /dev/null +++ b/examples/test-app/app/webview.tsx @@ -0,0 +1,9 @@ +import { useRouter } from 'expo-router'; + +import { WebViewScreen } from '../src/screens/WebViewScreen'; + +export default function WebViewRoute() { + const router = useRouter(); + + return router.back()} />; +} diff --git a/examples/test-app/package.json b/examples/test-app/package.json index aa88c59002..3fa736d950 100644 --- a/examples/test-app/package.json +++ b/examples/test-app/package.json @@ -26,7 +26,8 @@ "react-native-gesture-handler": "^2.31.2", "react-native-safe-area-context": "~5.7.0", "react-native-screens": "~4.25.2", - "react-native-web": "^0.21.2" + "react-native-web": "^0.21.2", + "react-native-webview": "13.16.1" }, "devDependencies": { "@expo/fingerprint": "^0.19.4", diff --git a/examples/test-app/pnpm-lock.yaml b/examples/test-app/pnpm-lock.yaml index aa8751ca3b..4bb1170b2e 100644 --- a/examples/test-app/pnpm-lock.yaml +++ b/examples/test-app/pnpm-lock.yaml @@ -27,7 +27,7 @@ importers: version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) expo: specifier: ~56.0.12 - version: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + version: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-audio: specifier: ~56.0.12 version: 56.0.12(expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) @@ -70,6 +70,9 @@ importers: react-native-web: specifier: ^0.21.2 version: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-native-webview: + specifier: 13.16.1 + version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) devDependencies: '@expo/fingerprint': specifier: ^0.19.4 @@ -2460,6 +2463,12 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 + react-native-webview@13.16.1: + resolution: {integrity: sha512-If0eHhoEdOYDcHsX+xBFwHMbWBGK1BvGDQDQdVkwtSIXiq1uiqjkpWVP2uQ1as94J0CzvFE9PUNDuhiX0Z6ubw==} + peerDependencies: + react: '*' + react-native: '*' + react-native-worklets@0.10.0: resolution: {integrity: sha512-JhE6IxDf6iabC0qu3+TAKA4v9RlluXmoIngPQX7/QUByf75lfrsHZ6/dQhyjEWnp1EEQiwzz8Cpew140ZcewDw==} peerDependencies: @@ -3565,7 +3574,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.4 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-server: 56.0.5 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -3663,7 +3672,7 @@ snapshots: '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) @@ -3730,7 +3739,7 @@ snapshots: dependencies: '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) anser: 1.4.10 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) stacktrace-parser: 0.1.11 @@ -3761,7 +3770,7 @@ snapshots: postcss: 8.5.12 resolve-from: 5.0.0 optionalDependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - bufferutil - supports-color @@ -3783,7 +3792,7 @@ snapshots: dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) anser: 1.4.10 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) pretty-format: 29.7.0 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) @@ -3866,7 +3875,7 @@ snapshots: '@expo/router-server@56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: debug: 4.4.3 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)) expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) expo-server: 56.0.5 @@ -3890,7 +3899,7 @@ snapshots: '@expo/ui@56.0.18(@babel/core@7.29.7)(@types/react@19.2.14)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native-reanimated@4.5.0(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)': dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) sf-symbols-typescript: 2.2.0 @@ -4155,7 +4164,7 @@ snapshots: '@react-native/babel-plugin-codegen@0.86.0(@babel/core@7.29.7)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.7 '@react-native/codegen': 0.86.0(@babel/core@7.29.7) transitivePeerDependencies: - '@babel/core' @@ -4212,7 +4221,7 @@ snapshots: '@react-native/codegen@0.86.0(@babel/core@7.29.7)': dependencies: '@babel/core': 7.29.7 - '@babel/parser': 7.29.2 + '@babel/parser': 7.29.7 hermes-parser: 0.36.0 invariant: 2.2.4 nullthrows: 1.1.1 @@ -4287,7 +4296,9 @@ snapshots: metro-runtime: 0.84.4 transitivePeerDependencies: - '@babel/core' + - bufferutil - supports-color + - utf-8-validate '@react-native/normalize-colors@0.74.89': {} @@ -4306,7 +4317,7 @@ snapshots: '@testing-library/dom@10.4.1': dependencies: - '@babel/code-frame': 7.29.0 + '@babel/code-frame': 7.29.7 '@babel/runtime': 7.29.2 '@types/aria-query': 5.0.4 aria-query: 5.3.0 @@ -4509,7 +4520,7 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.2 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -4775,7 +4786,7 @@ snapshots: expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) @@ -4785,7 +4796,7 @@ snapshots: expo-audio@56.0.12(expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) @@ -4803,14 +4814,14 @@ snapshots: expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)): dependencies: '@expo/env': 2.3.0 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) transitivePeerDependencies: - supports-color expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)) expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)) expo-dev-menu-interface: 56.0.1(expo@56.0.12) @@ -4822,36 +4833,36 @@ snapshots: expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)) expo-manifests: 56.0.4(expo@56.0.12) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) expo-dev-menu-interface@56.0.1(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-dev-menu-interface: 56.0.1(expo@56.0.12) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) fontfaceobserver: 2.3.0 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) @@ -4859,7 +4870,7 @@ snapshots: expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): @@ -4874,7 +4885,7 @@ snapshots: expo-manifests@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-json-utils: 56.0.0 expo-modules-autolinking@56.0.16(typescript@6.0.3): @@ -4916,7 +4927,7 @@ snapshots: color: 4.2.3 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3)) expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) @@ -4956,14 +4967,14 @@ snapshots: expo-status-bar@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.29 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) @@ -4971,9 +4982,9 @@ snapshots: expo-updates-interface@56.0.2(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo@56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo@56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-router@56.2.11)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): dependencies: '@babel/runtime': 7.29.2 '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) @@ -5004,6 +5015,7 @@ snapshots: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -5773,6 +5785,13 @@ snapshots: transitivePeerDependencies: - encoding + react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): + dependencies: + escape-string-regexp: 4.0.0 + invariant: 2.2.4 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3) + react-native-worklets@0.10.0(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.0(@babel/core@7.29.7))(@types/react@19.2.14)(react@19.2.3))(react@19.2.3): dependencies: '@babel/core': 7.29.7 @@ -5785,7 +5804,7 @@ snapshots: '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.7) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.7) - '@babel/types': 7.29.0 + '@babel/types': 7.29.7 '@react-native/metro-config': 0.86.0(@babel/core@7.29.7) convert-source-map: 2.0.0 react: 19.2.3 diff --git a/examples/test-app/src/screens/SettingsScreen.tsx b/examples/test-app/src/screens/SettingsScreen.tsx index f9d4ba10ec..87055c81c6 100644 --- a/examples/test-app/src/screens/SettingsScreen.tsx +++ b/examples/test-app/src/screens/SettingsScreen.tsx @@ -19,6 +19,7 @@ export interface SettingsScreenProps { reducedMotionEnabled: boolean; onOpenAccessorySetup: () => void; onOpenInertSurface: () => void; + onOpenWebViewLab: () => void; onLoadDiagnostics: () => void; onRetryDiagnostics: () => void; onSetNotificationsEnabled: (value: boolean) => void; @@ -83,6 +84,17 @@ export function SettingsScreen(props: SettingsScreenProps) { /> + + + + void }) { + const colors = useAppColors(); + const insets = useSafeAreaInsets(); + const styles = createStyles(colors); + const [site, setSite] = useState(SITES[0]); + const [loadState, setLoadState] = useState({ kind: 'loading' }); + + function selectSite(nextSite: Site) { + if (nextSite.key === site.key) return; + setSite(nextSite); + setLoadState({ kind: 'loading' }); + } + + return ( + + + [styles.closeButton, pressed ? styles.pressed : null]} + testID="close-webview-lab" + > + Close + + + WebView accessibility + + Compare semantic roles exposed by real websites through the native accessibility tree. + + + + + + {SITES.map((candidate) => { + const selected = candidate.key === site.key; + return ( + selectSite(candidate)} + style={({ pressed }) => [ + styles.siteButton, + selected ? styles.siteButtonSelected : null, + pressed ? styles.pressed : null, + ]} + testID={`webview-site-${candidate.key}`} + > + + {candidate.label} + + + ); + })} + + + + + {formatLoadStatus(site.label, loadState)} + + {loadState.kind === 'loading' ? ( + + ) : null} + + + + { + setLoadState({ kind: 'error', message: event.nativeEvent.description }); + }} + onHttpError={(event) => { + const { description, statusCode } = event.nativeEvent; + setLoadState({ + kind: 'error', + message: description || `HTTP ${statusCode}`, + }); + }} + onLoad={() => + setLoadState((current) => (current.kind === 'error' ? current : { kind: 'loaded' })) + } + onLoadStart={() => setLoadState({ kind: 'loading' })} + source={site.source} + startInLoadingState + style={styles.webView} + testID="webview-content" + /> + + + ); +} + +function formatLoadStatus(siteLabel: string, loadState: LoadState): string { + switch (loadState.kind) { + case 'loading': + return `Loading ${siteLabel}…`; + case 'loaded': + return `Loaded ${siteLabel}`; + case 'error': + return `Failed to load ${siteLabel}: ${loadState.message}`; + } +} + +function createStyles(colors: AppColors) { + return StyleSheet.create({ + screen: { + backgroundColor: colors.surface, + flex: 1, + }, + header: { + alignItems: 'flex-start', + borderBottomColor: colors.line, + borderBottomWidth: StyleSheet.hairlineWidth, + flexDirection: 'row', + gap: 14, + paddingBottom: 14, + paddingHorizontal: 16, + }, + closeButton: { + borderColor: colors.lineStrong, + borderRadius: 4, + borderWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 12, + paddingVertical: 10, + }, + closeLabel: { + color: colors.text, + fontSize: 14, + fontWeight: '600', + }, + heading: { + flex: 1, + gap: 4, + }, + title: { + color: colors.text, + fontSize: 24, + fontWeight: '600', + lineHeight: 28, + }, + subtitle: { + color: colors.textSoft, + fontSize: 13, + lineHeight: 18, + }, + sitePicker: { + gap: 8, + paddingHorizontal: 16, + paddingVertical: 12, + }, + siteButton: { + alignSelf: 'flex-start', + borderColor: colors.lineStrong, + borderRadius: 4, + borderWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 12, + paddingVertical: 9, + }, + siteButtonSelected: { + backgroundColor: colors.text, + borderColor: colors.text, + }, + siteLabel: { + color: colors.text, + fontSize: 13, + fontWeight: '600', + }, + siteLabelSelected: { + color: colors.surface, + }, + status: { + alignItems: 'center', + borderBottomColor: colors.line, + borderBottomWidth: StyleSheet.hairlineWidth, + flexDirection: 'row', + gap: 8, + justifyContent: 'space-between', + minHeight: 38, + paddingHorizontal: 16, + paddingVertical: 8, + }, + statusText: { + color: colors.textSoft, + flex: 1, + fontSize: 12, + }, + webViewFrame: { + flex: 1, + }, + webView: { + backgroundColor: colors.surface, + flex: 1, + }, + pressed: { + opacity: 0.75, + }, + }); +} diff --git a/examples/test-app/src/webview-accessibility-fixture.ts b/examples/test-app/src/webview-accessibility-fixture.ts new file mode 100644 index 0000000000..84f3820756 --- /dev/null +++ b/examples/test-app/src/webview-accessibility-fixture.ts @@ -0,0 +1,73 @@ +export const WEBVIEW_ACCESSIBILITY_FIXTURE = ` + + + + + WebView semantic fixture + + + +
+ +
+
+

WebView semantic fixture

+

Ordinary paragraph text should not be categorized as other.

+

Account form

+
+ + + + +
+

Media

+ Blue accessibility test square +
+
Fixture footer text
+ + +`; diff --git a/src/daemon/__tests__/screenshot-overlay.test.ts b/src/daemon/__tests__/screenshot-overlay.test.ts index 5a9fd26d1b..9280c95164 100644 --- a/src/daemon/__tests__/screenshot-overlay.test.ts +++ b/src/daemon/__tests__/screenshot-overlay.test.ts @@ -193,7 +193,7 @@ test('buildScreenshotOverlayRefs skips generic actionable container labels when { index: 1, parentIndex: 0, - type: 'XCUIElementTypeSearchField', + type: 'XCUIElementTypeToolbar', label: 'Toolbar', rect: { x: 0, y: 150, width: 100, height: 30 }, }, diff --git a/src/daemon/snapshot-presentation/ios/actions.ts b/src/daemon/snapshot-presentation/ios/actions.ts index 08afa463b7..7e0154f1ee 100644 --- a/src/daemon/snapshot-presentation/ios/actions.ts +++ b/src/daemon/snapshot-presentation/ios/actions.ts @@ -12,10 +12,9 @@ export function collectIosImplicitScrollableActions( nodes: RawSnapshotNode[], context: SnapshotTreeRuleContext, ): void { - const byIndex = new Map(nodes.map((node) => [node.index, node])); - const viewport = findLargestViewportRect(byIndex.values()); + const viewport = findLargestViewportRect(context.sourceNodesByIndex.values()); for (const node of nodes) { - if (!isImplicitScrollableAction(node, byIndex, viewport)) { + if (!isImplicitScrollableAction(node, context.sourceNodesByIndex, viewport)) { continue; } mergeReplacement(context.replacements, node, { type: 'Cell' }); @@ -24,7 +23,7 @@ export function collectIosImplicitScrollableActions( function isImplicitScrollableAction( node: RawSnapshotNode, - byIndex: Map, + byIndex: ReadonlyMap, viewport: RawSnapshotNode['rect'], ): boolean { if (normalizeType(node.type ?? '') !== 'other') { diff --git a/src/daemon/snapshot-presentation/ios/index.ts b/src/daemon/snapshot-presentation/ios/index.ts index 5580997d39..b654eb3fae 100644 --- a/src/daemon/snapshot-presentation/ios/index.ts +++ b/src/daemon/snapshot-presentation/ios/index.ts @@ -2,6 +2,7 @@ import type { RawSnapshotNode } from '../../../kernel/snapshot.ts'; import { collectIosImplicitScrollableActions } from './actions.ts'; import { collectIosPresentationNoiseSuppression } from './noise.ts'; import { collectIosRowPresentation } from './rows.ts'; +import { collectIosWebSemanticPresentation } from './web.ts'; import { reindexSnapshotNodesWithSuppressedParents, type SnapshotTreeRuleContext, @@ -10,6 +11,8 @@ import { const IOS_PRESENTATION_RULES: Array< (nodes: RawSnapshotNode[], context: SnapshotTreeRuleContext) => void > = [ + // Semantic representatives must be collected before duplicate-label suppression. + collectIosWebSemanticPresentation, collectIosPresentationNoiseSuppression, collectIosImplicitScrollableActions, collectIosRowPresentation, @@ -32,14 +35,22 @@ export function buildIosInteractiveSnapshotPresentation( } const replacements = new Map(); + const semanticRepresentativeIndexes = new Set(); + const sourceNodesByIndex = new Map(nodes.map((node) => [node.index, node])); const suppressedIndexes = new Set(); + const ruleContext: SnapshotTreeRuleContext = { + replacements, + semanticRepresentativeIndexes, + sourceNodesByIndex, + suppressedIndexes, + }; for (const rule of IOS_PRESENTATION_RULES) { - rule(nodes, { replacements, suppressedIndexes }); + rule(nodes, ruleContext); } if (suppressedIndexes.size === 0 && replacements.size === 0) { - return { nodes, sourceNodes: new Map(nodes.map((node) => [node.index, node])) }; + return { nodes, sourceNodes: sourceNodesByIndex }; } const presentedSourceNodes = nodes diff --git a/src/daemon/snapshot-presentation/ios/noise.ts b/src/daemon/snapshot-presentation/ios/noise.ts index 509d8c4253..3bfa25f2ad 100644 --- a/src/daemon/snapshot-presentation/ios/noise.ts +++ b/src/daemon/snapshot-presentation/ios/noise.ts @@ -25,14 +25,18 @@ export function collectIosPresentationNoiseSuppression( context: SnapshotTreeRuleContext, ): void { const { suppressedIndexes } = context; - collectIosOffscreenKeyboardSuppression(nodes, suppressedIndexes); + collectIosOffscreenKeyboardSuppression(nodes, context.sourceNodesByIndex, suppressedIndexes); collectIosStructuralIdentifierSuppression(nodes, suppressedIndexes); collectIosScrollIndicatorPresentation(nodes, context); - collectIosSearchToolbarSuppression(nodes, suppressedIndexes); + collectIosSearchToolbarSuppression(nodes, context.sourceNodesByIndex, suppressedIndexes); collectIosActionWrapperSuppression(nodes, suppressedIndexes); collectIosReactNativeOverlayActionPresentation(nodes, context.replacements); collectIosReactNativeOverlayWrapperSuppression(nodes, suppressedIndexes); - collectIosRepeatedStaticSuppression(nodes, suppressedIndexes); + collectIosRepeatedStaticSuppression( + nodes, + suppressedIndexes, + context.semanticRepresentativeIndexes, + ); } function collectIosReactNativeOverlayActionPresentation( @@ -133,6 +137,7 @@ function collectDescendantNodes(nodes: RawSnapshotNode[], position: number): Raw function collectIosRepeatedStaticSuppression( nodes: RawSnapshotNode[], suppressedIndexes: Set, + semanticRepresentativeIndexes: ReadonlySet, ): void { for (let position = 0; position < nodes.length; position += 1) { const node = nodes[position]; @@ -141,7 +146,14 @@ function collectIosRepeatedStaticSuppression( continue; } - collectRepeatedStaticSuppressionForNode(nodes, position, node, nodeLabel, suppressedIndexes); + collectRepeatedStaticSuppressionForNode( + nodes, + position, + node, + nodeLabel, + suppressedIndexes, + semanticRepresentativeIndexes, + ); } } @@ -151,10 +163,17 @@ function collectRepeatedStaticSuppressionForNode( node: RawSnapshotNode, nodeLabel: string, suppressedIndexes: Set, + semanticRepresentativeIndexes: ReadonlySet, ): void { const type = normalizeType(node.type ?? ''); if (type === 'statictext' || type === 'link') { - suppressRepeatedStaticDescendants(nodes, position, nodeLabel, suppressedIndexes); + suppressRepeatedStaticDescendants( + nodes, + position, + nodeLabel, + suppressedIndexes, + semanticRepresentativeIndexes, + ); return; } if (type !== 'other') { @@ -164,7 +183,13 @@ function collectRepeatedStaticSuppressionForNode( suppressedIndexes.add(node.index); return; } - suppressRepeatedStaticDescendants(nodes, position, nodeLabel, suppressedIndexes); + suppressRepeatedStaticDescendants( + nodes, + position, + nodeLabel, + suppressedIndexes, + semanticRepresentativeIndexes, + ); } function hasEquivalentSemanticDescendant( @@ -188,9 +213,13 @@ function suppressRepeatedStaticDescendants( position: number, label: string, suppressedIndexes: Set, + semanticRepresentativeIndexes: ReadonlySet, ): void { forEachDescendant(nodes, position, (descendant) => { - if (isRepeatedStaticNode(descendant, label)) { + if ( + !semanticRepresentativeIndexes.has(descendant.index) && + isRepeatedStaticNode(descendant, label) + ) { suppressedIndexes.add(descendant.index); } }); @@ -254,6 +283,7 @@ function isFullscreenActionLabelWrapper( function collectIosOffscreenKeyboardSuppression( nodes: RawSnapshotNode[], + sourceNodesByIndex: ReadonlyMap, suppressedIndexes: Set, ): void { const viewport = findLargestViewportRect(nodes); @@ -267,7 +297,7 @@ function collectIosOffscreenKeyboardSuppression( continue; } suppressedIndexes.add(node.index); - suppressOffscreenKeyboardAncestors(node, nodes, suppressedIndexes, screenBottom); + suppressOffscreenKeyboardAncestors(node, sourceNodesByIndex, suppressedIndexes, screenBottom); forEachDescendant(nodes, position, (descendant) => { suppressedIndexes.add(descendant.index); }); @@ -283,16 +313,18 @@ function isOffscreenKeyboardNode(node: RawSnapshotNode, screenBottom: number): b function suppressOffscreenKeyboardAncestors( node: RawSnapshotNode, - nodes: RawSnapshotNode[], + sourceNodesByIndex: ReadonlyMap, suppressedIndexes: Set, screenBottom: number, ): void { - const byIndex = new Map(nodes.map((candidate) => [candidate.index, candidate])); - let current = typeof node.parentIndex === 'number' ? byIndex.get(node.parentIndex) : undefined; + let current = + typeof node.parentIndex === 'number' ? sourceNodesByIndex.get(node.parentIndex) : undefined; while (current?.rect && current.rect.y >= screenBottom) { suppressedIndexes.add(current.index); current = - typeof current.parentIndex === 'number' ? byIndex.get(current.parentIndex) : undefined; + typeof current.parentIndex === 'number' + ? sourceNodesByIndex.get(current.parentIndex) + : undefined; } } @@ -316,20 +348,18 @@ function collectIosStructuralIdentifierSuppression( function collectIosSearchToolbarSuppression( nodes: RawSnapshotNode[], + sourceNodesByIndex: ReadonlyMap, suppressedIndexes: Set, ): void { for (let position = 0; position < nodes.length; position += 1) { const node = nodes[position]; - if (!node || normalizeType(node.type ?? '') !== 'searchfield') { - continue; - } - if (node.label === 'Search') { + if (!node) continue; + if (isExposedSearchField(node)) { suppressSearchToolbarDescendants(nodes, position, null, suppressedIndexes); continue; } - if (node.label !== 'Toolbar') { - continue; - } + if (!isSearchToolbar(node)) continue; + const innerSearch = findDescendant( nodes, position, @@ -341,11 +371,20 @@ function collectIosSearchToolbarSuppression( } suppressedIndexes.add(node.index); - suppressToolbarAncestors(node, nodes, suppressedIndexes); + suppressToolbarAncestors(node, sourceNodesByIndex, suppressedIndexes); suppressSearchToolbarDescendants(nodes, position, innerSearch.index, suppressedIndexes); } } +function isExposedSearchField(node: RawSnapshotNode): boolean { + return normalizeType(node.type ?? '') === 'searchfield' && node.label === 'Search'; +} + +function isSearchToolbar(node: RawSnapshotNode): boolean { + const type = normalizeType(node.type ?? ''); + return node.label === 'Toolbar' && (type === 'toolbar' || type === 'searchfield'); +} + function suppressSearchToolbarDescendants( nodes: RawSnapshotNode[], position: number, @@ -364,13 +403,12 @@ function suppressSearchToolbarDescendants( function suppressToolbarAncestors( node: RawSnapshotNode, - nodes: RawSnapshotNode[], + sourceNodesByIndex: ReadonlyMap, suppressedIndexes: Set, ): void { - const byIndex = new Map(nodes.map((candidate) => [candidate.index, candidate])); let current = node; while (typeof current.parentIndex === 'number') { - const parent = byIndex.get(current.parentIndex); + const parent = sourceNodesByIndex.get(current.parentIndex); if (!parent || parent.label !== 'Toolbar') { return; } diff --git a/src/daemon/snapshot-presentation/ios/presentation.test.ts b/src/daemon/snapshot-presentation/ios/presentation.test.ts index 8703f42d61..f1174d4d7d 100644 --- a/src/daemon/snapshot-presentation/ios/presentation.test.ts +++ b/src/daemon/snapshot-presentation/ios/presentation.test.ts @@ -397,7 +397,7 @@ test('buildSnapshotState uses the innermost iOS RedBox dismiss action geometry', index: 4, depth: 4, parentIndex: 3, - type: 'Other', + type: 'StaticText', label: 'Dismiss', rect: { x: 0, y: 770, width: 196.6666717529297, height: 48 }, }, @@ -419,6 +419,7 @@ test('buildSnapshotState uses the innermost iOS RedBox dismiss action geometry', width: 196.6666717529297, height: 48, }); + expect(state.nodes.filter((node) => node.label === 'Dismiss')).toHaveLength(1); }); test('buildSnapshotState promotes iOS scroll-contained other rows to cells', () => { @@ -757,7 +758,7 @@ test('buildSnapshotState collapses duplicated iOS search toolbar wrappers', () = index: 2, depth: 2, parentIndex: 1, - type: 'SearchField', + type: 'Toolbar', label: 'Toolbar', identifier: 'Toolbar', rect: { x: 0, y: 788, width: 402, height: 86 }, diff --git a/src/daemon/snapshot-presentation/ios/scroll.ts b/src/daemon/snapshot-presentation/ios/scroll.ts index bfc87e795e..71feb88ea3 100644 --- a/src/daemon/snapshot-presentation/ios/scroll.ts +++ b/src/daemon/snapshot-presentation/ios/scroll.ts @@ -14,12 +14,11 @@ export function collectIosScrollIndicatorPresentation( nodes: RawSnapshotNode[], context: SnapshotTreeRuleContext, ): void { - const byIndex = new Map(nodes.map((node) => [node.index, node])); for (const node of nodes) { if (!isIosScrollIndicatorNode(node)) { continue; } - collectIosScrollIndicatorNodePresentation(node, byIndex, context); + collectIosScrollIndicatorNodePresentation(node, context.sourceNodesByIndex, context); } } @@ -30,7 +29,7 @@ function isIosScrollIndicatorNode(node: RawSnapshotNode): boolean { function collectIosScrollIndicatorNodePresentation( node: RawSnapshotNode, - byIndex: Map, + byIndex: ReadonlyMap, context: SnapshotTreeRuleContext, ): void { if (!isScrollableSnapshotType(node.type)) { diff --git a/src/daemon/snapshot-presentation/ios/web.test.ts b/src/daemon/snapshot-presentation/ios/web.test.ts new file mode 100644 index 0000000000..469f5c88f7 --- /dev/null +++ b/src/daemon/snapshot-presentation/ios/web.test.ts @@ -0,0 +1,133 @@ +import { expect, test } from 'vitest'; +import type { RawSnapshotNode } from '../../../kernel/snapshot.ts'; +import { presentIosInteractiveSnapshot } from './index.ts'; + +test('projects iOS WebKit heading and text wrappers to semantic roles', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'Application', label: 'Example' }, + { index: 1, depth: 1, parentIndex: 0, type: 'WebView', label: 'Example page' }, + { index: 2, depth: 2, parentIndex: 1, type: 'Other', label: 'Example page' }, + { index: 3, depth: 3, parentIndex: 2, type: 'Other', label: 'Welcome', value: '1' }, + { index: 4, depth: 4, parentIndex: 3, type: 'StaticText', label: 'Welcome' }, + { index: 5, depth: 3, parentIndex: 2, type: 'Other', label: 'Introduction' }, + { index: 6, depth: 4, parentIndex: 5, type: 'StaticText', label: 'Introduction' }, + { index: 7, depth: 3, parentIndex: 2, type: 'Link', label: 'Read more' }, + ]; + + expect( + presentIosInteractiveSnapshot(nodes).map((node) => [ + node.type, + node.label, + node.value, + node.parentIndex, + ]), + ).toEqual([ + ['Application', 'Example', undefined, undefined], + ['WebView', 'Example page', undefined, 0], + ['Heading', 'Welcome', undefined, 1], + ['StaticText', 'Introduction', undefined, 1], + ['Link', 'Read more', undefined, 1], + ]); + expect(nodes[3]?.value).toBe('1'); +}); + +test('recognizes numeric WebView types from retained older runners', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'Element(58)', label: 'Example page' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'Body copy' }, + { index: 2, depth: 2, parentIndex: 1, type: 'StaticText', label: 'Body copy' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['WebView', 'Example page'], + ['StaticText', 'Body copy'], + ]); +}); + +test('keeps a nested heading whose label matches the WebView title', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'WebView', label: 'Same title' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'Same title' }, + { index: 2, depth: 2, parentIndex: 1, type: 'Other', label: 'Same title', value: '1' }, + { index: 3, depth: 3, parentIndex: 2, type: 'StaticText', label: 'Same title' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['WebView', 'Same title'], + ['Heading', 'Same title'], + ]); +}); + +test('keeps a heading through repeated WebKit document-title wrappers', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'WebView', label: 'Same title' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'Same title' }, + { index: 2, depth: 2, parentIndex: 1, type: 'Other', label: 'Same title' }, + { index: 3, depth: 3, parentIndex: 2, type: 'Other', label: 'main' }, + { index: 4, depth: 4, parentIndex: 3, type: 'Other', label: 'Same title', value: '1' }, + { index: 5, depth: 5, parentIndex: 4, type: 'StaticText', label: 'Same title' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['WebView', 'Same title'], + ['Other', 'Same title'], + ['Other', 'main'], + ['Heading', 'Same title'], + ]); +}); + +test('keeps a direct heading whose label matches the WebView title', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'WebView', label: 'Same title' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'Same title', value: '1' }, + { index: 2, depth: 2, parentIndex: 1, type: 'StaticText', label: 'Same title' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['WebView', 'Same title'], + ['Heading', 'Same title'], + ]); +}); + +test('does not infer text from a nested same-label descendant', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'WebView', label: 'Example' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'Container' }, + { index: 2, depth: 2, parentIndex: 1, type: 'Button', label: 'Action' }, + { index: 3, depth: 3, parentIndex: 2, type: 'StaticText', label: 'Container' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['WebView', 'Example'], + ['Other', 'Container'], + ['Button', 'Action'], + ]); +}); + +test('keeps a labelled WebKit container with additional semantic content', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'WebView', label: 'Example' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'News' }, + { index: 2, depth: 2, parentIndex: 1, type: 'StaticText', label: 'News' }, + { index: 3, depth: 2, parentIndex: 1, type: 'Link', label: 'Read more' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['WebView', 'Example'], + ['Other', 'News'], + ['Link', 'Read more'], + ]); +}); + +test('does not infer semantic roles for matching wrappers outside a WebView', () => { + const nodes: RawSnapshotNode[] = [ + { index: 0, depth: 0, type: 'Application', label: 'Example' }, + { index: 1, depth: 1, parentIndex: 0, type: 'Other', label: 'Native copy', value: '1' }, + { index: 2, depth: 2, parentIndex: 1, type: 'StaticText', label: 'Native copy' }, + ]; + + expect(presentIosInteractiveSnapshot(nodes).map((node) => [node.type, node.label])).toEqual([ + ['Application', 'Example'], + ['Other', 'Native copy'], + ]); +}); diff --git a/src/daemon/snapshot-presentation/ios/web.ts b/src/daemon/snapshot-presentation/ios/web.ts new file mode 100644 index 0000000000..4a9683f187 --- /dev/null +++ b/src/daemon/snapshot-presentation/ios/web.ts @@ -0,0 +1,111 @@ +import type { RawSnapshotNode } from '../../../kernel/snapshot.ts'; +import { normalizeType } from '../../../snapshot/snapshot-processing.ts'; +import { findNearestAncestor, mergeReplacement, type SnapshotTreeRuleContext } from '../tree.ts'; + +/** + * WebKit exposes HTML text through an `Other -> StaticText` wrapper pair on iOS. + * Keep the wrapper's stable tree position but project the semantic leaf role so + * selectors and rendered snapshots do not describe ordinary text as "other". + * + * HTML headings use the same pair and expose their level as the wrapper value. + * XCTest does not publish the level as a dedicated field, so the public role is + * `Heading` while the backend-specific level remains available only in the raw + * diagnostic node. + */ +export function collectIosWebSemanticPresentation( + nodes: RawSnapshotNode[], + context: SnapshotTreeRuleContext, +): void { + const childrenByParent = collectChildrenByParent(nodes); + + for (const node of nodes) { + if (normalizeType(node.type ?? '') === 'element(58)') { + mergeReplacement(context.replacements, node, { type: 'WebView' }); + continue; + } + const presentation = classifyIosWebTextWrapper( + node, + context.sourceNodesByIndex, + childrenByParent, + ); + if (presentation?.kind === 'suppress') { + context.suppressedIndexes.add(node.index); + continue; + } + if (presentation) { + mergeReplacement(context.replacements, node, { + type: presentation.type, + ...(presentation.type === 'Heading' ? { value: undefined } : {}), + }); + context.semanticRepresentativeIndexes.add(node.index); + } + } +} + +type IosWebTextWrapperPresentation = + | { kind: 'semantic'; type: 'Heading' | 'StaticText' } + | { kind: 'suppress' }; + +function classifyIosWebTextWrapper( + node: RawSnapshotNode, + byIndex: ReadonlyMap, + childrenByParent: ReadonlyMap, +): IosWebTextWrapperPresentation | null { + if (normalizeType(node.type ?? '') !== 'other') return null; + const webView = findNearestAncestor(node, byIndex, isWebView); + const label = node.label?.trim(); + if (!webView || !label) return null; + if (isDocumentTitleWrapper(node, webView, label)) return { kind: 'suppress' }; + if (!isWebTextLeafWrapper(node, label, childrenByParent)) return null; + return { + kind: 'semantic', + type: isHtmlHeadingLevel(node.value) ? 'Heading' : 'StaticText', + }; +} + +function isWebTextLeafWrapper( + node: RawSnapshotNode, + label: string, + childrenByParent: ReadonlyMap, +): boolean { + const children = childrenByParent.get(node.index); + if (children?.length !== 1) return false; + const text = children[0]!; + return ( + normalizeType(text.type ?? '') === 'statictext' && + text.label?.trim() === label && + !childrenByParent.has(text.index) + ); +} + +function isDocumentTitleWrapper( + node: RawSnapshotNode, + webView: RawSnapshotNode, + label: string, +): boolean { + return ( + node.parentIndex === webView.index && + webView.label?.trim() === label && + !isHtmlHeadingLevel(node.value) + ); +} + +function collectChildrenByParent(nodes: RawSnapshotNode[]): Map { + const childrenByParent = new Map(); + for (const node of nodes) { + if (typeof node.parentIndex !== 'number') continue; + const children = childrenByParent.get(node.parentIndex) ?? []; + children.push(node); + childrenByParent.set(node.parentIndex, children); + } + return childrenByParent; +} + +function isWebView(node: RawSnapshotNode): boolean { + const type = normalizeType(node.type ?? ''); + return type === 'webview' || type === 'element(58)'; +} + +function isHtmlHeadingLevel(value: string | undefined): boolean { + return /^[1-6]$/.test(value?.trim() ?? ''); +} diff --git a/src/daemon/snapshot-presentation/tree.ts b/src/daemon/snapshot-presentation/tree.ts index 8f4a87b69a..6b0435a498 100644 --- a/src/daemon/snapshot-presentation/tree.ts +++ b/src/daemon/snapshot-presentation/tree.ts @@ -4,6 +4,9 @@ export { areRectsApproximatelyEqual } from '../../utils/rect-center.ts'; export type SnapshotTreeRuleContext = { replacements: Map; + /** Projected label owners that repeated-text suppression must retain. */ + semanticRepresentativeIndexes: Set; + sourceNodesByIndex: ReadonlyMap; suppressedIndexes: Set; }; @@ -71,7 +74,10 @@ function getDescendantEndPositions(nodes: RawSnapshotNode[]): number[] { return endPositions; } -function collectAncestors(node: T, byIndex: Map): T[] { +function collectAncestors( + node: T, + byIndex: ReadonlyMap, +): T[] { const ancestors: T[] = []; let current = typeof node.parentIndex === 'number' ? byIndex.get(node.parentIndex) : undefined; const visited = new Set(); @@ -86,7 +92,7 @@ function collectAncestors(node: T, byIndex: Map( node: T, - byIndex: Map, + byIndex: ReadonlyMap, predicate: (ancestor: T) => boolean, ): T | null { for (const ancestor of collectAncestors(node, byIndex)) { @@ -99,7 +105,7 @@ export function findNearestAncestor( export function findNearestScrollableContainer( node: T, - byIndex: Map, + byIndex: ReadonlyMap, options: { includeSelf?: boolean } = {}, ): T | null { const self = options.includeSelf === true && isScrollableSnapshotType(node.type) ? node : null; diff --git a/src/snapshot/snapshot-lines.ts b/src/snapshot/snapshot-lines.ts index 1c3a9f0c87..30a0a8a881 100644 --- a/src/snapshot/snapshot-lines.ts +++ b/src/snapshot/snapshot-lines.ts @@ -42,6 +42,9 @@ const ROLE_LABELS: Record = { recyclerview: 'list', collectionview: 'collection', searchfield: 'search', + heading: 'heading', + activityindicator: 'activity-indicator', + progressindicator: 'progress-indicator', segmentedcontrol: 'segmented-control', group: 'group', window: 'window',