validate: removed-from-sale IAPs are no blockers; shots capture --device landscape (Mac / Chromebook frames from a web build); playwright lookup says why each place failed
This commit is contained in:
parent
cdd9321794
commit
d0f568ebc2
13 changed files with 230 additions and 48 deletions
|
|
@ -1,9 +1,15 @@
|
|||
// Raw frames from a running build of your app or game, from any engine.
|
||||
// storebound shots capture [--lang en,tr] [--only hero,shop] [--device phone|tablet] [--source web|ios-sim|android-emu|folder]
|
||||
// storebound shots capture [--lang en,tr] [--only hero,shop] [--device phone|tablet|landscape] [--source web|ios-sim|android-emu|folder]
|
||||
// [--dry [--probe]] [--keep-booted] [--concurrency N] [--only-missing | --force]
|
||||
// Frames go to <shots.frames>/<lang>/<device>/<name>.png: the screen BELOW the status bar (the template draws one).
|
||||
// Devices: phone, tablet and landscape (<lang>/landscape/<name>.png, web source only: Mac App Store and Chromebook
|
||||
// screenshots; a desktop window, no status bar crop). The default device list adds landscape when the project renders a
|
||||
// landscape size (apple.mac, play.chromebook, shots.landscape.playTablets); then shots with no device (and 'both') are
|
||||
// captured for landscape too, since render needs a landscape frame per scene. A shot says device: 'landscape' to be
|
||||
// landscape only, 'phone' / 'tablet' to stay out of it. ios-sim / android-emu skip landscape with a warning.
|
||||
// shots.capture.source (default web):
|
||||
// web headless Chromium (playwright), mobile viewport + touch at phone / tablet size. Each shot:
|
||||
// web headless Chromium (playwright), mobile viewport + touch at phone / tablet size, or a desktop viewport (no touch)
|
||||
// at shots.landscape.size (default 1440 x 900 @2x) for landscape. Each shot:
|
||||
// { path, waitFor?: JS expression that turns true, eval?: [JS], delayMs?, device? }; URL = baseUrl + path +
|
||||
// query, "{lang}" replaced. Parallel contexts, per-language page reuse, the input-hash cache and the
|
||||
// per-frame timing (goto / waitFor / eval / delay / screenshot): capture-web.ts. Signed-in apps:
|
||||
|
|
@ -36,15 +42,16 @@ import { done, log, step, warn } from '../lib/log';
|
|||
import { closeBrowser, launch } from './browser';
|
||||
import { checkFrameFolder } from './check';
|
||||
import { adbAppLocaleArgs, adbDemoExitArgs, adbDemoModeArgs, adbForceStopArgs, adbInstallArgs, adbScreencapArgs, adbStartArgs, adbStateArgs, adbTarget,
|
||||
cropArgs, cropTopFor, fill, pickSimulator, shotsFor, simBootArgs, simBootStatusArgs, simInstallArgs, simLaunchArgs, simOpenUrlArgs,
|
||||
cropArgs, cropTopFor, fill, CAPTURE_DEVICES, type CaptureDevice, pickSimulator, shotsFor, simBootArgs, simBootStatusArgs, simInstallArgs, simLaunchArgs, simOpenUrlArgs,
|
||||
simScreenshotArgs, simShutdownArgs, simStatusBarArgs, simStatusBarClearArgs, simTarget, SOURCES, type Source, TOOL_HINTS, validateCapture } from './devices';
|
||||
import { type CopyFile, copyText, type FrameDims, frameDims } from './lib';
|
||||
import { type CopyFile, copyText, type FrameDims, frameDims, landscapeDims, rendersLandscape } from './lib';
|
||||
import { captureWeb, defaultConcurrency, probeSelectors, hashPaths, hashServed, planWeb, readCache, selectJobs, summaryLines, writeCaptureState } from './capture-web';
|
||||
import { imageInfo } from './spec';
|
||||
import { authPlan, safeUrl } from './auth';
|
||||
import * as rt from '../lib/rt';
|
||||
|
||||
type Device = 'phone' | 'tablet';
|
||||
type Device = CaptureDevice;
|
||||
type PhoneTablet = 'phone' | 'tablet';
|
||||
|
||||
export function sh(cmd: string, args: string[], o: { allowFail?: boolean; timeout?: number } = {}): { out: Buffer; err: string; code: number } {
|
||||
const r = spawnSync(cmd, args, { timeout: o.timeout ?? 120_000, maxBuffer: 256 * 1024 * 1024 });
|
||||
|
|
@ -75,7 +82,7 @@ export function localeOf(config: ShipConfig, lang: string, source: Source): stri
|
|||
}
|
||||
|
||||
/** Crop the status bar off a full-screen capture and store it as a 24-bit RGB frame. */
|
||||
function storeFrame(tmp: string, out: string, cropTop: ShotsCapture['cropTop'], device: Device, dims: FrameDims): void {
|
||||
function storeFrame(tmp: string, out: string, cropTop: ShotsCapture['cropTop'], device: PhoneTablet, dims: FrameDims): void {
|
||||
const { w, h } = imageInfo(readFileSync(tmp));
|
||||
if (!h) throw new Error('screenshot is not a PNG');
|
||||
const top = cropTopFor(cropTop, device, h, dims);
|
||||
|
|
@ -98,13 +105,20 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (problems.length) throw new Error(`shots.capture:\n ${problems.join('\n ')}`);
|
||||
const langs = a.list('lang').length ? a.list('lang') : config.languages.map(l => l.id);
|
||||
const only = a.list('only');
|
||||
const devices: Device[] = a.get('device') ? [a.get('device') as Device] : ['phone', 'tablet'];
|
||||
if (devices.some(d => d !== 'phone' && d !== 'tablet')) throw new Error('--device: phone | tablet');
|
||||
if (a.get('device') && !CAPTURE_DEVICES.includes(a.get('device') as Device)) throw new Error(`--device: ${CAPTURE_DEVICES.join(' | ')}`);
|
||||
const landscapeOn = rendersLandscape(config);
|
||||
let devices: Device[] = a.get('device') ? [a.get('device') as Device] : ['phone', 'tablet', ...(landscapeOn ? ['landscape' as const] : [])];
|
||||
const allLandscape = landscapeOn || a.get('device') === 'landscape'; // shots with no device / 'both' also make a landscape frame
|
||||
if (source !== 'web' && source !== 'folder' && devices.includes('landscape')) {
|
||||
warn('shots', `landscape frames (Mac, Chromebook) are not supported from ${source}: skipped; capture them from a web build (--source web) or put them in <shots.frames>/<lang>/landscape/ (--source folder)`);
|
||||
devices = devices.filter(d => d !== 'landscape');
|
||||
}
|
||||
const mobile = devices.filter((d): d is PhoneTablet => d !== 'landscape');
|
||||
const framesDir = resolve(shots.frames);
|
||||
const dims = frameDims(config);
|
||||
const dims = { ...frameDims(config), landscape: landscapeDims(config) };
|
||||
const tmpDir = join(root, '.storebound', 'tmp');
|
||||
const target = (lang: string, device: Device, name: string) => { const d = join(framesDir, lang, device); if (!dry) mkdirSync(d, { recursive: true }); return join(d, `${name}.png`); };
|
||||
const total = langs.length * devices.reduce((t, d) => t + shotsFor(cap, d, only).length, 0);
|
||||
const total = langs.length * devices.reduce((t, d) => t + shotsFor(cap, d, only, allLandscape).length, 0);
|
||||
let failed = 0, n = 0, i = 0;
|
||||
const progress = (what: string) => log('shots', step(++i, total, what));
|
||||
/** Runs a simctl / adb step, or prints it with --dry. */
|
||||
|
|
@ -125,7 +139,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
|
||||
if (source === 'web') {
|
||||
if (dry) {
|
||||
for (const lang of langs) for (const device of devices) for (const [name, s] of shotsFor(cap, device, only)) {
|
||||
for (const lang of langs) for (const device of devices) for (const [name, s] of shotsFor(cap, device, only, allLandscape)) {
|
||||
progress(`${lang}/${device}/${name}.png <- ${cap.baseUrl}${(s.path ?? '/').replaceAll('{lang}', lang)}`);
|
||||
const sel: [string, string[]][] = [['waitSelector', candidates(s.waitSelector, device)], ...(Array.isArray(s.click) && s.click.some(c => typeof c !== 'string') ? s.click : s.click === undefined ? [] : [s.click]).map((c, k) => [`click[${k}]`, candidates(c as ShotSelector, device)] as [string, string[]])];
|
||||
for (const [what, list] of sel) if (list.length) log('shots', ` ${what} on ${device} ${dims[device].w}x${dims[device].h}: ${list.join(' | ')}`);
|
||||
|
|
@ -133,7 +147,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (a.has('probe')) { // open the pages and test every candidate on each size (needs the app running at baseUrl)
|
||||
need(cap.baseUrl, 'shots.capture.baseUrl');
|
||||
const auth = cap.auth ? authPlan(cap.auth, { root, baseUrl: cap.baseUrl! }) : undefined;
|
||||
const jobs = planWeb(cap, { langs, devices, only, dims, framesDir, build: '', fonts: '' });
|
||||
const jobs = planWeb(cap, { langs, devices, only, dims, framesDir, build: '', fonts: '', allLandscape });
|
||||
const browser = await launch(root, { headless: true, args: ['--mute-audio'] });
|
||||
let r;
|
||||
try { r = await probeSelectors(browser, jobs, { dims, ...(auth ? { auth } : {}) }); } finally { await closeBrowser(browser); }
|
||||
|
|
@ -152,7 +166,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
let copy: CopyFile = {};
|
||||
try { copy = JSON.parse(readFileSync(resolve(shots.copy), 'utf8')) as CopyFile; } catch { /* no copy yet */ }
|
||||
const fontsCss = join(root, '.storebound', 'fonts', 'fonts.css');
|
||||
const jobs = planWeb(cap, { langs, devices, only, dims, framesDir, build, copyText: l => copyText(l, copy[l]),
|
||||
const jobs = planWeb(cap, { langs, devices, only, dims, framesDir, build, allLandscape, copyText: l => copyText(l, copy[l]),
|
||||
fonts: existsSync(fontsCss) ? readFileSync(fontsCss, 'utf8') : '' });
|
||||
const cache = readCache(root);
|
||||
const { run: todo, skipped } = selectJobs(jobs, cache, a.has('force') ? 'force' : a.has('only-missing') ? 'only-missing' : 'changed');
|
||||
|
|
@ -174,7 +188,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const ios = need(cap.ios, 'shots.capture.ios');
|
||||
if (!dry) { requireTool('xcrun'); requireTool('ffmpeg'); }
|
||||
const list = dry && !rt.which('xcrun') ? '{"devices":{}}' : sh('xcrun', ['simctl', 'list', 'devices', '-j']).out.toString();
|
||||
for (const device of devices) {
|
||||
for (const device of mobile) {
|
||||
const want = simTarget(ios, device);
|
||||
const todo = shotsFor(cap, device, only);
|
||||
if (!todo.length) continue;
|
||||
|
|
@ -222,7 +236,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const and = need(cap.android, 'shots.capture.android');
|
||||
const adb = adbPath();
|
||||
if (!dry) { requireTool(adb, 'adb'); requireTool('ffmpeg'); }
|
||||
for (const device of devices) {
|
||||
for (const device of mobile) {
|
||||
const todo = shotsFor(cap, device, only);
|
||||
if (!todo.length) continue;
|
||||
const serial = adbTarget(and, device);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue