Skip to content

Commit 2472392

Browse files
committed
fix: apply app icon to iOS UI test runner
1 parent 34713df commit 2472392

11 files changed

Lines changed: 410 additions & 24 deletions

File tree

ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,6 @@
402402
20EA2EEE2F2CFC7C001CF0EF /* Debug */ = {
403403
isa = XCBuildConfiguration;
404404
buildSettings = {
405-
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
406-
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
407405
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements;
408406
CODE_SIGN_STYLE = Automatic;
409407
CURRENT_PROJECT_VERSION = 1;
@@ -430,8 +428,6 @@
430428
20EA2EEF2F2CFC7C001CF0EF /* Release */ = {
431429
isa = XCBuildConfiguration;
432430
buildSettings = {
433-
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
434-
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
435431
"CODE_SIGN_ENTITLEMENTS[sdk=macosx*]" = AgentDeviceRunnerUITests/AgentDeviceRunnerUITests.entitlements;
436432
CODE_SIGN_STYLE = Automatic;
437433
CURRENT_PROJECT_VERSION = 1;
-79.6 KB
Loading
-99.6 KB
Loading

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/Assets.xcassets/AppIcon.appiconset/Contents.json

Lines changed: 0 additions & 14 deletions
This file was deleted.

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/Assets.xcassets/Contents.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

scripts/build-xcuitest-apple.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,8 @@ xcodebuild build-for-testing \
104104
ENABLE_CODE_COVERAGE=NO \
105105
$SIGNING_BUILD_SETTINGS
106106

107+
node --experimental-strip-types --input-type=module -e '
108+
import { applyXctestRunnerAppIconFromDerivedPath } from "./src/platforms/ios/runner-icon.ts";
109+
await applyXctestRunnerAppIconFromDerivedPath(process.argv[1]);
110+
' "$DERIVED_PATH"
107111
node scripts/write-xcuitest-cache-metadata.mjs "$PLATFORM" "$DERIVED_PATH" "$DESTINATION"

scripts/write-xcuitest-cache-metadata.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ function isRunnerSourceFile(fileName, filePath) {
8585
return filePath.includes(`${path.sep}.xcodeproj${path.sep}`);
8686
}
8787
return [
88+
'.jpg',
89+
'.json',
90+
'.png',
8891
'.swift',
8992
'.plist',
9093
'.entitlements',
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import assert from 'node:assert/strict';
2+
import fs from 'node:fs';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
import { test } from 'vitest';
6+
7+
import { createLocalAppleToolProvider, withAppleToolProvider } from '../tool-provider.ts';
8+
import {
9+
applyXctestRunnerAppIcon,
10+
applyXctestRunnerAppIconFromDerivedPath,
11+
} from '../runner-icon.ts';
12+
13+
type AppleToolCall = [string, string[]];
14+
15+
const IPHONE_ICON_PLIST = {
16+
CFBundlePrimaryIcon: {
17+
CFBundleIconFiles: ['AppIcon60x60'],
18+
CFBundleIconName: 'AppIcon',
19+
},
20+
};
21+
22+
const IPAD_ICON_PLIST = {
23+
CFBundlePrimaryIcon: {
24+
CFBundleIconFiles: ['AppIcon76x76'],
25+
CFBundleIconName: 'AppIcon',
26+
},
27+
};
28+
29+
async function withTempDir<T>(prefix: string, fn: (root: string) => Promise<T> | T): Promise<T> {
30+
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
31+
try {
32+
return await fn(root);
33+
} finally {
34+
fs.rmSync(root, { recursive: true, force: true });
35+
}
36+
}
37+
38+
function applyPlutilMutation(args: string[]): void {
39+
const [operation, key, type, rawValue, plistPath] = args;
40+
assert.match(operation as string, /^-(insert|replace)$/);
41+
assert.equal(type, '-json');
42+
const plist = JSON.parse(fs.readFileSync(plistPath as string, 'utf8')) as Record<string, unknown>;
43+
plist[key as string] = JSON.parse(rawValue as string);
44+
fs.writeFileSync(plistPath as string, JSON.stringify(plist));
45+
}
46+
47+
function makeProductApps(root: string, configuration: string): [string, string] {
48+
const productsDir = path.join(root, 'Build', 'Products', configuration);
49+
const sourceAppPath = path.join(productsDir, 'AgentDeviceRunner.app');
50+
const runnerAppPath = path.join(productsDir, 'AgentDeviceRunnerUITests-Runner.app');
51+
fs.mkdirSync(sourceAppPath, { recursive: true });
52+
fs.mkdirSync(runnerAppPath, { recursive: true });
53+
return [sourceAppPath, runnerAppPath];
54+
}
55+
56+
function writeJsonPlist(appPath: string, value: Record<string, unknown>): void {
57+
fs.writeFileSync(path.join(appPath, 'Info.plist'), JSON.stringify(value));
58+
}
59+
60+
function createRecordingProvider(calls: AppleToolCall[], mutatePlist = false) {
61+
return createLocalAppleToolProvider({
62+
runCommand: async (cmd, args) => {
63+
calls.push([cmd, args]);
64+
if (mutatePlist && cmd === 'plutil') {
65+
applyPlutilMutation(args);
66+
}
67+
return { exitCode: 0, stdout: '', stderr: '' };
68+
},
69+
plist: {
70+
readJson: async (plistPath) => JSON.parse(fs.readFileSync(plistPath, 'utf8')),
71+
},
72+
});
73+
}
74+
75+
function expectedPlutilIconCalls(runnerAppPath: string): AppleToolCall[] {
76+
const plistPath = path.join(runnerAppPath, 'Info.plist');
77+
return [
78+
['plutil', ['-insert', 'CFBundleIcons', '-json', JSON.stringify(IPHONE_ICON_PLIST), plistPath]],
79+
[
80+
'plutil',
81+
['-insert', 'CFBundleIcons~ipad', '-json', JSON.stringify(IPAD_ICON_PLIST), plistPath],
82+
],
83+
];
84+
}
85+
86+
test('copies app icon artifacts into synthesized simulator XCTest runner app', async () => {
87+
await withTempDir('agent-device-runner-icon-', async (root) => {
88+
const [sourceAppPath, runnerAppPath] = makeProductApps(root, 'Debug-iphonesimulator');
89+
fs.writeFileSync(path.join(sourceAppPath, 'AppIcon60x60@2x.png'), 'icon');
90+
fs.writeFileSync(path.join(sourceAppPath, 'Assets.car'), 'catalog');
91+
writeJsonPlist(sourceAppPath, {
92+
CFBundleIcons: IPHONE_ICON_PLIST,
93+
'CFBundleIcons~ipad': IPAD_ICON_PLIST,
94+
});
95+
writeJsonPlist(runnerAppPath, { CFBundleName: 'XCTRunner' });
96+
97+
const calls: AppleToolCall[] = [];
98+
const provider = createRecordingProvider(calls, true);
99+
100+
await withAppleToolProvider(
101+
provider,
102+
async () => await applyXctestRunnerAppIcon([sourceAppPath, runnerAppPath]),
103+
);
104+
105+
assert.equal(fs.readFileSync(path.join(runnerAppPath, 'AppIcon60x60@2x.png'), 'utf8'), 'icon');
106+
assert.equal(fs.readFileSync(path.join(runnerAppPath, 'Assets.car'), 'utf8'), 'catalog');
107+
assert.deepEqual(JSON.parse(fs.readFileSync(path.join(runnerAppPath, 'Info.plist'), 'utf8')), {
108+
CFBundleName: 'XCTRunner',
109+
CFBundleIcons: IPHONE_ICON_PLIST,
110+
'CFBundleIcons~ipad': IPAD_ICON_PLIST,
111+
});
112+
assert.deepEqual(calls.slice(0, 2), expectedPlutilIconCalls(runnerAppPath));
113+
assert.deepEqual(calls.at(-1), [
114+
'codesign',
115+
['--force', '--sign', '-', '--timestamp=none', '--generate-entitlement-der', runnerAppPath],
116+
]);
117+
});
118+
});
119+
120+
test('skips signing when synthesized simulator XCTest runner app is already patched', async () => {
121+
await withTempDir('agent-device-runner-icon-', async (root) => {
122+
const [sourceAppPath, runnerAppPath] = makeProductApps(root, 'Debug-iphonesimulator');
123+
fs.writeFileSync(path.join(sourceAppPath, 'AppIcon60x60@2x.png'), 'icon');
124+
fs.writeFileSync(path.join(runnerAppPath, 'AppIcon60x60@2x.png'), 'icon');
125+
fs.writeFileSync(path.join(sourceAppPath, 'Assets.car'), 'catalog');
126+
fs.writeFileSync(path.join(runnerAppPath, 'Assets.car'), 'catalog');
127+
writeJsonPlist(sourceAppPath, { CFBundleIcons: IPHONE_ICON_PLIST });
128+
writeJsonPlist(runnerAppPath, { CFBundleName: 'XCTRunner', CFBundleIcons: IPHONE_ICON_PLIST });
129+
130+
const calls: AppleToolCall[] = [];
131+
const provider = createRecordingProvider(calls);
132+
133+
await withAppleToolProvider(
134+
provider,
135+
async () => await applyXctestRunnerAppIcon([sourceAppPath, runnerAppPath]),
136+
);
137+
138+
assert.deepEqual(calls, []);
139+
});
140+
});
141+
142+
test('finds simulator XCTest runner app from derived data', async () => {
143+
await withTempDir('agent-device-runner-icon-', async (root) => {
144+
const [sourceAppPath, runnerAppPath] = makeProductApps(root, 'Debug-iphonesimulator');
145+
fs.writeFileSync(path.join(sourceAppPath, 'AppIcon60x60@2x.png'), 'icon');
146+
writeJsonPlist(sourceAppPath, {});
147+
writeJsonPlist(runnerAppPath, {});
148+
149+
const provider = createLocalAppleToolProvider({
150+
runCommand: async () => ({ exitCode: 0, stdout: '', stderr: '' }),
151+
plist: {
152+
readJson: async (plistPath) => JSON.parse(fs.readFileSync(plistPath, 'utf8')),
153+
},
154+
});
155+
156+
await withAppleToolProvider(
157+
provider,
158+
async () => await applyXctestRunnerAppIconFromDerivedPath(root),
159+
);
160+
161+
assert.equal(fs.readFileSync(path.join(runnerAppPath, 'AppIcon60x60@2x.png'), 'utf8'), 'icon');
162+
});
163+
});
164+
165+
test('does not patch device XCTest runner apps', async () => {
166+
await withTempDir('agent-device-runner-icon-', async (root) => {
167+
const [sourceAppPath, runnerAppPath] = makeProductApps(root, 'Debug-iphoneos');
168+
fs.writeFileSync(path.join(sourceAppPath, 'AppIcon60x60@2x.png'), 'icon');
169+
170+
const calls: AppleToolCall[] = [];
171+
const provider = createRecordingProvider(calls);
172+
173+
await withAppleToolProvider(
174+
provider,
175+
async () => await applyXctestRunnerAppIcon([sourceAppPath, runnerAppPath]),
176+
);
177+
178+
assert.equal(fs.existsSync(path.join(runnerAppPath, 'AppIcon60x60@2x.png')), false);
179+
assert.deepEqual(calls, []);
180+
});
181+
});

0 commit comments

Comments
 (0)