Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
52 changes: 51 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,39 @@ 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 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 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 +846,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 +911,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 +944,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 +1362,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