From 164727c9a1bd19e2206df919262903f4a4a3958f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 2 Jun 2026 20:45:31 +0100 Subject: [PATCH 1/2] fix(ui-automation): Preserve AXe tab roles Bundle AXe 1.7.1 so SwiftUI tab bar accessibility nodes are exposed through the packaged AXe artifact. Also classify AXe radio button nodes with role_description "tab" as runtime tabs so wait-for-ui and tap selectors can target them by role. Fixes GH-439 Co-Authored-By: OpenAI Codex --- .axe-version | 2 +- .../__tests__/runtime-snapshot.test.ts | 25 +++++++++++++++++++ .../ui-automation/shared/runtime-snapshot.ts | 3 +++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/.axe-version b/.axe-version index bd8bf882..943f9cbc 100644 --- a/.axe-version +++ b/.axe-version @@ -1 +1 @@ -1.7.0 +1.7.1 diff --git a/src/mcp/tools/ui-automation/__tests__/runtime-snapshot.test.ts b/src/mcp/tools/ui-automation/__tests__/runtime-snapshot.test.ts index 92987efe..b6254399 100644 --- a/src/mcp/tools/ui-automation/__tests__/runtime-snapshot.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/runtime-snapshot.test.ts @@ -142,6 +142,31 @@ describe('runtime snapshot normalization', () => { expect(snapshot.payload.elements[0]?.actions).not.toContain('tap'); }); + it('classifies tab radio buttons from AXe as tabs', () => { + const snapshot = createRuntimeSnapshotRecord({ + simulatorId, + uiHierarchy: [ + createNode({ + type: 'RadioButton', + role: 'AXRadioButton', + role_description: 'tab', + AXLabel: 'Reports', + AXValue: '0', + }), + ], + nowMs: 1_000, + }); + + expect(snapshot.payload.elements[0]).toEqual( + expect.objectContaining({ + role: 'tab', + label: 'Reports', + value: '0', + actions: expect.arrayContaining(['tap', 'longPress', 'touch']), + }), + ); + }); + it('derives deterministic screen hashes from normalized UI content', () => { const uiHierarchy = [createNode({ AXLabel: 'Continue' }), createNode({ AXLabel: 'Cancel' })]; diff --git a/src/mcp/tools/ui-automation/shared/runtime-snapshot.ts b/src/mcp/tools/ui-automation/shared/runtime-snapshot.ts index 89e49546..9ced5d17 100644 --- a/src/mcp/tools/ui-automation/shared/runtime-snapshot.ts +++ b/src/mcp/tools/ui-automation/shared/runtime-snapshot.ts @@ -108,6 +108,9 @@ function deriveRole( node: AccessibilityNode, identifier: string | undefined, ): RuntimeElementRoleV1 | undefined { + const roleDescription = normalizeText(node.role_description)?.toLowerCase(); + if (roleDescription === 'tab') return 'tab'; + const roleText = [node.role, node.type, node.subrole, node.role_description] .map((value) => normalizeText(value)?.toLowerCase()) .filter((value): value is string => value !== undefined) From be5a05f62218981d94217b48e425e90f87571a1f Mon Sep 17 00:00:00 2001 From: Cameron Cooke Date: Tue, 2 Jun 2026 20:52:43 +0100 Subject: [PATCH 2/2] test(ui-automation): Stabilize key sequence snapshot polling Inject deterministic post-action snapshot timing into the key sequence executor test. This keeps the exact polling assertion stable under CI scheduler jitter. Co-Authored-By: OpenAI Codex --- .../__tests__/key_sequence.test.ts | 18 +++++++++++++++++- src/mcp/tools/ui-automation/key_sequence.ts | 7 ++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts index 2833b31d..c7971950 100644 --- a/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts +++ b/src/mcp/tools/ui-automation/__tests__/key_sequence.test.ts @@ -12,6 +12,17 @@ import { simulatorId, } from './ui-action-test-helpers.ts'; +function createImmediatePostActionTiming() { + let nowMs = 0; + + return { + now: () => nowMs, + sleep: async (durationMs: number) => { + nowMs += durationMs; + }, + }; +} + describe('Key Sequence Tool', () => { beforeEach(() => { sessionStore.clear(); @@ -194,7 +205,12 @@ describe('Key Sequence Tool', () => { describe('Handler Behavior (Complete Literal Returns)', () => { it('captures a fresh runtime snapshot after a successful key sequence', async () => { const { calls, executor } = createTrackingExecutor(); - const executeKeySequence = createKeySequenceExecutor(executor, createMockAxeHelpers()); + const executeKeySequence = createKeySequenceExecutor( + executor, + createMockAxeHelpers(), + undefined, + createImmediatePostActionTiming(), + ); const result = await executeKeySequence({ simulatorId, keyCodes: [40, 42, 44] }); diff --git a/src/mcp/tools/ui-automation/key_sequence.ts b/src/mcp/tools/ui-automation/key_sequence.ts index b5fa5054..018c1f56 100644 --- a/src/mcp/tools/ui-automation/key_sequence.ts +++ b/src/mcp/tools/ui-automation/key_sequence.ts @@ -22,7 +22,10 @@ import { clearRuntimeSnapshot, withSimulatorUiAutomationTransaction, } from './shared/snapshot-ui-state.ts'; -import { captureRuntimeSnapshotAfterActionSafely } from './shared/post-action-snapshot.ts'; +import { + captureRuntimeSnapshotAfterActionSafely, + type PostActionSnapshotTiming, +} from './shared/post-action-snapshot.ts'; import type { AxeHelpers } from './shared/axe-command.ts'; import type { NonStreamingExecutor } from '../../../types/tool-execution.ts'; import type { UiActionResultDomainResult } from '../../../types/domain-results.ts'; @@ -57,6 +60,7 @@ export function createKeySequenceExecutor( executor: CommandExecutor, axeHelpers: AxeHelpers = defaultAxeHelpers, debuggerManager: DebuggerManager = getDefaultDebuggerManager(), + postActionSnapshotTiming?: PostActionSnapshotTiming, ): NonStreamingExecutor { return async (params) => withSimulatorUiAutomationTransaction(params.simulatorId, async () => { @@ -91,6 +95,7 @@ export function createKeySequenceExecutor( simulatorId, executor, axeHelpers, + timing: postActionSnapshotTiming, }); return createUiActionSuccessResult( action,