Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Fix tool loading bugs in static tool registration.
- Fix xcodemake command argument corruption when project directory path appears as substring in non-path arguments.
- Fix snapshot_ui warning state being isolated per UI automation tool, causing false warnings.
- 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))

## [1.14.0] - 2025-09-22
- Add video capture tool for simulators
Expand Down
179 changes: 132 additions & 47 deletions src/mcp/tools/simulator/__tests__/screenshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
createMockExecutor,
createMockFileSystemExecutor,
createCommandMatchingMockExecutor,
mockProcess,
} from '../../../../test-utils/mock-executors.ts';
import type { CommandExecutor } from '../../../../utils/execution/index.ts';
import { SystemError } from '../../../../utils/responses/index.ts';
Expand Down Expand Up @@ -48,18 +49,34 @@ describe('screenshot plugin', () => {
});

describe('Command Generation', () => {
// Mock device list JSON for proper device name lookup
const mockDeviceListJson = JSON.stringify({
devices: {
'com.apple.CoreSimulator.SimRuntime.iOS-17-2': [
{ udid: 'test-uuid', name: 'iPhone 15 Pro', state: 'Booted' },
{ udid: 'another-uuid', name: 'iPhone 15', state: 'Booted' },
],
},
});

it('should generate correct simctl and sips commands', async () => {
const capturedCommands: string[][] = [];

const mockExecutor = createCommandMatchingMockExecutor({
'xcrun simctl': { success: true, output: 'Screenshot saved' },
sips: { success: true, output: 'Image optimized' },
});

// Wrap to capture both commands
// Wrap to capture commands and return appropriate mock responses
const capturingExecutor = async (command: string[], ...args: any[]) => {
capturedCommands.push(command);
return mockExecutor(command, ...args);
const cmdStr = command.join(' ');
// Return device list JSON for list command
if (cmdStr.includes('simctl list devices')) {
return {
success: true,
output: mockDeviceListJson,
error: undefined,
process: mockProcess,
};
}
// Return success for all other commands
return { success: true, output: '', error: undefined, process: mockProcess };
};

const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -85,8 +102,8 @@ describe('screenshot plugin', () => {
mockUuidDeps,
);

// Should execute both commands in sequence
expect(capturedCommands).toHaveLength(2);
// Should execute all commands in sequence: screenshot, list devices, orientation detection, optimization
expect(capturedCommands).toHaveLength(4);

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

// Second command: sips optimization
expect(capturedCommands[1]).toEqual([
// Second command: xcrun simctl list devices (to get device name)
expect(capturedCommands[1][0]).toBe('xcrun');
expect(capturedCommands[1][1]).toBe('simctl');
expect(capturedCommands[1][2]).toBe('list');

// Third command: swift orientation detection
expect(capturedCommands[2][0]).toBe('swift');
expect(capturedCommands[2][1]).toBe('-e');

// Fourth command: sips optimization
expect(capturedCommands[3]).toEqual([
'sips',
'-Z',
'800',
Expand All @@ -118,15 +144,21 @@ describe('screenshot plugin', () => {
it('should generate correct path with different uuid', async () => {
const capturedCommands: string[][] = [];

const mockExecutor = createCommandMatchingMockExecutor({
'xcrun simctl': { success: true, output: 'Screenshot saved' },
sips: { success: true, output: 'Image optimized' },
});

// Wrap to capture both commands
// Wrap to capture commands and return appropriate mock responses
const capturingExecutor = async (command: string[], ...args: any[]) => {
capturedCommands.push(command);
return mockExecutor(command, ...args);
const cmdStr = command.join(' ');
// Return device list JSON for list command
if (cmdStr.includes('simctl list devices')) {
return {
success: true,
output: mockDeviceListJson,
error: undefined,
process: mockProcess,
};
}
// Return success for all other commands
return { success: true, output: '', error: undefined, process: mockProcess };
};

const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -152,8 +184,8 @@ describe('screenshot plugin', () => {
mockUuidDeps,
);

// Should execute both commands in sequence
expect(capturedCommands).toHaveLength(2);
// Should execute all commands in sequence: screenshot, list devices, orientation detection, optimization
expect(capturedCommands).toHaveLength(4);

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

// Second command: sips optimization
expect(capturedCommands[1]).toEqual([
// Second command: xcrun simctl list devices (to get device name)
expect(capturedCommands[1][0]).toBe('xcrun');
expect(capturedCommands[1][1]).toBe('simctl');
expect(capturedCommands[1][2]).toBe('list');

// Third command: swift orientation detection
expect(capturedCommands[2][0]).toBe('swift');
expect(capturedCommands[2][1]).toBe('-e');

// Fourth command: sips optimization
expect(capturedCommands[3]).toEqual([
'sips',
'-Z',
'800',
Expand All @@ -185,15 +226,21 @@ describe('screenshot plugin', () => {
it('should use default dependencies when not provided', async () => {
const capturedCommands: string[][] = [];

const mockExecutor = createCommandMatchingMockExecutor({
'xcrun simctl': { success: true, output: 'Screenshot saved' },
sips: { success: true, output: 'Image optimized' },
});

// Wrap to capture both commands
// Wrap to capture commands and return appropriate mock responses
const capturingExecutor = async (command: string[], ...args: any[]) => {
capturedCommands.push(command);
return mockExecutor(command, ...args);
const cmdStr = command.join(' ');
// Return device list JSON for list command
if (cmdStr.includes('simctl list devices')) {
return {
success: true,
output: mockDeviceListJson,
error: undefined,
process: mockProcess,
};
}
// Return success for all other commands
return { success: true, output: '', error: undefined, process: mockProcess };
};

const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -208,8 +255,8 @@ describe('screenshot plugin', () => {
mockFileSystemExecutor,
);

// Should execute both commands in sequence
expect(capturedCommands).toHaveLength(2);
// Should execute all commands in sequence: screenshot, list devices, orientation detection, optimization
expect(capturedCommands).toHaveLength(4);

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

// Second command should be sips optimization
const secondCommand = capturedCommands[1];
expect(secondCommand[0]).toBe('sips');
expect(secondCommand[1]).toBe('-Z');
expect(secondCommand[2]).toBe('800');
// Second command should be xcrun simctl list devices
expect(capturedCommands[1][0]).toBe('xcrun');
expect(capturedCommands[1][1]).toBe('simctl');
expect(capturedCommands[1][2]).toBe('list');

// Third command should be swift orientation detection
expect(capturedCommands[2][0]).toBe('swift');
expect(capturedCommands[2][1]).toBe('-e');

// Fourth command should be sips optimization
const thirdCommand = capturedCommands[3];
expect(thirdCommand[0]).toBe('sips');
expect(thirdCommand[1]).toBe('-Z');
expect(thirdCommand[2]).toBe('800');
// Should have proper PNG input and JPG output paths
expect(secondCommand[secondCommand.length - 3]).toMatch(/\/.*\/screenshot_.*\.png/);
expect(secondCommand[secondCommand.length - 1]).toMatch(/\/.*\/screenshot_optimized_.*\.jpg/);
expect(thirdCommand[thirdCommand.length - 3]).toMatch(/\/.*\/screenshot_.*\.png/);
expect(thirdCommand[thirdCommand.length - 1]).toMatch(/\/.*\/screenshot_optimized_.*\.jpg/);
});
});

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

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

// Wrap to capture both command executions
// Capture all command executions and return appropriate mock responses
const capturingExecutor: CommandExecutor = async (...args) => {
capturedArgs.push(args);
return mockExecutor(...args);
const command = args[0] as string[];
const cmdStr = command.join(' ');
// Return device list JSON for list command
if (cmdStr.includes('simctl list devices')) {
return {
success: true,
output: mockDeviceListJson,
error: undefined,
process: mockProcess,
};
}
// Return success for all other commands
return { success: true, output: '', error: undefined, process: mockProcess };
};

const mockFileSystemExecutor = createMockFileSystemExecutor({
Expand All @@ -404,8 +476,8 @@ describe('screenshot plugin', () => {
mockUuidDeps,
);

// Should capture both command executions
expect(capturedArgs).toHaveLength(2);
// Should capture all command executions: screenshot, list devices, orientation detection, optimization
expect(capturedArgs).toHaveLength(4);

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

// Second call: sips optimization (3 args: command, logPrefix, useShell)
expect(capturedArgs[1]).toEqual([
// Second call: xcrun simctl list devices (to get device name)
expect(capturedArgs[1][0][0]).toBe('xcrun');
expect(capturedArgs[1][0][1]).toBe('simctl');
expect(capturedArgs[1][0][2]).toBe('list');
expect(capturedArgs[1][1]).toBe('[Screenshot]: list devices');
expect(capturedArgs[1][2]).toBe(false);

// Third call: swift orientation detection
expect(capturedArgs[2][0][0]).toBe('swift');
expect(capturedArgs[2][0][1]).toBe('-e');
expect(capturedArgs[2][1]).toBe('[Screenshot]: detect orientation');
expect(capturedArgs[2][2]).toBe(false);

// Fourth call: sips optimization (3 args: command, logPrefix, useShell)
expect(capturedArgs[3]).toEqual([
[
'sips',
'-Z',
Expand Down
Loading