mobile-debug-mcp 0.32.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/observe/android-tree-bridge.js +202 -0
- package/dist/observe/android.js +49 -15
- package/dist/observe/ios.js +45 -29
- package/dist/observe/performance-metrics.js +124 -0
- package/dist/server/tool-handlers.js +31 -9
- package/dist/server-core.js +1 -1
- package/dist/utils/exec.js +15 -4
- package/docs/CHANGELOG.md +6 -0
- package/docs/ROADMAP.md +9 -0
- package/docs/benchmarks/ui-tree-performance/README.md +58 -0
- package/docs/benchmarks/ui-tree-performance/baseline.json +59546 -0
- package/docs/benchmarks/ui-tree-performance/bridge-probe.json +29 -0
- package/docs/benchmarks/ui-tree-performance/candidate.json +51691 -0
- package/docs/benchmarks/ui-tree-performance/comparison-attempt-1.json +110679 -0
- package/docs/benchmarks/ui-tree-performance/comparison.json +111230 -0
- package/docs/benchmarks/ui-tree-performance/comparison.md +41 -0
- package/docs/benchmarks/ui-tree-performance/initial-baseline.json +59560 -0
- package/docs/specs/019-ui-tree-observation-performance/spec.md +148 -0
- package/docs/tools/observe.md +2 -0
- package/package.json +1 -1
- package/skills/mobile-debug-tools-usage/SKILL.md +74 -0
- package/src/observe/android-tree-bridge.ts +178 -0
- package/src/observe/android.ts +44 -17
- package/src/observe/ios.ts +45 -36
- package/src/observe/performance-metrics.ts +125 -0
- package/src/server/tool-handlers.ts +34 -10
- package/src/server-core.ts +1 -1
- package/src/utils/exec.ts +10 -4
- package/test/device/automated/ui-tree-performance/benchmark.ts +401 -0
- package/test/device/automated/ui-tree-performance/bridge-probe.ts +197 -0
- package/test/fixtures/latency-journey-app/AndroidManifest.xml +2 -1
- package/test/fixtures/latency-journey-app/README.md +19 -1
- package/test/fixtures/latency-journey-app/src/MainActivity.java +88 -1
- package/test/fixtures/ui-tree-bridge/AndroidManifest.xml +6 -0
- package/test/fixtures/ui-tree-bridge/build.ts +34 -0
- package/test/fixtures/ui-tree-bridge/src/TreeInstrumentation.java +126 -0
- package/test/unit/observe/android_tree_acquisition.test.ts +70 -0
- package/test/unit/observe/android_tree_bridge_cleanup.test.ts +95 -0
- package/test/unit/observe/ios_tree_acquisition.test.ts +77 -0
- package/test/unit/observe/performance_metrics.test.ts +40 -0
- package/test/unit/server/operation_budget.test.ts +13 -0
- package/test/unit/server/run_journey.test.ts +30 -0
- /package/docs/{specs-deltas/001-state-verification.md → specs/001-state-verification/spec.md} +0 -0
- /package/docs/{specs-deltas/002-richer-element-identity → specs/002-richer-element-identity/spec.md} +0 -0
- /package/docs/{specs-deltas/003-wait-and-synchronization-reliability.md → specs/003-wait-and-synchronization-reliability/spec.md} +0 -0
- /package/docs/{specs-deltas/004-action-verification-routing.md → specs/004-action-verification-routing/spec.md} +0 -0
- /package/docs/{specs-deltas/005-unified-action-execution-and-verification-model.md → specs/005-unified-action-execution-and-verification-model/spec.md} +0 -0
- /package/docs/{specs-deltas/006-runtime-action-instrumentation-and-binding-layer.md → specs/006-runtime-action-instrumentation-and-binding-layer/spec.md} +0 -0
- /package/docs/{specs-deltas/007-actionability-resolution-and-executable-target-selection.md → specs/007-actionability-resolution-and-executable-target-selection/spec.md} +0 -0
- /package/docs/{specs-deltas/008-adjustable-control-support-and-semantic-value-manipulation.md → specs/008-adjustable-control-support-and-semantic-value-manipulation/spec.md} +0 -0
- /package/docs/{specs-deltas/009-semantic-control-modeling-for-custom-and-composite-controls.md → specs/009-semantic-control-modeling-for-custom-and-composite-controls/spec.md} +0 -0
- /package/docs/{specs-deltas/010-verification-stabilization-and-temporal-convergence.md → specs/010-verification-stabilization-and-temporal-convergence/spec.md} +0 -0
- /package/docs/{specs-deltas/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows.md → specs/011-recovery-and-replanning-for-failed-or-ambiguous-interaction-flows/spec.md} +0 -0
- /package/docs/{specs-deltas/011.1-recovery-contract-types-and-runtime-wiring-spec.md → specs/011.1-recovery-contract-types-and-runtime-wiring-spec/spec.md} +0 -0
- /package/docs/{specs-deltas/012-action-trace-and-xecution-observability.md → specs/012-action-trace-and-xecution-observability/spec.md} +0 -0
- /package/docs/{specs-deltas/013-wait-and-synchronization-reliability.md → specs/013-wait-and-synchronization-reliability/spec.md} +0 -0
- /package/docs/{specs-deltas/014-actionability-resolution.md → specs/014-actionability-resolution/spec.md} +0 -0
- /package/docs/{specs-deltas/015-adjustable-control-support.md → specs/015-adjustable-control-support/spec.md} +0 -0
- /package/docs/{specs-deltas/016-environment-tools.md → specs/016-environment-tools/spec.md} +0 -0
- /package/docs/{specs-deltas/018-interaction-latency-and-batched-journeys.md → specs/018-interaction-latency-and-batched-journeys/spec.md} +0 -0
|
@@ -9,6 +9,7 @@ import { ToolsObserve } from '../observe/index.js';
|
|
|
9
9
|
import { classifyActionOutcome } from '../interact/classify.js';
|
|
10
10
|
import { ToolsNetwork } from '../network/index.js';
|
|
11
11
|
import { getSystemStatus } from '../system/index.js';
|
|
12
|
+
import { recordObservationConsumer, recordObservationEvent, withObservationContext } from '../observe/performance-metrics.js';
|
|
12
13
|
import { buildActionExecutionResult, captureActionFingerprint, getArrayArg, getBooleanArg, getNumberArg, getObjectArg, getStringArg, inferGenericFailure, inferScrollFailure, requireBooleanArg, requireNumberArg, requireObjectArg, requireStringArg, wrapResponse, wrapToolError } from './common.js';
|
|
13
14
|
function interactionControls(args) {
|
|
14
15
|
const verificationMode = getStringArg(args, 'verificationMode') ?? 'full';
|
|
@@ -79,11 +80,25 @@ function actionHandler(type, execute) {
|
|
|
79
80
|
result.retryable = !dispatchStarted;
|
|
80
81
|
result.action_id ??= randomUUID();
|
|
81
82
|
result.action_type ??= type;
|
|
83
|
+
recordObservationEvent({
|
|
84
|
+
step_id: args._stepId,
|
|
85
|
+
action_type: type,
|
|
86
|
+
dispatch_ms: timing.dispatch_ms,
|
|
87
|
+
verification_mode: controls.verificationMode,
|
|
88
|
+
dispatch_started: dispatchStarted
|
|
89
|
+
});
|
|
82
90
|
let delta;
|
|
83
91
|
let observationAvailable = false;
|
|
84
92
|
if (result.success && controls.verificationMode === 'light') {
|
|
85
|
-
const
|
|
93
|
+
const verificationStarted = performance.now();
|
|
94
|
+
const tree = await withObservationContext({ stepId: args._stepId, purpose: 'verification' }, () => measure('post_observation_ms', () => runWithBudget(controls.verificationTimeoutMs, () => ToolsObserve.getUITreeHandler({ platform: args.platform ?? result.device?.platform, deviceId: args.deviceId ?? result.device?.id })))).catch(() => null);
|
|
86
95
|
observationAvailable = !!tree && !tree.error;
|
|
96
|
+
recordObservationEvent({
|
|
97
|
+
verification_budget_ms: controls.verificationTimeoutMs,
|
|
98
|
+
verification_elapsed_ms: performance.now() - verificationStarted,
|
|
99
|
+
verification_outcome: observationAvailable ? 'observed' : 'unavailable',
|
|
100
|
+
verification_budget_expired: !observationAvailable && performance.now() - verificationStarted >= controls.verificationTimeoutMs
|
|
101
|
+
});
|
|
87
102
|
if (observationAvailable && tree.device)
|
|
88
103
|
delta = ToolsObserve.getLatestStateDelta(tree.device.platform, tree.device.id) ?? undefined;
|
|
89
104
|
}
|
|
@@ -286,7 +301,8 @@ async function handleGetUITree(args) {
|
|
|
286
301
|
const deviceId = getStringArg(args, 'deviceId');
|
|
287
302
|
const responseMode = getStringArg(args, 'responseMode') ?? 'debug';
|
|
288
303
|
const sinceSnapshotRevision = getNumberArg(args, 'sinceSnapshotRevision');
|
|
289
|
-
const
|
|
304
|
+
const metricsStepId = process.env.MOBILE_DEBUG_MCP_UI_TREE_METRICS === '1' ? getStringArg(args, '_metricsStepId') : undefined;
|
|
305
|
+
const res = await withObservationContext({ stepId: metricsStepId, purpose: metricsStepId?.includes('post-response') ? 'assertion' : 'debug' }, () => ToolsObserve.getUITreeHandler({ platform, deviceId, responseMode, sinceSnapshotRevision }));
|
|
290
306
|
return wrapResponse(res);
|
|
291
307
|
}
|
|
292
308
|
async function handleGetCurrentScreen(args) {
|
|
@@ -396,7 +412,9 @@ async function handleFindElement(args) {
|
|
|
396
412
|
const timeoutMs = getNumberArg(args, 'timeoutMs') ?? 3000;
|
|
397
413
|
const platform = getStringArg(args, 'platform');
|
|
398
414
|
const deviceId = getStringArg(args, 'deviceId');
|
|
399
|
-
const
|
|
415
|
+
const targetStarted = performance.now();
|
|
416
|
+
const res = await withObservationContext({ purpose: 'target' }, () => ToolsInteract.findElementHandler({ query, exact, timeoutMs, platform, deviceId }));
|
|
417
|
+
recordObservationEvent({ target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: res.found ? 'found' : 'not_found', operation: 'find_element' });
|
|
400
418
|
return wrapResponse(res);
|
|
401
419
|
}
|
|
402
420
|
async function executeTap(args) {
|
|
@@ -431,13 +449,17 @@ async function executeTapElement(args) {
|
|
|
431
449
|
let freshTree;
|
|
432
450
|
if (selector) {
|
|
433
451
|
const waitFor = getObjectArg(args, 'waitFor');
|
|
434
|
-
const
|
|
452
|
+
const targetStarted = performance.now();
|
|
453
|
+
const resolution = await withObservationContext({ stepId: args._stepId, purpose: 'target' }, () => measure('target_resolution_ms', () => runWithBudget(waitFor?.timeoutMs ?? 1500, () => ToolsInteract.waitForUIHandler({ selector, condition: waitFor?.condition ?? 'clickable', timeout_ms: waitFor?.timeoutMs ?? 1500, poll_interval_ms: waitFor?.pollIntervalMs ?? 100, match: waitFor?.match, singleObservation: true, singleAttempt: !waitFor, rejectAmbiguous: !waitFor, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }))));
|
|
454
|
+
recordObservationEvent({ step_id: args._stepId, purpose: 'target', target_resolution_ms: performance.now() - targetStarted, target_resolution_outcome: resolution.status });
|
|
435
455
|
if (resolution.status !== 'success' || !resolution.element?.elementId)
|
|
436
456
|
return wrapResponse({ success: false, action_type: 'tap_element', failure_code: resolution.error?.code ?? 'ELEMENT_NOT_FOUND', retryable: true });
|
|
437
457
|
resolvedId = resolution.element.elementId;
|
|
438
458
|
freshTree = resolution._tree;
|
|
439
459
|
}
|
|
440
460
|
ToolsNetwork.notifyActionStart();
|
|
461
|
+
if (freshTree)
|
|
462
|
+
recordObservationConsumer(freshTree, 'target', args._stepId);
|
|
441
463
|
const res = await measure('dispatch_ms', () => ToolsInteract.tapElementHandler({ elementId: resolvedId, freshTree, verificationMode, timeoutMs: actionTimeoutMs, platform: getStringArg(args, 'platform'), deviceId: getStringArg(args, 'deviceId') }));
|
|
442
464
|
return wrapResponse(res);
|
|
443
465
|
}
|
|
@@ -505,14 +527,14 @@ async function handleRunJourney(args) {
|
|
|
505
527
|
}
|
|
506
528
|
try {
|
|
507
529
|
if (step.type === 'tap') {
|
|
508
|
-
result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
|
|
530
|
+
result = payloadOf(await handleTapElement({ ...defaults, ...step, platform, deviceId, responseMode, _journey: true, _stepId: step.id, verificationMode: lightCoveredByAssertion ? 'none' : requestedVerification }));
|
|
509
531
|
}
|
|
510
532
|
else if (step.type === 'wait') {
|
|
511
|
-
result = await ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
|
|
533
|
+
result = await withObservationContext({ stepId: step.id, purpose: 'target' }, () => ToolsInteract.waitForUIHandler({ selector: step.selector, condition: step.condition ?? 'exists', timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId }));
|
|
512
534
|
result = { success: result.status === 'success', ...result };
|
|
513
535
|
}
|
|
514
536
|
else if (step.type === 'assert') {
|
|
515
|
-
result = await runWithBudget(step.timeoutMs ?? 5000, async () => {
|
|
537
|
+
result = await withObservationContext({ stepId: step.id, purpose: 'assertion' }, () => runWithBudget(step.timeoutMs ?? 5000, async () => {
|
|
516
538
|
const assertion = step.assertion;
|
|
517
539
|
if (assertion?.kind === 'element_visible')
|
|
518
540
|
result = await ToolsInteract.expectElementVisibleHandler({ selector: assertion.selector, timeout_ms: step.timeoutMs ?? 5000, poll_interval_ms: step.pollIntervalMs ?? 100, platform, deviceId });
|
|
@@ -525,10 +547,10 @@ async function handleRunJourney(args) {
|
|
|
525
547
|
else
|
|
526
548
|
result = { success: false, failure_code: 'INVALID_ASSERTION' };
|
|
527
549
|
return result;
|
|
528
|
-
});
|
|
550
|
+
}));
|
|
529
551
|
}
|
|
530
552
|
else if (step.type === 'start_app') {
|
|
531
|
-
result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
553
|
+
result = payloadOf(await handleStartApp({ ...defaults, platform, deviceId, appId: step.appId, responseMode, _journey: true, _stepId: step.id, verificationMode: step.verificationMode ?? defaults?.verificationMode ?? 'full' }));
|
|
532
554
|
}
|
|
533
555
|
else {
|
|
534
556
|
result = { success: false, failure_code: 'UNSUPPORTED_JOURNEY_STEP' };
|
package/dist/server-core.js
CHANGED
|
@@ -6,7 +6,7 @@ import { handleToolCall } from './server/tool-handlers.js';
|
|
|
6
6
|
export { wrapResponse, toolDefinitions, handleToolCall };
|
|
7
7
|
export const serverInfo = {
|
|
8
8
|
name: 'mobile-debug-mcp',
|
|
9
|
-
version: '0.32.
|
|
9
|
+
version: '0.32.2'
|
|
10
10
|
};
|
|
11
11
|
export function createServer() {
|
|
12
12
|
const server = new Server(serverInfo, {
|
package/dist/utils/exec.js
CHANGED
|
@@ -12,24 +12,35 @@ export async function execCmd(cmd, args, opts = {}) {
|
|
|
12
12
|
if (child.stderr)
|
|
13
13
|
child.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
14
14
|
let timedOut = false;
|
|
15
|
+
let forceKillTimer = null;
|
|
15
16
|
const timer = timeout && timeout > 0 ? setTimeout(() => {
|
|
16
17
|
timedOut = true;
|
|
17
18
|
try {
|
|
18
19
|
child.kill();
|
|
19
20
|
}
|
|
20
21
|
catch { }
|
|
21
|
-
|
|
22
|
+
forceKillTimer = setTimeout(() => {
|
|
23
|
+
try {
|
|
24
|
+
child.kill('SIGKILL');
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
}, 1000);
|
|
28
|
+
forceKillTimer.unref();
|
|
22
29
|
}, timeout) : null;
|
|
23
30
|
child.on('close', (code) => {
|
|
24
31
|
if (timer)
|
|
25
32
|
clearTimeout(timer);
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
resolve(
|
|
33
|
+
if (forceKillTimer)
|
|
34
|
+
clearTimeout(forceKillTimer);
|
|
35
|
+
resolve(timedOut
|
|
36
|
+
? { exitCode: null, stdout: stdout.trim(), stderr: 'ACTION_TIMEOUT' }
|
|
37
|
+
: { exitCode: code, stdout: stdout.trim(), stderr: stderr.trim() });
|
|
29
38
|
});
|
|
30
39
|
child.on('error', (err) => {
|
|
31
40
|
if (timer)
|
|
32
41
|
clearTimeout(timer);
|
|
42
|
+
if (forceKillTimer)
|
|
43
|
+
clearTimeout(forceKillTimer);
|
|
33
44
|
reject(err);
|
|
34
45
|
});
|
|
35
46
|
});
|
package/docs/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the **Mobile Debug MCP** project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.32.2]
|
|
6
|
+
- bug fix
|
|
7
|
+
|
|
8
|
+
## [0.32.1]
|
|
9
|
+
- Further performance enhancements
|
|
10
|
+
|
|
5
11
|
## [0.32.0]
|
|
6
12
|
- Added bounded interaction verification modes, compact responses, selector-based taps, and batched journeys to reduce MCP interaction latency.
|
|
7
13
|
|
package/docs/ROADMAP.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
- Actionability Resolution
|
|
12
12
|
- Environment Auto-Configuration and Toolchain Discovery
|
|
13
13
|
- Adjustable Control Support
|
|
14
|
+
- UI-Tree Observation Performance (Android implemented; iOS latency unmeasured)
|
|
14
15
|
|
|
15
16
|
## Todo
|
|
16
17
|
|
|
@@ -21,6 +22,14 @@
|
|
|
21
22
|
- Pinch to Zoom
|
|
22
23
|
- Advanced Trace Correlation and Analysis
|
|
23
24
|
|
|
25
|
+
# UI-Tree Observation Performance
|
|
26
|
+
|
|
27
|
+
**Status:** Android implemented and benchmarked; iOS latency remains unmeasured — [Specification 019](specs/019-ui-tree-observation-performance/spec.md)
|
|
28
|
+
|
|
29
|
+
Measure physical hierarchy reads and their cost, then reduce the measured hot path while preserving fresh post-action and asynchronous UI evidence. Validate the change against the existing static journey and a controlled network-backed screen before claiming a speedup.
|
|
30
|
+
|
|
31
|
+
Exit criteria and performance gates are defined in Specification 019.
|
|
32
|
+
|
|
24
33
|
# Stronger State Verification
|
|
25
34
|
|
|
26
35
|
## Rationale
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# UI tree observation performance benchmark
|
|
2
|
+
|
|
3
|
+
This paired Android benchmark compares a clean, built instrumentation baseline
|
|
4
|
+
with a clean, built candidate. It runs the Spec 018 static journey and a local
|
|
5
|
+
network-backed loading-to-ready flow against the same fixture APK and emulator.
|
|
6
|
+
The network flow asserts a run-unique value from a physical tree read that
|
|
7
|
+
started after the local server sent its response.
|
|
8
|
+
|
|
9
|
+
Capture the instrumented baseline first from a clean, built baseline checkout:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
UI_TREE_BASELINE_ROOT=/path/to/instrumented-baseline \
|
|
13
|
+
LATENCY_DEVICE_ID=emulator-5554 \
|
|
14
|
+
npx tsx test/device/automated/ui-tree-performance/benchmark.ts --baseline-only
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This writes `baseline.json`. After measuring and implementing an optimization,
|
|
18
|
+
run the paired comparison from the clean, built candidate checkout:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
UI_TREE_BASELINE_ROOT=/path/to/instrumented-baseline \
|
|
22
|
+
UI_TREE_CANDIDATE_ROOT=/path/to/optimized-candidate \
|
|
23
|
+
LATENCY_DEVICE_ID=emulator-5554 \
|
|
24
|
+
npx tsx test/device/automated/ui-tree-performance/benchmark.ts
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Add `--smoke --baseline-only` for one attempt per flow while checking the
|
|
28
|
+
fixture and emulator connection. Smoke mode writes no performance report and
|
|
29
|
+
does not satisfy the sample-count or acceptance gates.
|
|
30
|
+
|
|
31
|
+
Both input checkouts must contain `dist/server.js` and have clean Git working
|
|
32
|
+
trees. The harness checks both before installing or resetting the fixture. It
|
|
33
|
+
builds one APK from the candidate fixture and uses that exact APK in both modes.
|
|
34
|
+
Each flow and mode receives three warm-ups and ten measured attempts; baseline
|
|
35
|
+
and candidate order alternates by pair.
|
|
36
|
+
|
|
37
|
+
The paired run writes `baseline.json`, `candidate.json`, `comparison.json`, and
|
|
38
|
+
`comparison.md` here. Timing events are enabled only in the spawned benchmark
|
|
39
|
+
servers through `MOBILE_DEBUG_MCP_UI_TREE_METRICS=1`; normal MCP responses do
|
|
40
|
+
not gain timing fields. Events contain timings, platform, generated journey step
|
|
41
|
+
IDs, and opaque acquisition IDs. They omit UI contents, app identifiers,
|
|
42
|
+
screenshots, logs, payloads, and device serials. iOS latency remains unmeasured.
|
|
43
|
+
|
|
44
|
+
The checked-in `initial-baseline.json` is the standalone capture taken before
|
|
45
|
+
the optimization. The checked-in `baseline.json` is the baseline side of the
|
|
46
|
+
paired run, so it shares the fixture APK hash with `candidate.json`.
|
|
47
|
+
`comparison-attempt-1.json` retains an earlier paired run with two visible
|
|
48
|
+
network failures; see `comparison.md` for the outcome and limitation.
|
|
49
|
+
The report commit SHAs are available locally as `codex/spec-019-baseline` and
|
|
50
|
+
`codex/spec-019-candidate`; create clean, built checkouts of those refs to
|
|
51
|
+
reproduce the paired measurement.
|
|
52
|
+
|
|
53
|
+
For the opt-in Android bridge, run
|
|
54
|
+
`LATENCY_DEVICE_ID=emulator-5554 node --import tsx test/device/automated/ui-tree-performance/bridge-probe.ts`.
|
|
55
|
+
Its `bridge-probe.json` compares ten bridge and legacy reads, checks a network
|
|
56
|
+
update, and checks a timer that changes visually without emitting an
|
|
57
|
+
accessibility event. The timer check catches a stale accessibility cache even
|
|
58
|
+
when every bridge request calls `getRootInActiveWindow()`.
|