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
|
|
@ -23,11 +23,15 @@ import { join } from 'node:path';
|
|||
import type { ShotSpec, ShotsCapture } from '../lib/config';
|
||||
import { log, warn } from '../lib/log';
|
||||
import { type AuthPlan, expiredHint, landedOnLogin, maskSecrets, safeUrl } from './auth';
|
||||
import { shotsFor } from './devices';
|
||||
import { type CaptureDevice, shotsFor } from './devices';
|
||||
import { applySelectors, candidates, probe } from './selectors';
|
||||
import type { FrameDims } from './lib';
|
||||
import { DEFAULT_LANDSCAPE, type FrameDims } from './lib';
|
||||
|
||||
type Device = 'phone' | 'tablet';
|
||||
/** Viewport per device class; landscape defaults to shots.landscape.size's default when a caller passes only phone / tablet. */
|
||||
export type CaptureDims = Record<'phone' | 'tablet', FrameDims> & { landscape?: FrameDims };
|
||||
const dimsOf = (dims: CaptureDims, device: Device): FrameDims => dims[device] ?? DEFAULT_LANDSCAPE;
|
||||
|
||||
type Device = CaptureDevice;
|
||||
export const STEPS = ['goto', 'waitFor', 'eval', 'delay', 'screenshot'] as const;
|
||||
export type Step = typeof STEPS[number];
|
||||
export type Timing = Record<Step | 'total', number>;
|
||||
|
|
@ -100,14 +104,14 @@ export async function hashServed(baseUrl: string): Promise<string> {
|
|||
}
|
||||
|
||||
/** Every web frame to capture, in a fixed order (lang, device, shot), with its URL and its input hash. */
|
||||
export function planWeb(cap: ShotsCapture, o: { langs: string[]; devices: Device[]; only: string[]; dims: Record<Device, FrameDims>; framesDir: string;
|
||||
build: string; copyText?: (lang: string) => string; fonts?: string }): WebJob[] {
|
||||
export function planWeb(cap: ShotsCapture, o: { langs: string[]; devices: Device[]; only: string[]; dims: CaptureDims; framesDir: string;
|
||||
build: string; copyText?: (lang: string) => string; fonts?: string; allLandscape?: boolean }): WebJob[] {
|
||||
const jobs: WebJob[] = [];
|
||||
for (const lang of o.langs) for (const device of o.devices) for (const [name, spec] of shotsFor(cap, device, o.only)) {
|
||||
for (const lang of o.langs) for (const device of o.devices) for (const [name, spec] of shotsFor(cap, device, o.only, o.allLandscape)) {
|
||||
const q = (cap.query ?? '').replaceAll('{lang}', lang);
|
||||
const path = (spec.path ?? '/').replaceAll('{lang}', lang) + (q ? ((spec.path ?? '/').includes('?') ? '&' : '?') + q : '');
|
||||
const url = (cap.baseUrl ?? '').replace(/\/$/, '') + path;
|
||||
const hash = sha(JSON.stringify({ spec, url, dims: o.dims[device], session: cap.session ?? 'per-shot', build: o.build, copy: o.copyText?.(lang) ?? '', fonts: o.fonts ?? '' }));
|
||||
const hash = sha(JSON.stringify({ spec, url, dims: dimsOf(o.dims, device), session: cap.session ?? 'per-shot', build: o.build, copy: o.copyText?.(lang) ?? '', fonts: o.fonts ?? '' }));
|
||||
const frame = `${lang}/${device}/${name}.png`;
|
||||
jobs.push({ lang, device, name, spec, url, path, out: join(o.framesDir, frame), frame, hash });
|
||||
}
|
||||
|
|
@ -137,7 +141,7 @@ export interface CaptureReport {
|
|||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/** Captures the jobs in a browser: per-shot contexts, or one page per language + device in per-language sessions. */
|
||||
export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[], o: { concurrency: number; dims: Record<Device, FrameDims>; auth?: AuthPlan }): Promise<CaptureReport> {
|
||||
export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[], o: { concurrency: number; dims: CaptureDims; auth?: AuthPlan }): Promise<CaptureReport> {
|
||||
const auth = o.auth;
|
||||
const mask = (t: string): string => (auth ? maskSecrets(t, auth.secrets) : t);
|
||||
const expired = new Set<string>();
|
||||
|
|
@ -147,8 +151,8 @@ export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[]
|
|||
let finished = 0;
|
||||
const total = jobs.length;
|
||||
const newPage = async (j: WebJob): Promise<{ c: any; page: any; errs: string[] }> => {
|
||||
const d = o.dims[j.device];
|
||||
const c = await browser.newContext({ viewport: { width: d.w, height: d.h }, deviceScaleFactor: d.dpr, hasTouch: true, isMobile: true, serviceWorkers: 'block', locale: j.lang, ...(auth?.contextOptions ?? {}) });
|
||||
const d = dimsOf(o.dims, j.device);
|
||||
const c = await browser.newContext({ viewport: { width: d.w, height: d.h }, deviceScaleFactor: d.dpr, hasTouch: j.device !== 'landscape', isMobile: j.device !== 'landscape', serviceWorkers: 'block', locale: j.lang, ...(auth?.contextOptions ?? {}) }); // landscape = a desktop window: no touch, no mobile emulation
|
||||
// sign in once per context, cookies kept in it (request auth); a failed sign-in stops the whole capture
|
||||
if (auth) try { await auth.prepare(c); } catch (e) { await c.close(); throw Object.assign(new Error(mask((e as Error).message)), { authFailed: true }); }
|
||||
const page = await c.newPage();
|
||||
|
|
@ -167,7 +171,7 @@ export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[]
|
|||
if (!expired.has(safeUrl(s.page.url()))) { expired.add(safeUrl(s.page.url())); warn('shots', `${j.lang}/${j.device}/${j.name}: ${mask(expiredHint(s.page.url()))}`); }
|
||||
throw new Error(`landed on the sign-in page ${safeUrl(s.page.url())}`);
|
||||
}
|
||||
await applySelectors(s.page, j.spec, { device: j.device, w: o.dims[j.device].w, h: o.dims[j.device].h }, waitTimeout); // waitSelector, click: a miss names the selector and the size
|
||||
await applySelectors(s.page, j.spec, { device: j.device, w: dimsOf(o.dims, j.device).w, h: dimsOf(o.dims, j.device).h }, waitTimeout); // waitSelector, click: a miss names the selector and the size
|
||||
if (j.spec.waitFor) await s.page.waitForFunction(j.spec.waitFor, null, { timeout: waitTimeout, polling: 50 });
|
||||
t.waitFor = Date.now() - t0; t0 = Date.now();
|
||||
for (const js of j.spec.eval ?? []) await s.page.evaluate(js);
|
||||
|
|
@ -251,14 +255,14 @@ export function writeCaptureState(root: string, report: CaptureReport, cache: Ca
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/** `shots capture --dry --probe`: opens each page (no screenshot), runs the shot's waitFor, and reports per size which
|
||||
* selector candidates match. Returns the log lines; `missed` counts shots where no candidate of a list matched. */
|
||||
export async function probeSelectors(browser: any, jobs: WebJob[], o: { dims: Record<Device, FrameDims>; auth?: AuthPlan; timeoutMs?: number }): Promise<{ lines: string[]; missed: number }> {
|
||||
export async function probeSelectors(browser: any, jobs: WebJob[], o: { dims: CaptureDims; auth?: AuthPlan; timeoutMs?: number }): Promise<{ lines: string[]; missed: number }> {
|
||||
const lines: string[] = []; let missed = 0;
|
||||
for (const j of jobs) {
|
||||
const lists = [['waitSelector', candidates(j.spec.waitSelector, j.device)], ...(Array.isArray(j.spec.click) && j.spec.click.some(c => typeof c !== 'string') ? j.spec.click : j.spec.click === undefined ? [] : [j.spec.click])
|
||||
.map((c, i) => [`click[${i}]`, candidates(c as any, j.device)] as const)].filter(([, l]) => l.length) as [string, string[]][];
|
||||
if (!lists.length) continue;
|
||||
const d = o.dims[j.device];
|
||||
const c = await browser.newContext({ viewport: { width: d.w, height: d.h }, deviceScaleFactor: 1, hasTouch: true, isMobile: true, serviceWorkers: 'block', locale: j.lang, ...(o.auth?.contextOptions ?? {}) });
|
||||
const d = dimsOf(o.dims, j.device);
|
||||
const c = await browser.newContext({ viewport: { width: d.w, height: d.h }, deviceScaleFactor: 1, hasTouch: j.device !== 'landscape', isMobile: j.device !== 'landscape', serviceWorkers: 'block', locale: j.lang, ...(o.auth?.contextOptions ?? {}) });
|
||||
try {
|
||||
if (o.auth) await o.auth.prepare(c);
|
||||
const page = await c.newPage();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue