shots capture: signed-in web apps (shots.capture.auth request with ${cred:}/${env:} references or storageState), shots login saves a demo sign-in to .storebound/auth.json (0600), expired-session warning, secrets and cookies masked
This commit is contained in:
parent
3949ff1531
commit
e1e4e05237
15 changed files with 545 additions and 9 deletions
|
|
@ -11,6 +11,9 @@
|
|||
// size, session, the copy text of its language, the app build (shots.capture.build files, else
|
||||
// the HTML and scripts at baseUrl), fonts.css). Unchanged frames that exist are skipped;
|
||||
// --only-missing only captures frames that do not exist; --force captures everything.
|
||||
// auth shots.capture.auth (auth.ts): every new context is signed in before its first shot (request
|
||||
// auth through the context's request API, or a storageState file); a shot that lands on a login
|
||||
// page warns; resolved secrets and cookie values are masked in every log line and the report.
|
||||
// timing per frame: goto, waitFor, eval, delay, screenshot, total, in the progress line and in
|
||||
// .storebound/capture-report.json, with the slowest frames and steps at the end.
|
||||
import { createHash } from 'node:crypto';
|
||||
|
|
@ -19,6 +22,7 @@ import { availableParallelism, cpus } from 'node:os';
|
|||
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 { FrameDims } from './lib';
|
||||
|
||||
|
|
@ -132,7 +136,10 @@ 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> }): Promise<CaptureReport> {
|
||||
export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[], o: { concurrency: number; dims: Record<Device, FrameDims>; auth?: AuthPlan }): Promise<CaptureReport> {
|
||||
const auth = o.auth;
|
||||
const mask = (t: string): string => (auth ? maskSecrets(t, auth.secrets) : t);
|
||||
const expired = new Set<string>();
|
||||
const started = Date.now();
|
||||
const perLang = cap.session === 'per-language';
|
||||
const report: CaptureReport['frames'] = [];
|
||||
|
|
@ -140,7 +147,9 @@ export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[]
|
|||
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 });
|
||||
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 ?? {}) });
|
||||
// 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();
|
||||
const errs: string[] = [];
|
||||
page.on('pageerror', (e: Error) => errs.push(e.message));
|
||||
|
|
@ -152,6 +161,11 @@ export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[]
|
|||
if (how === 'goto') await s.page.goto(j.url, { waitUntil: 'domcontentloaded', timeout: 90000 });
|
||||
else await s.page.evaluate(`(p => { if (location.pathname + location.search !== p) { history.pushState(null, '', p); dispatchEvent(new PopStateEvent('popstate', { state: null })); } })(${JSON.stringify(j.path)})`);
|
||||
t.goto = Date.now() - t0; t0 = Date.now();
|
||||
if (auth && landedOnLogin(j.url, s.page.url(), auth.loginUrl)) {
|
||||
// a sign-in page is never a store frame: warn once per login URL, fail this shot now (no waitFor timeout)
|
||||
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())}`);
|
||||
}
|
||||
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);
|
||||
|
|
@ -166,6 +180,8 @@ export async function captureWeb(browser: any, cap: ShotsCapture, jobs: WebJob[]
|
|||
};
|
||||
const record = (j: WebJob, r: { ok: boolean; reused: boolean; timing: Timing; error?: string; errs?: string[] }): void => {
|
||||
finished++;
|
||||
if (r.error) r.error = mask(r.error);
|
||||
if (r.errs) r.errs = r.errs.map(mask);
|
||||
report.push({ frame: j.frame, ok: r.ok, reused: r.reused, timing: r.timing, ...(r.error ? { error: r.error } : {}) });
|
||||
const head = `${progressLine(finished, total, started)} ${j.lang}/${j.device}/${j.name}`;
|
||||
if (r.ok) log('shots', `${head}.png ${r.reused ? '(reused page) ' : ''}${timingText(r.timing)}${r.errs?.length ? ' (page errors: ' + r.errs.join(' | ').slice(0, 200) + ')' : ''}`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue