Skip to content

Commit 46fdf65

Browse files
committed
refactor: migrate UI automation tools to event-based handler contract
1 parent ae0e2ac commit 46fdf65

25 files changed

+2645
-3766
lines changed

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

Lines changed: 162 additions & 182 deletions
Large diffs are not rendered by default.

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

Lines changed: 165 additions & 185 deletions
Large diffs are not rendered by default.

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

Lines changed: 153 additions & 149 deletions
Large diffs are not rendered by default.

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

Lines changed: 156 additions & 226 deletions
Large diffs are not rendered by default.

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

Lines changed: 160 additions & 197 deletions
Large diffs are not rendered by default.

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

Lines changed: 173 additions & 216 deletions
Large diffs are not rendered by default.
Lines changed: 109 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,43 @@
1-
/**
2-
* Tests for snapshot_ui tool plugin
3-
*/
4-
51
import { describe, it, expect } from 'vitest';
62
import * as z from 'zod';
73
import { createMockExecutor, createNoopExecutor } from '../../../../test-utils/mock-executors.ts';
84
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
95
import { schema, handler, snapshot_uiLogic } from '../snapshot_ui.ts';
106
import { AXE_NOT_AVAILABLE_MESSAGE } from '../../../../utils/axe-helpers.ts';
7+
import { allText, createMockToolHandlerContext } from '../../../../test-utils/test-helpers.ts';
8+
9+
const runLogic = async (logic: () => Promise<unknown>) => {
10+
const { result, run } = createMockToolHandlerContext();
11+
const response = await run(logic);
12+
13+
if (
14+
response &&
15+
typeof response === 'object' &&
16+
'content' in (response as Record<string, unknown>)
17+
) {
18+
return response as {
19+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
20+
isError?: boolean;
21+
nextStepParams?: unknown;
22+
};
23+
}
24+
25+
const text = result.text();
26+
const textContent = text.length > 0 ? [{ type: 'text' as const, text }] : [];
27+
const imageContent = result.attachments.map((attachment) => ({
28+
type: 'image' as const,
29+
data: attachment.data,
30+
mimeType: attachment.mimeType,
31+
}));
32+
33+
return {
34+
content: [...textContent, ...imageContent],
35+
isError: result.isError() ? true : undefined,
36+
nextStepParams: result.nextStepParams,
37+
attachments: result.attachments,
38+
text,
39+
};
40+
};
1141

