Skip to content

Commit 7e91100

Browse files
yi-jiang-applovincameroncooke
authored andcommitted
fix(simulator-management): tighten keyboard window matching
1 parent 24a5896 commit 7e91100

2 files changed

Lines changed: 61 additions & 2 deletions

File tree

src/mcp/tools/simulator-management/__tests__/_keyboard_shortcut.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,20 @@ const SHUTDOWN_JSON = JSON.stringify({
2222
});
2323

2424
const EMPTY_JSON = JSON.stringify({ devices: {} });
25+
const ESCAPED_NAME_JSON = JSON.stringify({
26+
devices: {
27+
'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
28+
{ udid: 'escaped-uuid', name: 'Test\\Device"', state: 'Booted' },
29+
],
30+
},
31+
});
32+
const PREFIX_NAME_JSON = JSON.stringify({
33+
devices: {
34+
'com.apple.CoreSimulator.SimRuntime.iOS-17-0': [
35+
{ udid: 'prefix-uuid', name: 'iPhone 15', state: 'Booted' },
36+
],
37+
},
38+
});
2539

2640
type Call = { command: string[] };
2741

@@ -87,6 +101,36 @@ describe('sendKeyboardShortcut', () => {
87101
expect(keystrokeScript).toContain('shift down');
88102
});
89103

104+
it('escapes backslashes before embedding simulator names in the focus AppleScript', async () => {
105+
const { executor, calls } = makeFifoExecutor([
106+
{ success: true, output: ESCAPED_NAME_JSON },
107+
{ success: true, output: '' },
108+
{ success: true, output: 'OK' },
109+
{ success: true, output: '' },
110+
]);
111+
112+
const result = await sendKeyboardShortcut('escaped-uuid', 'software-keyboard', executor);
113+
114+
expect(result.success).toBe(true);
115+
expect(calls[2].command[2]).toContain('Test\\\\Device\\"');
116+
});
117+
118+
it('matches the simulator window by exact title or runtime suffix instead of substring contains', async () => {
119+
const { executor, calls } = makeFifoExecutor([
120+
{ success: true, output: PREFIX_NAME_JSON },
121+
{ success: true, output: '' },
122+
{ success: true, output: 'OK' },
123+
{ success: true, output: '' },
124+
]);
125+
126+
const result = await sendKeyboardShortcut('prefix-uuid', 'software-keyboard', executor);
127+
128+
expect(result.success).toBe(true);
129+
expect(calls[2].command[2]).toContain('title is "iPhone 15"');
130+
expect(calls[2].command[2]).toContain('title starts with "iPhone 15 –"');
131+
expect(calls[2].command[2]).not.toContain('title contains');
132+
});
133+
90134
it('errors when simulator UUID is not found', async () => {
91135
const { executor, calls } = makeFifoExecutor([{ success: true, output: EMPTY_JSON }]);
92136

src/mcp/tools/simulator-management/_keyboard_shortcut.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ export type KeyboardShortcutResult = { success: true } | { success: false; error
88
type SimctlDevice = { udid: string; name: string; state: string };
99
type SimctlList = { devices: Record<string, SimctlDevice[]> };
1010

11+
function escapeAppleScriptStringLiteral(value: string): string {
12+
return value
13+
.replace(/\\/g, '\\\\')
14+
.replace(/"/g, '\\"')
15+
.replace(/\n/g, '\\n')
16+
.replace(/\r/g, '\\r')
17+
.replace(/\t/g, '\\t');
18+
}
19+
1120
function resolveDevice(list: SimctlList, simulatorId: string): SimctlDevice | undefined {
1221
for (const runtime in list.devices) {
1322
const found = list.devices[runtime]?.find((d) => d.udid === simulatorId);
@@ -17,12 +26,18 @@ function resolveDevice(list: SimctlList, simulatorId: string): SimctlDevice | un
1726
}
1827

1928
function buildFocusScript(deviceName: string): string {
20-
const safeName = deviceName.replace(/"/g, '\\"');
29+
const safeName = escapeAppleScriptStringLiteral(deviceName);
2130
return [
2231
'tell application "System Events"',
2332
' tell process "Simulator"',
2433
' set frontmost to true',
25-
' set matchingWindows to (every window whose title contains "' + safeName + '")',
34+
' set matchingWindows to (every window whose (title is "' +
35+
safeName +
36+
'" or title starts with "' +
37+
safeName +
38+
' –" or title starts with "' +
39+
safeName +
40+
' -"))',
2641
' if (count of matchingWindows) is 0 then',
2742
' return "NO_WINDOW"',
2843
' end if',

0 commit comments

Comments
 (0)