Skip to content

Commit a2d8777

Browse files
cameroncookecodex
andcommitted
fix(ui-automation): Preserve runtime snapshot visibility contracts
Keep non-streaming UI action tests anchored to explicit success text for every action case instead of accepting any non-empty output. Avoid re-adding swipe actions to containers already marked offscreen by viewport visibility filtering, and cover that ordering with a regression test. Co-Authored-By: Codex <noreply@openai.com>
1 parent 89358fa commit a2d8777

3 files changed

Lines changed: 50 additions & 5 deletions

File tree

src/mcp/tools/ui-automation/__tests__/non_streaming_progress.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ describe('ui automation non-streaming tools', () => {
8585
axeHelpers,
8686
);
8787
},
88+
expectedText: 'Long press on elementRef e1 for 1500ms simulated successfully.',
8889
},
8990
{
9091
name: 'swipe',
@@ -97,6 +98,7 @@ describe('ui automation non-streaming tools', () => {
9798
axeHelpers,
9899
);
99100
},
101+
expectedText: 'Swipe up within elementRef e1 simulated successfully.',
100102
},
101103
{
102104
name: 'tap',
@@ -109,6 +111,7 @@ describe('ui automation non-streaming tools', () => {
109111
axeHelpers,
110112
);
111113
},
114+
expectedText: 'Tap on elementRef e1 simulated successfully.',
112115
},
113116
{
114117
name: 'touch',
@@ -121,6 +124,7 @@ describe('ui automation non-streaming tools', () => {
121124
axeHelpers,
122125
);
123126
},
127+
expectedText: 'Touch event (touch down) on elementRef e1 executed successfully.',
124128
},
125129
{
126130
name: 'type_text',
@@ -141,11 +145,7 @@ describe('ui automation non-streaming tools', () => {
141145
const { result } = await runToolLogic(testCase.run);
142146
expect(result.events, `${testCase.name} should not emit progress events`).toEqual([]);
143147
expect(result.isError()).toBe(false);
144-
if (testCase.expectedText) {
145-
expect(result.text()).toContain(testCase.expectedText);
146-
} else {
147-
expect(result.text().trim().length).toBeGreaterThan(0);
148-
}
148+
expect(result.text()).toContain(testCase.expectedText);
149149
}
150150
});
151151

src/mcp/tools/ui-automation/__tests__/runtime-snapshot.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,50 @@ describe('runtime snapshot normalization', () => {
316316
);
317317
});
318318

319+
it('does not re-add swipeWithin to offscreen containers', () => {
320+
const root = createNode({
321+
type: 'Application',
322+
role: 'AXApplication',
323+
frame: { x: 0, y: 0, width: 390, height: 844 },
324+
children: [
325+
createNode({
326+
type: 'Other',
327+
role: 'AXGroup',
328+
AXLabel: 'Offscreen panel',
329+
frame: { x: 0, y: 900, width: 300, height: 200 },
330+
children: [
331+
createNode({
332+
type: 'StaticText',
333+
role: 'AXStaticText',
334+
AXLabel: 'Overflowing child',
335+
frame: { x: 10, y: 1160, width: 100, height: 20 },
336+
}),
337+
],
338+
}),
339+
],
340+
});
341+
342+
const snapshot = createRuntimeSnapshotRecord({
343+
simulatorId,
344+
uiHierarchy: [root],
345+
nowMs: 1_000,
346+
});
347+
348+
expect(snapshot.payload.elements[1]).toEqual(
349+
expect.objectContaining({
350+
role: 'other',
351+
label: 'Offscreen panel',
352+
state: expect.objectContaining({ visible: false }),
353+
actions: [],
354+
}),
355+
);
356+
expect(snapshot.payload.actions).not.toContainEqual({
357+
action: 'swipeWithin',
358+
elementRef: 'e2',
359+
label: 'Offscreen panel',
360+
});
361+
});
362+
319363
it('removes point-based actions from clipped elements with offscreen activation points', () => {
320364
const root = createNode({
321365
type: 'Application',

src/mcp/tools/ui-automation/shared/runtime-snapshot.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,7 @@ function inferScrollableContainers(elements: RuntimeSnapshotElementRecord[]): vo
407407
const { publicElement, metadata } = element;
408408
if (
409409
!isContainerRole(publicElement.role) ||
410+
publicElement.state?.visible === false ||
410411
!isVisible(publicElement.frame) ||
411412
!isLargeEnoughInferredScrollContainer(publicElement.role, publicElement.frame)
412413
) {

0 commit comments

Comments
 (0)