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
5 changes: 3 additions & 2 deletions docs/operations/mobile-app-store-screenshots.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ The default matrix is:
| ----------------------------- | ------------------------- | ----------------- | ----------------------------------------- |
| `apple/iphone-6.9/dark/` | iPhone 17 Pro Max | 1320×2868 | App Store Connect iPhone 6.9-inch |
| `apple/iphone-6.5/dark/` | disposable iPhone 14 Plus | 1284×2778 | App Store Connect iPhone 6.5-inch |
| `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2064×2752 | App Store Connect iPad 13-inch |
| `apple/ipad-13/dark/` | iPad Pro 13-inch (M5) | 2752×2064 | App Store Connect iPad 13-inch, landscape |
| `google-play/phone/dark/` | Pixel AVD at 420 dpi | 1080×1920 | Google Play phone, portrait 9:16 |
| `google-play/tablet-7/dark/` | Pixel AVD at 600dp width | 1080×1920 | Google Play 7-inch tablet, portrait 9:16 |
| `google-play/tablet-10/dark/` | Pixel AVD at 800dp width | 1440×2560 | Google Play 10-inch tablet, portrait 9:16 |
Expand All @@ -86,7 +86,8 @@ A light-only run writes the same tree under `light/`; `--appearance both` writes
folders.

Edit [mobile-showcase.config.ts](../../scripts/mobile-showcase.config.ts) to change simulator or AVD
names, light/dark appearance, scenes, output directory, capture delay, Android ABI, or viewport.
names, light/dark appearance, iOS orientation, scenes, output directory, capture delay, Android ABI,
or viewport.

## Capture in GitHub Actions

Expand Down
7 changes: 5 additions & 2 deletions scripts/mobile-showcase.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface ShowcaseIosDevice {
readonly simulatorDeviceType?: string;
/** Appearance used when the CLI does not pass --appearance. */
readonly appearance: ShowcaseAppearance;
/** Orientation applied by the capture harness. Defaults to portrait. */
readonly orientation?: "portrait" | "landscape";
readonly scenes: ReadonlyArray<ShowcaseScene>;
readonly storeAsset: ShowcaseStoreAssetSpec;
}
Expand Down Expand Up @@ -122,12 +124,13 @@ const config: ShowcaseConfig = {
simulator: "iPad Pro 13-inch (M5)",
simulatorDeviceType: "com.apple.CoreSimulator.SimDeviceType.iPad-Pro-13-inch-M5-16GB",
appearance: "dark",
orientation: "landscape",
scenes: ["thread", "terminal", "review", "threads", "environments"],
storeAsset: {
store: "apple",
directory: "apple/ipad-13",
width: 2064,
height: 2752,
width: 2752,
height: 2064,
minimumUploadCount: 1,
maximumUploadCount: 10,
},
Expand Down
13 changes: 7 additions & 6 deletions scripts/mobile-showcase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,17 +216,18 @@ it("configures every default device with an exact upload-ready store target", ()
assert.deepStrictEqual(
showcaseConfig.devices.map((device) => [
device.id,
device.platform === "ios" ? (device.orientation ?? "portrait") : null,
device.storeAsset.directory,
device.storeAsset.width,
device.storeAsset.height,
]),
[
["iphone-6.9", "apple/iphone-6.9", 1320, 2868],
["iphone-6.5", "apple/iphone-6.5", 1284, 2778],
["ipad-13", "apple/ipad-13", 2064, 2752],
["pixel", "google-play/phone", 1080, 1920],
["android-tablet-7", "google-play/tablet-7", 1080, 1920],
["android-tablet-10", "google-play/tablet-10", 1440, 2560],
["iphone-6.9", "portrait", "apple/iphone-6.9", 1320, 2868],
["iphone-6.5", "portrait", "apple/iphone-6.5", 1284, 2778],
["ipad-13", "landscape", "apple/ipad-13", 2752, 2064],
["pixel", null, "google-play/phone", 1080, 1920],
["android-tablet-7", null, "google-play/tablet-7", 1080, 1920],
["android-tablet-10", null, "google-play/tablet-10", 1440, 2560],
],
);
});
Expand Down
62 changes: 61 additions & 1 deletion scripts/mobile-showcase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,11 @@ export interface ShowcaseCapture {
}

interface IosCaptureCleanup {
readonly name: string;
readonly udid: string;
readonly startedByRunner: boolean;
readonly createdByRunner: boolean;
readonly restorePortrait: boolean;
}

interface AndroidCaptureCleanup {
Expand Down Expand Up @@ -775,6 +777,49 @@ async function normalizeIosSimulator(appearance: ShowcaseAppearance, udid: strin
]);
}

async function setIosSimulatorOrientation(
orientation: NonNullable<ShowcaseIosDevice["orientation"]>,
simulator: Pick<SimctlDevice, "name" | "udid">,
): Promise<void> {
await runCommand("open", ["-a", "Simulator", "--args", "-CurrentDeviceUDID", simulator.udid]);
const menuItem = orientation === "landscape" ? "Landscape Right" : "Portrait";
await runCommand("osascript", [
"-e",
"on run argv",
"-e",
"set simulatorName to item 1 of argv",
"-e",
'tell application "Simulator" to activate',
"-e",
'tell application "System Events" to tell process "Simulator"',
"-e",
"set simulatorWindows to {}",
"-e",
"repeat 40 times",
"-e",
'set simulatorWindows to menu items of menu "Window" of menu bar item "Window" of menu bar 1 whose name starts with simulatorName',
"-e",
"if (count of simulatorWindows) is greater than 0 then exit repeat",
"-e",
"delay 0.25",
"-e",
"end repeat",
"-e",
'if (count of simulatorWindows) is not 1 then error "Expected exactly one Simulator window for " & simulatorName',
"-e",
"click item 1 of simulatorWindows",
"-e",
`click menu item "${menuItem}" of menu "Orientation" of menu item "Orientation" of menu "Device" of menu bar item "Device" of menu bar 1`,
"-e",
"end tell",
"-e",
"delay 1",
"-e",
"end run",
simulator.name,
Comment thread
PixPMusic marked this conversation as resolved.
]);
}
Comment thread
PixPMusic marked this conversation as resolved.

async function iosAppContainer(udid: string): Promise<string> {
return (
await commandOutput("xcrun", ["simctl", "get_app_container", udid, ANDROID_PACKAGE, "data"])
Expand Down Expand Up @@ -811,7 +856,13 @@ async function captureIos(
): Promise<void> {
const { simulator, createdByRunner } = await ensureIosSimulator(capture.device);
const startedByRunner = simulator.state !== "Booted";
registerCleanup({ udid: simulator.udid, startedByRunner, createdByRunner });
registerCleanup({
name: simulator.name,
udid: simulator.udid,
startedByRunner,
createdByRunner,
restorePortrait: capture.device.orientation === "landscape",
});
if (!startedByRunner) {
// Clear transient SpringBoard state (permission prompts, stale URL-open
// confirmations, keyboards) without erasing the developer's simulator.
Expand Down Expand Up @@ -870,6 +921,9 @@ async function captureIos(
"--showcaseScene",
firstScene,
]);
if (capture.device.orientation === "landscape") {
await setIosSimulatorOrientation("landscape", simulator);
}
};
await NodeFSP.rm(readyPath, { force: true });
await NodeFSP.writeFile(scenePath, firstScene);
Expand Down Expand Up @@ -900,6 +954,9 @@ async function captureIos(
`${scene}.png`,
);
await runCommand("xcrun", ["simctl", "io", simulator.udid, "screenshot", destination]);
if (capture.device.orientation === "landscape") {
await runCommand("sips", ["--rotate", "90", destination]);
}
await finalizeCapture(destination, capture.device);
}
}
Expand Down Expand Up @@ -1315,6 +1372,9 @@ async function main(): Promise<void> {
}
}
for (const cleanup of iosCleanups) {
if (cleanup.restorePortrait) {
await setIosSimulatorOrientation("portrait", cleanup).catch(() => undefined);
}
if (cleanup.startedByRunner || cleanup.createdByRunner) {
await runCommand("xcrun", ["simctl", "shutdown", cleanup.udid]).catch(() => undefined);
}
Expand Down
Loading