Skip to content

Commit d0a4fff

Browse files
authored
Fix landscape screenshot orientation (#186)
1 parent 4821534 commit d0a4fff

4 files changed

Lines changed: 735 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
- Fix tool loading bugs in static tool registration.
2222
- Fix xcodemake command argument corruption when project directory path appears as substring in non-path arguments.
2323
- Fix snapshot_ui warning state being isolated per UI automation tool, causing false warnings.
24+
- Fixed screenshot tool capturing rotated images when simulator is in landscape orientation by detecting window dimensions and applying +90° rotation to correct the framebuffer capture. ([`#186`](https://github.com/cameroncooke/XcodeBuildMCP/pull/186) by [`@VincentStark`](https://github.com/VincentStark))
2425

2526
## [1.14.0] - 2025-09-22
2627
- Add video capture tool for simulators

src/mcp/tools/simulator/__tests__/screenshot.test.ts

Lines changed: 132 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createMockExecutor,
1111
createMockFileSystemExecutor,
1212
createCommandMatchingMockExecutor,
13+
mockProcess,
1314
} from '../../../../test-utils/mock-executors.ts';
1415
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
1516
import { SystemError } from '../../../../utils/responses/index.ts';
@@ -48,18 +49,34 @@ describe('screenshot plugin', () => {
4849
});
4950

5051
describe('Command Generation', () => {
52+
// Mock device list JSON for proper device name lookup
53+
const mockDeviceListJson = JSON.stringify({
54+
devices: {
55+
'com.apple.CoreSimulator.SimRuntime.iOS-17-2': [
56+
{ udid: 'test-uuid', name: 'iPhone 15 Pro', state: 'Booted' },
57+
{ udid: 'another-uuid', name: 'iPhone 15', state: 'Booted' },
58+
],
59+
},
60+
});
61+
5162
it('should generate correct simctl and sips commands', async () => {
5263
const capturedCommands: string[][] = [];
5364

54-
const mockExecutor = createCommandMatchingMockExecutor({
55-
'xcrun simctl': { success: true, output: 'Screenshot saved' },
56-
sips: { success: true, output: 'Image optimized' },
57-
});
58-
59-
// Wrap to capture both commands
65+
// Wrap to capture commands and return appropriate mock responses
6066
const capturingExecutor = async (command: string[], ...args: any[]) => {
6167
capturedCommands.push(command);
62-
return mockExecutor(command, ...args);
68+
const cmdStr = command.join(' ');
69+
// Return device list JSON for list command
70+
if (cmdStr.includes('simctl list devices')) {
71+
return {
72+
success: true,
73+
output: mockDeviceListJson,
74+
error: undefined,
75+
process: mockProcess,
76+
};
77+
}
78+
// Return success for all other commands
79+
return { success: true, output: '', error: undefined, process: mockProcess };
6380
};
6481

6582
const mockFileSystemExecutor = createMockFileSystemExecutor({
@@ -85,8 +102,8 @@ describe('screenshot plugin', () => {
85102
mockUuidDeps,
86103
);
87104

88-
// Should execute both commands in sequence
89-
expect(capturedCommands).toHaveLength(2);
105+
// Should execute all commands in sequence: screenshot, list devices, orientation detection, optimization
106+
expect(capturedCommands).toHaveLength(4);
90107

91108
// First command: xcrun simctl screenshot
92109
expect(capturedCommands[0]).toEqual([
@@ -98,8 +115,17 @@ describe('screenshot plugin', () => {
98115
'/tmp/screenshot_mock-uuid-123.png',
99116
]);
100117

101-
// Second command: sips optimization
102-
expect(capturedCommands[1]).toEqual([
118+
// Second command: xcrun simctl list devices (to get device name)
119+
expect(capturedCommands[1][0]).toBe('xcrun');
120+
expect(capturedCommands[1][1]).toBe('simctl');
121+
expect(capturedCommands[1][2]).toBe('list');
122+
123+
// Third command: swift orientation detection
124+
expect(capturedCommands[2][0]).toBe('swift');
125+
expect(capturedCommands[2][1]).toBe('-e');
126+
127+
// Fourth command: sips optimization
128+
expect(capturedCommands[3]).toEqual([
103129
'sips',
104130
'-Z',
105131
'800',
@@ -118,15 +144,21 @@ describe('screenshot plugin', () => {
118144
it('should generate correct path with different uuid', async () => {
119145
const capturedCommands: string[][] = [];
120146

121-
const mockExecutor = createCommandMatchingMockExecutor({
122-
'xcrun simctl': { success: true, output: 'Screenshot saved' },
123-
sips: { success: true, output: 'Image optimized' },
124-
});
125-
126-
// Wrap to capture both commands
147+
// Wrap to capture commands and return appropriate mock responses
127148
const capturingExecutor = async (command: string[], ...args: any[]) => {
128149
capturedCommands.push(command);
129-
return mockExecutor(command, ...args);
150+
const cmdStr = command.join(' ');
151+
// Return device list JSON for list command
152+
if (cmdStr.includes('simctl list devices')) {
153+
return {
154+
success: true,
155+
output: mockDeviceListJson,
156+
error: undefined,
157+
process: mockProcess,
158+
};
159+
}
160+
// Return success for all other commands
161+
return { success: true, output: '', error: undefined, process: mockProcess };
130162
};
131163

132164
const mockFileSystemExecutor = createMockFileSystemExecutor({
@@ -152,8 +184,8 @@ describe('screenshot plugin', () => {
152184
mockUuidDeps,
153185
);
154186

155-
// Should execute both commands in sequence
156-
expect(capturedCommands).toHaveLength(2);
187+
// Should execute all commands in sequence: screenshot, list devices, orientation detection, optimization
188+
expect(capturedCommands).toHaveLength(4);
157189

158190
// First command: xcrun simctl screenshot
159191
expect(capturedCommands[0]).toEqual([
@@ -165,8 +197,17 @@ describe('screenshot plugin', () => {
165197
'/tmp/screenshot_different-uuid-456.png',
166198
]);
167199

168-
// Second command: sips optimization
169-
expect(capturedCommands[1]).toEqual([
200+
// Second command: xcrun simctl list devices (to get device name)
201+
expect(capturedCommands[1][0]).toBe('xcrun');
202+
expect(capturedCommands[1][1]).toBe('simctl');
203+
expect(capturedCommands[1][2]).toBe('list');
204+
205+
// Third command: swift orientation detection
206+
expect(capturedCommands[2][0]).toBe('swift');
207+
expect(capturedCommands[2][1]).toBe('-e');
208+
209+
// Fourth command: sips optimization
210+
expect(capturedCommands[3]).toEqual([
170211
'sips',
171212
'-Z',
172213
'800',
@@ -185,15 +226,21 @@ describe('screenshot plugin', () => {
185226
it('should use default dependencies when not provided', async () => {
186227
const capturedCommands: string[][] = [];
187228

188-
const mockExecutor = createCommandMatchingMockExecutor({
189-
'xcrun simctl': { success: true, output: 'Screenshot saved' },
190-
sips: { success: true, output: 'Image optimized' },
191-
});
192-
193-
// Wrap to capture both commands
229+
// Wrap to capture commands and return appropriate mock responses
194230
const capturingExecutor = async (command: string[], ...args: any[]) => {
195231
capturedCommands.push(command);
196-
return mockExecutor(command, ...args);
232+
const cmdStr = command.join(' ');
233+
// Return device list JSON for list command
234+
if (cmdStr.includes('simctl list devices')) {
235+
return {
236+
success: true,
237+
output: mockDeviceListJson,
238+
error: undefined,
239+
process: mockProcess,
240+
};
241+
}
242+
// Return success for all other commands
243+
return { success: true, output: '', error: undefined, process: mockProcess };
197244
};
198245

199246
const mockFileSystemExecutor = createMockFileSystemExecutor({
@@ -208,8 +255,8 @@ describe('screenshot plugin', () => {
208255
mockFileSystemExecutor,
209256
);
210257

211-
// Should execute both commands in sequence
212-
expect(capturedCommands).toHaveLength(2);
258+
// Should execute all commands in sequence: screenshot, list devices, orientation detection, optimization
259+
expect(capturedCommands).toHaveLength(4);
213260

214261
// First command should be generated with real os.tmpdir, path.join, and uuidv4
215262
const firstCommand = capturedCommands[0];
@@ -221,14 +268,23 @@ describe('screenshot plugin', () => {
221268
expect(firstCommand[4]).toBe('screenshot');
222269
expect(firstCommand[5]).toMatch(/\/.*\/screenshot_.*\.png/);
223270

224-
// Second command should be sips optimization
225-
const secondCommand = capturedCommands[1];
226-
expect(secondCommand[0]).toBe('sips');
227-
expect(secondCommand[1]).toBe('-Z');
228-
expect(secondCommand[2]).toBe('800');
271+
// Second command should be xcrun simctl list devices
272+
expect(capturedCommands[1][0]).toBe('xcrun');
273+
expect(capturedCommands[1][1]).toBe('simctl');
274+
expect(capturedCommands[1][2]).toBe('list');
275+
276+
// Third command should be swift orientation detection
277+
expect(capturedCommands[2][0]).toBe('swift');
278+
expect(capturedCommands[2][1]).toBe('-e');
279+
280+
// Fourth command should be sips optimization
281+
const thirdCommand = capturedCommands[3];
282+
expect(thirdCommand[0]).toBe('sips');
283+
expect(thirdCommand[1]).toBe('-Z');
284+
expect(thirdCommand[2]).toBe('800');
229285
// Should have proper PNG input and JPG output paths
230-
expect(secondCommand[secondCommand.length - 3]).toMatch(/\/.*\/screenshot_.*\.png/);
231-
expect(secondCommand[secondCommand.length - 1]).toMatch(/\/.*\/screenshot_optimized_.*\.jpg/);
286+
expect(thirdCommand[thirdCommand.length - 3]).toMatch(/\/.*\/screenshot_.*\.png/);
287+
expect(thirdCommand[thirdCommand.length - 1]).toMatch(/\/.*\/screenshot_optimized_.*\.jpg/);
232288
});
233289
});
234290

@@ -370,15 +426,31 @@ describe('screenshot plugin', () => {
370426
it('should call correct command with direct execution', async () => {
371427
const capturedArgs: any[][] = [];
372428

373-
const mockExecutor = createCommandMatchingMockExecutor({
374-
'xcrun simctl': { success: true, output: 'Screenshot saved' },
375-
sips: { success: true, output: 'Image optimized' },
429+
// Mock device list JSON for proper device name lookup
430+
const mockDeviceListJson = JSON.stringify({
431+
devices: {
432+
'com.apple.CoreSimulator.SimRuntime.iOS-17-2': [
433+
{ udid: 'test-uuid', name: 'iPhone 15 Pro', state: 'Booted' },
434+
],
435+
},
376436
});
377437

378-
// Wrap to capture both command executions
438+
// Capture all command executions and return appropriate mock responses
379439
const capturingExecutor: CommandExecutor = async (...args) => {
380440
capturedArgs.push(args);
381-
return mockExecutor(...args);
441+
const command = args[0] as string[];
442+
const cmdStr = command.join(' ');
443+
// Return device list JSON for list command
444+
if (cmdStr.includes('simctl list devices')) {
445+
return {
446+
success: true,
447+
output: mockDeviceListJson,
448+
error: undefined,
449+
process: mockProcess,
450+
};
451+
}
452+
// Return success for all other commands
453+
return { success: true, output: '', error: undefined, process: mockProcess };
382454
};
383455

384456
const mockFileSystemExecutor = createMockFileSystemExecutor({
@@ -404,8 +476,8 @@ describe('screenshot plugin', () => {
404476
mockUuidDeps,
405477
);
406478

407-
// Should capture both command executions
408-
expect(capturedArgs).toHaveLength(2);
479+
// Should capture all command executions: screenshot, list devices, orientation detection, optimization
480+
expect(capturedArgs).toHaveLength(4);
409481

410482
// First call: xcrun simctl screenshot (3 args: command, logPrefix, useShell)
411483
expect(capturedArgs[0]).toEqual([
@@ -414,8 +486,21 @@ describe('screenshot plugin', () => {
414486
false,
415487
]);
416488

417-
// Second call: sips optimization (3 args: command, logPrefix, useShell)
418-
expect(capturedArgs[1]).toEqual([
489+
// Second call: xcrun simctl list devices (to get device name)
490+
expect(capturedArgs[1][0][0]).toBe('xcrun');
491+
expect(capturedArgs[1][0][1]).toBe('simctl');
492+
expect(capturedArgs[1][0][2]).toBe('list');
493+
expect(capturedArgs[1][1]).toBe('[Screenshot]: list devices');
494+
expect(capturedArgs[1][2]).toBe(false);
495+
496+
// Third call: swift orientation detection
497+
expect(capturedArgs[2][0][0]).toBe('swift');
498+
expect(capturedArgs[2][0][1]).toBe('-e');
499+
expect(capturedArgs[2][1]).toBe('[Screenshot]: detect orientation');
500+
expect(capturedArgs[2][2]).toBe(false);
501+
502+
// Fourth call: sips optimization (3 args: command, logPrefix, useShell)
503+
expect(capturedArgs[3]).toEqual([
419504
[
420505
'sips',
421506
'-Z',

0 commit comments

Comments
 (0)