1242
describe('Snapshot UI Plugin', () => {
1343
describe('Export Field Validation (Literal)', () => {
@@ -33,8 +63,8 @@ describe('Snapshot UI Plugin', () => {
3363
const result = await handler({});
3464

3565
expect(result.isError).toBe(true);
36-
expect(result.content[0].text).toContain('Missing required session defaults');
37-
expect(result.content[0].text).toContain('simulatorId is required');
66+
expect(allText(result)).toContain('Missing required session defaults');
67+
expect(allText(result)).toContain('simulatorId is required');
3868
});
3969

4070
it('should handle invalid simulatorId format via schema validation', async () => {
@@ -44,8 +74,8 @@ describe('Snapshot UI Plugin', () => {
4474
});
4575

4676
expect(result.isError).toBe(true);
47-
expect(result.content[0].text).toContain('Parameter validation failed');
48-
expect(result.content[0].text).toContain('Invalid Simulator UUID format');
77+
expect(allText(result)).toContain('Parameter validation failed');
78+
expect(allText(result)).toContain('Invalid Simulator UUID format');
4979
});
5080

5181
it('should return success for valid snapshot_ui execution', async () => {
@@ -63,10 +93,6 @@ describe('Snapshot UI Plugin', () => {
6393
const mockAxeHelpers = {
6494
getAxePath: () => '/usr/local/bin/axe',
6595
getBundledAxeEnvironment: () => ({}),
66-
createAxeNotAvailableResponse: () => ({
67-
content: [{ type: 'text' as const, text: 'axe not available' }],
68-
isError: true,
69-
}),
7096
};
7197

7298
// Wrap executor to track calls
@@ -76,12 +102,14 @@ describe('Snapshot UI Plugin', () => {
76102
return mockExecutor(...args);
77103
};
78104

79-
const result = await snapshot_uiLogic(
80-
{
81-
simulatorId: '12345678-1234-4234-8234-123456789012',
82-
},
83-
trackingExecutor,
84-
mockAxeHelpers,
105+
const result = await runLogic(() =>
106+
snapshot_uiLogic(
107+
{
108+
simulatorId: '12345678-1234-4234-8234-123456789012',
109+
},
110+
trackingExecutor,
111+
mockAxeHelpers,
112+
),
85113
);
86114

87115
expect(executorCalls[0]).toEqual([
@@ -91,22 +119,17 @@ describe('Snapshot UI Plugin', () => {
91119
{ env: {} },
92120
]);
93121

94-
expect(result).toEqual({
95-
content: [
96-
{
97-
type: 'text' as const,
98-
text: 'Accessibility hierarchy retrieved successfully:\n```json\n{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}\n```',
99-
},
100-
{
101-
type: 'text' as const,
102-
text: 'Tips:\n- Use frame coordinates for tap/swipe (center: x+width/2, y+height/2)\n- If a debugger is attached, ensure the app is running (not stopped on breakpoints)\n- Screenshots are for visual verification only',
103-
},
104-
],
105-
nextStepParams: {
106-
snapshot_ui: { simulatorId: '12345678-1234-4234-8234-123456789012' },
107-
tap: { simulatorId: '12345678-1234-4234-8234-123456789012', x: 0, y: 0 },
108-
screenshot: { simulatorId: '12345678-1234-4234-8234-123456789012' },
109-
},
122+
expect(result.isError).toBeFalsy();
123+
const text = allText(result);
124+
expect(text).toContain('Accessibility hierarchy retrieved successfully.');
125+
expect(text).toContain(
126+
'{"elements": [{"type": "Button", "frame": {"x": 100, "y": 200, "width": 50, "height": 30}}]}',
127+
);
128+
expect(text).toContain('Use frame coordinates for tap/swipe');
129+
expect(result.nextStepParams).toEqual({
130+
snapshot_ui: { simulatorId: '12345678-1234-4234-8234-123456789012' },
131+
tap: { simulatorId: '12345678-1234-4234-8234-123456789012', x: 0, y: 0 },
132+
screenshot: { simulatorId: '12345678-1234-4234-8234-123456789012' },
110133
});
111134
});
112135

@@ -115,34 +138,20 @@ describe('Snapshot UI Plugin', () => {
115138
const mockAxeHelpers = {
116139
getAxePath: () => null,
117140
getBundledAxeEnvironment: () => ({}),
118-
createAxeNotAvailableResponse: () => ({
119-
content: [
120-
{
121-
type: 'text' as const,
122-
text: AXE_NOT_AVAILABLE_MESSAGE,
123-
},
124-
],
125-
isError: true,
126-
}),
127141
};
128142

129-
const result = await snapshot_uiLogic(
130-
{
131-
simulatorId: '12345678-1234-4234-8234-123456789012',
132-
},
133-
createNoopExecutor(),
134-
mockAxeHelpers,
135-
);
136-
137-
expect(result).toEqual({
138-
content: [
143+
const result = await runLogic(() =>
144+
snapshot_uiLogic(
139145
{
140-
type: 'text' as const,
141-
text: AXE_NOT_AVAILABLE_MESSAGE,
146+
simulatorId: '12345678-1234-4234-8234-123456789012',
142147
},
143-
],
144-
isError: true,
145-
});
148+
createNoopExecutor(),
149+
mockAxeHelpers,
150+
),
151+
);
152+
153+
expect(result.isError).toBe(true);
154+
expect(allText(result)).toContain(AXE_NOT_AVAILABLE_MESSAGE);
146155
});
147156

148157
it('should handle AxeError from failed command execution', async () => {
@@ -157,29 +166,22 @@ describe('Snapshot UI Plugin', () => {
157166
const mockAxeHelpers = {
158167
getAxePath: () => '/usr/local/bin/axe',
159168
getBundledAxeEnvironment: () => ({}),
160-
createAxeNotAvailableResponse: () => ({
161-
content: [{ type: 'text' as const, text: 'axe not available' }],
162-
isError: true,
163-
}),
164169
};
165170

166-
const result = await snapshot_uiLogic(
167-
{
168-
simulatorId: '12345678-1234-4234-8234-123456789012',
169-
},
170-
mockExecutor,
171-
mockAxeHelpers,
172-
);
173-
174-
expect(result).toEqual({
175-
content: [
171+
const result = await runLogic(() =>
172+
snapshot_uiLogic(
176173
{
177-
type: 'text' as const,
178-
text: "Error: Failed to get accessibility hierarchy: axe command 'describe-ui' failed.\nDetails: axe command failed",
174+
simulatorId: '12345678-1234-4234-8234-123456789012',
179175
},
180-
],
181-
isError: true,
182-
});
176+
mockExecutor,
177+
mockAxeHelpers,
178+
),
179+
);
180+
181+
expect(result.isError).toBe(true);
182+
expect(allText(result)).toContain(
183+
"Failed to get accessibility hierarchy: axe command 'describe-ui' failed.",
184+
);
183185
});
184186

185187
it('should handle SystemError from command execution', async () => {
@@ -189,31 +191,19 @@ describe('Snapshot UI Plugin', () => {
189191
const mockAxeHelpers = {
190192
getAxePath: () => '/usr/local/bin/axe',
191193
getBundledAxeEnvironment: () => ({}),
192-
createAxeNotAvailableResponse: () => ({
193-
content: [{ type: 'text' as const, text: 'axe not available' }],
194-
isError: true,
195-
}),
196194
};
197195

198-
const result = await snapshot_uiLogic(
199-
{
200-
simulatorId: '12345678-1234-4234-8234-123456789012',
201-
},
202-
mockExecutor,
203-
mockAxeHelpers,
204-
);
205-
206-
expect(result).toEqual({
207-
content: [
196+
const result = await runLogic(() =>
197+
snapshot_uiLogic(
208198
{
209-
type: 'text' as const,
210-
text: expect.stringContaining(
211-
'Error: System error executing axe: Failed to execute axe command: ENOENT: no such file or directory',
212-
),
199+
simulatorId: '12345678-1234-4234-8234-123456789012',
213200
},
214-
],
215-
isError: true,
216-
});
201+
mockExecutor,
202+
mockAxeHelpers,
203+
),
204+
);
205+
206+
expect(result.isError).toBe(true);
217207
});
218208

219209
it('should handle unexpected Error objects', async () => {
@@ -223,31 +213,19 @@ describe('Snapshot UI Plugin', () => {
223213
const mockAxeHelpers = {
224214
getAxePath: () => '/usr/local/bin/axe',
225215
getBundledAxeEnvironment: () => ({}),
226-
createAxeNotAvailableResponse: () => ({
227-
content: [{ type: 'text' as const, text: 'axe not available' }],
228-
isError: true,
229-
}),
230216
};
231217

232-
const result = await snapshot_uiLogic(
233-
{
234-
simulatorId: '12345678-1234-4234-8234-123456789012',
235-
},
236-
mockExecutor,
237-
mockAxeHelpers,
238-
);
239-
240-
expect(result).toEqual({
241-
content: [
218+
const result = await runLogic(() =>
219+
snapshot_uiLogic(
242220
{
243-
type: 'text' as const,
244-
text: expect.stringContaining(
245-
'Error: System error executing axe: Failed to execute axe command: Unexpected error',
246-
),
221+
simulatorId: '12345678-1234-4234-8234-123456789012',
247222
},
248-
],
249-
isError: true,
250-
});
223+
mockExecutor,
224+
mockAxeHelpers,
225+
),
226+
);
227+
228+
expect(result.isError).toBe(true);
251229
});
252230

253231
it('should handle unexpected string errors', async () => {
@@ -257,29 +235,22 @@ describe('Snapshot UI Plugin', () => {
257235
const mockAxeHelpers = {
258236
getAxePath: () => '/usr/local/bin/axe',
259237
getBundledAxeEnvironment: () => ({}),
260-
createAxeNotAvailableResponse: () => ({
261-
content: [{ type: 'text' as const, text: 'axe not available' }],
262-
isError: true,
263-
}),
264238
};
265239

266-
const result = await snapshot_uiLogic(
267-
{
268-
simulatorId: '12345678-1234-4234-8234-123456789012',
269-
},
270-
mockExecutor,
271-
mockAxeHelpers,
272-
);
273-
274-
expect(result).toEqual({
275-
content: [
240+
const result = await runLogic(() =>
241+
snapshot_uiLogic(
276242
{
277-
type: 'text' as const,
278-
text: 'Error: System error executing axe: Failed to execute axe command: String error',
243+
simulatorId: '12345678-1234-4234-8234-123456789012',
279244
},
280-
],
281-
isError: true,
282-
});
245+
mockExecutor,
246+
mockAxeHelpers,
247+
),
248+
);
249+
250+
expect(result.isError).toBe(true);
251+
expect(allText(result)).toContain(
252+
'System error executing axe: Failed to execute axe command: String error',
253+
);
283254
});
284255
});
285256
});

0 commit comments

Comments
 (0)