also looking at this
test(ios): install the Watch-enabled app and read the APNs environment it signs
#218
6 files
+248
-12
| @@ -0,0 +1,55 @@ | ||
| 1 | import { existsSync } from 'node:fs' | |
| 2 | import { join } from 'node:path' | |
| 3 | ||
| 4 | /** A plist as JSON, or null when it cannot be read. */ | |
| 5 | export type PlistReader = (path: string) => Record<string, unknown> | null | |
| 6 | ||
| 7 | /** | |
| 8 | * `plutil`, which reads the binary plists Xcode writes into a built bundle. | |
| 9 | * macOS only, like the builds whose output it reads. | |
| 10 | */ | |
| 11 | export const readPlistWithPlutil: PlistReader = (path) => { | |
| 12 | const converted = Bun.spawnSync(['plutil', '-convert', 'json', '-o', '-', path]) | |
| 13 | if (converted.exitCode !== 0) return null | |
| 14 | try { | |
| 15 | return JSON.parse(converted.stdout.toString()) as Record<string, unknown> | |
| 16 | } | |
| 17 | catch { | |
| 18 | return null | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | /** | |
| 23 | * Why a built iOS app's embedded Watch companion would not install, one line | |
| 24 | * per reason. | |
| 25 | * | |
| 26 | * Each line is a message `simctl install` gave for a generated app (#194, | |
| 27 | * #195), checked in the bundle first so a failure names the missing piece | |
| 28 | * rather than only quoting simctl. The install itself still runs afterwards; | |
| 29 | * this does not replace it. | |
| 30 | */ | |
| 31 | export function watchCompanionProblems(app: string, name: string, readPlist: PlistReader = readPlistWithPlutil): string[] { | |
| 32 | const watch = join(app, 'Watch', `${name}Watch.app`) | |
| 33 | if (!existsSync(watch)) | |
| 34 | return [`${name}.app embeds no Watch/${name}Watch.app`] | |
| 35 | ||
| 36 | const problems: string[] = [] | |
| 37 | const watchInfo = readPlist(join(watch, 'Info.plist')) | |
| 38 | if (!watchInfo) | |
| 39 | problems.push(`Watch/${name}Watch.app has no readable Info.plist`) | |
| 40 | else if (watchInfo.WKWatchKitApp !== true && watchInfo.WKApplication !== true) | |
| 41 | problems.push(`Watch/${name}Watch.app sets neither WKWatchKitApp nor WKApplication to true, so iOS will not install it (#194)`) | |
| 42 | ||
| 43 | const extension = join(watch, 'PlugIns', `${name}WatchExtension.appex`) | |
| 44 | if (!existsSync(extension)) { | |
| 45 | problems.push(`Watch/${name}Watch.app has no PlugIns/${name}WatchExtension.appex, so it is not a WatchKit 2 app (#195)`) | |
| 46 | return problems | |
| 47 | } | |
| 48 | ||
| 49 | const extensionInfo = readPlist(join(extension, 'Info.plist')) | |
| 50 | const point = (extensionInfo?.NSExtension as Record<string, unknown> | undefined)?.NSExtensionPointIdentifier | |
| 51 | if (point !== 'com.apple.watchkit') | |
| 52 | problems.push(`${name}WatchExtension.appex declares extension point ${JSON.stringify(point)}, not com.apple.watchkit`) | |
| 53 | ||
| 54 | return problems | |
| 55 | } | |