teaser v7: 62 s, two new scenes (consoles: the real console plan of apple-app-privacy, then the app's plan panel stopping at Publish; resubmit: back from App Review, Resubmit), submit renewed as the app's own send-for-review (dry-run plan with Continue, then Waiting for review); v6's desktop scene folded into it; console plan lines checked against real output, cuts on the beat, image decodes batched with retries
This commit is contained in:
parent
710c3a53d0
commit
614c32bcba
8 changed files with 391 additions and 99 deletions
|
|
@ -18,8 +18,21 @@
|
|||
// "No rejection risks found" state; the fixture module is served with an empty findings list)
|
||||
// ready.jpg the same tab scrolled to "Store readiness" (validate, both stores) after the fixes: the
|
||||
// fixture's validate answer with no findings (the app's own "No blockers" state)
|
||||
// console-plan.jpg App info > Console steps > App Privacy > Plan: the console plan panel with the REAL plan
|
||||
// (`storebound console plan apple-app-privacy --json` run in a fresh `storebound init` folder,
|
||||
// served in place of the fixture's shortened plan), scrolled to the answer sheet's end and the
|
||||
// final button that stays the owner's (Publish) with the never-press list
|
||||
// console-plan-top.jpg the same panel at its top: the start page and the first steps
|
||||
// resubmit.jpg Tidy Lists' Releases > Send for review: back from App Review (UNRESOLVED_ISSUES), the rejected
|
||||
// version item, build 52 picked, Resubmit
|
||||
// submit-plan.jpg Prism Puddle's Send for review: build 52 picked, Submit for review pressed: the plan panel with
|
||||
// the dry run (nothing sent) and "Continue", the in-app step before the system dialog (the
|
||||
// native macOS dialog is not part of the web view, so it is never drawn here)
|
||||
// submit-waiting.jpg the same card after the submit: the fixture's apple status with the new submission waiting
|
||||
// for review (the app's own "Waiting for review" state)
|
||||
// render.ts runs it when out/desktop/ is missing; run it by hand after a UI change: bun media/teaser/desktop.ts
|
||||
import { mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { launch } from '../../cli/shots/browser';
|
||||
|
||||
|
|
@ -38,8 +51,21 @@ const SWEEPS = {
|
|||
health: { start: 'app:health', svg: '.card:has-text("User-perceived crash rate") .spark svg', x: [0.12, 0.9], y: 0.45 },
|
||||
} as const;
|
||||
|
||||
/** The real `storebound console plan <form> --json` of a fresh `storebound init` folder (offline). */
|
||||
export function realConsolePlan(form: string): Record<string, unknown> {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'sb-teaser-plan-'));
|
||||
try {
|
||||
const run = (argv: string[]) => Bun.spawnSync(['bun', join(ROOT, 'cli/index.ts'), ...argv], { cwd: dir, env: { ...process.env, NO_COLOR: '1' }, stdout: 'pipe', stderr: 'pipe' });
|
||||
if (run(['init']).exitCode !== 0) throw new Error('storebound init failed');
|
||||
const r = run(['console', 'plan', form, '--json']);
|
||||
if (r.exitCode !== 0) throw new Error(`console plan ${form} failed: ${r.stderr.toString().slice(-400)}`);
|
||||
return JSON.parse(r.stdout.toString()) as Record<string, unknown>;
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
}
|
||||
|
||||
export async function captureDesktop(): Promise<void> {
|
||||
mkdirSync(OUT, { recursive: true });
|
||||
const privacyPlan = realConsolePlan('apple-app-privacy');
|
||||
const port = 14_300 + Math.floor(Math.random() * 500);
|
||||
const vite = Bun.spawn(['bunx', 'vite', '--host', '127.0.0.1', '--port', String(port), '--strictPort'], {
|
||||
cwd: join(ROOT, 'apps/desktop'), env: { ...process.env, VITE_FIXTURES: '1' }, stdout: 'ignore', stderr: 'pipe',
|
||||
|
|
@ -56,8 +82,25 @@ export async function captureDesktop(): Promise<void> {
|
|||
const sweeps: Record<string, SweepFrame[]> = {};
|
||||
const errors: string[] = [];
|
||||
|
||||
const session = async (clean: boolean) => {
|
||||
const session = async (clean: boolean, waiting = false) => {
|
||||
const ctx = await browser.newContext({ viewport: VIEW, deviceScaleFactor: 2, colorScheme: 'dark', reducedMotion: 'reduce' });
|
||||
// the console plan panel shows the real plan of the CLI instead of the fixture's shortened one; after the submit
|
||||
// (waiting), the fixture's apple status holds the new submission waiting for review
|
||||
await ctx.route(/\/src\/dev\/submit-fixtures\.ts/, async (route: any) => {
|
||||
const res = await route.fetch();
|
||||
const src = await res.text();
|
||||
// (Vite serves the module as JS: types stripped, double quotes)
|
||||
let body = src.replace(/export function consolePlan\(form(?::\s*string)?\)\s*\{/, (m: string) => `${m}\n if (form === 'apple-app-privacy') return ${JSON.stringify(privacyPlan)};`);
|
||||
if (body === src) errors.push('submit-fixtures.ts: consolePlan moved (desktop.ts)');
|
||||
if (waiting) {
|
||||
if (!/export const APPLE_STATUS_READY = \{/.test(body) || !/const version = /.test(body)) errors.push('submit-fixtures.ts: APPLE_STATUS_READY moved (desktop.ts waiting capture)');
|
||||
// the submission the submit made (rs-new-1, the id the fixture's progress lines name), waiting for review
|
||||
body += `\nAPPLE_STATUS_READY.versions = [version('ver-240', '2.4.0', 'WAITING_FOR_REVIEW', 'b-52'), ...APPLE_STATUS_READY.versions.slice(1)];
|
||||
APPLE_STATUS_READY.subs = [{ id: 'rs-new-1', attributes: { state: 'WAITING_FOR_REVIEW', submittedDate: '${new Date(Date.now() - 60_000).toISOString()}' } }, ...APPLE_STATUS_READY.subs.slice(1)];
|
||||
APPLE_STATUS_READY.emptyDrafts = [];\n`;
|
||||
}
|
||||
await route.fulfill({ response: res, body });
|
||||
});
|
||||
if (clean) {
|
||||
// after the fixes: the fixture's scan of Prism Puddle answers with no findings (the UI is unchanged)
|
||||
await ctx.route(/\/src\/dev\/fixtures\.ts/, async (route: any) => {
|
||||
|
|
@ -189,11 +232,87 @@ export async function captureDesktop(): Promise<void> {
|
|||
await shot(b.page, 'ready');
|
||||
await b.ctx.close();
|
||||
|
||||
// the read / dry-run toasts are closed (their own close button), as the owner would, so they cover nothing
|
||||
const clearToasts = async (page: any): Promise<void> => {
|
||||
for (let i = 0; i < 12 && await page.locator('.toasts .toast').count(); i++) {
|
||||
await page.locator('.toasts .toast .toast-top button').first().click().catch(() => {});
|
||||
await page.waitForTimeout(250);
|
||||
}
|
||||
};
|
||||
// the console plan panel (App info > Console steps > App Privacy > Plan) with the real plan
|
||||
const c = await session(false);
|
||||
await open(c.page, 'app:appinfo');
|
||||
await c.page.locator('[data-form="apple-app-privacy"] button', { hasText: 'Plan' }).first().click();
|
||||
await c.page.waitForSelector('.cp-panel');
|
||||
await c.page.waitForTimeout(800);
|
||||
await clearToasts(c.page);
|
||||
const cpText = await c.page.locator('.cp-panel').innerText();
|
||||
for (const w of ['App Store Connect: App Privacy', '40 steps', 'The final button (Publish) is yours']) if (!cpText.includes(w)) errors.push(`console plan panel: missing "${w}"`);
|
||||
await box(c.page, 'cpTitle', c.page.locator('.cp-panel h2'));
|
||||
await box(c.page, 'cpSteps', c.page.locator('.cp-panel .cp-steps'));
|
||||
await shot(c.page, 'console-plan-top');
|
||||
const fin = c.page.locator('.cp-panel .notice', { hasText: 'The final button' }).first();
|
||||
await fin.evaluate((el: HTMLElement) => el.scrollIntoView({ block: 'center' }));
|
||||
await c.page.waitForTimeout(300);
|
||||
await box(c.page, 'cpFinal', fin);
|
||||
await shot(c.page, 'console-plan');
|
||||
await c.page.keyboard.press('Escape');
|
||||
|
||||
// send for review: Tidy Lists is back from App Review (unresolved), build 52 picked, Resubmit
|
||||
const submitSection = async (page: any, appName: string, ready = '[data-section="submit"] select'): Promise<void> => {
|
||||
await open(page, 'app:releases');
|
||||
await page.locator('nav.side .nav-app').filter({ hasText: appName }).first().click();
|
||||
await page.waitForTimeout(400);
|
||||
await page.locator('[role="tab"]#tab-releases').click();
|
||||
// the store reads are not cached in the fixture: press each card's "Read state", as the owner would
|
||||
await page.waitForSelector('[data-section="submit"]');
|
||||
for (const b of await page.locator('[data-section="submit"] button', { hasText: 'Read state' }).all()) await b.click();
|
||||
await page.waitForSelector(ready);
|
||||
await page.waitForTimeout(1500);
|
||||
await clearToasts(page);
|
||||
await page.locator('[data-section="submit"]').evaluate((el: HTMLElement) => {
|
||||
el.scrollIntoView({ block: 'start' });
|
||||
document.getElementById('content')?.scrollBy({ top: -70 });
|
||||
});
|
||||
await page.waitForTimeout(300);
|
||||
};
|
||||
await submitSection(c.page, 'Tidy Lists');
|
||||
await c.page.locator('[data-section="submit"] select').first().selectOption('52');
|
||||
await c.page.waitForTimeout(300);
|
||||
const unres = c.page.locator('[data-sub-kind="unresolved"]').first();
|
||||
if (!(await unres.count())) errors.push('resubmit: no "back from App Review" submission');
|
||||
await box(c.page, 'unresolvedItem', unres);
|
||||
await box(c.page, 'resubmitBtn', c.page.locator('[data-section="submit"] button', { hasText: 'Resubmit' }).first());
|
||||
await shot(c.page, 'resubmit');
|
||||
|
||||
// Prism Puddle: build 52 picked, Submit for review: the plan panel (dry run) with Continue
|
||||
await submitSection(c.page, 'Prism Puddle');
|
||||
await c.page.locator('[data-section="submit"] select').first().selectOption('52');
|
||||
await c.page.locator('[data-section="submit"] button', { hasText: 'Submit for review' }).first().click();
|
||||
await c.page.waitForSelector('.oa-panel');
|
||||
await c.page.waitForTimeout(800);
|
||||
await clearToasts(c.page);
|
||||
const oaText = await c.page.locator('.oa-panel').innerText();
|
||||
for (const w of ['Plan (dry run, nothing sent yet)', 'attach build 52', 'Continue: Submit for review']) if (!oaText.includes(w)) errors.push(`submit plan panel: missing "${w}"`);
|
||||
await box(c.page, 'oaPlan', c.page.locator('.oa-panel pre.plan-table'));
|
||||
await box(c.page, 'oaContinue', c.page.locator('.oa-panel .lb-foot button', { hasText: 'Continue' }));
|
||||
await shot(c.page, 'submit-plan');
|
||||
await c.ctx.close();
|
||||
|
||||
// after the submit: the new submission waiting for review
|
||||
const w = await session(false, true);
|
||||
await submitSection(w.page, 'Prism Puddle', '[data-sub-kind="waiting"]');
|
||||
const wait = w.page.locator('[data-sub-kind="waiting"]').first();
|
||||
if (!(await wait.count())) errors.push('submit-waiting: no "Waiting for review" submission');
|
||||
await box(w.page, 'waitingItem', wait);
|
||||
await shot(w.page, 'submit-waiting');
|
||||
await w.ctx.close();
|
||||
|
||||
const source = 'apps/desktop, Vite dev server, VITE_FIXTURES=1, headless Chromium 1280x800 @2x, dark';
|
||||
writeFileSync(join(OUT, 'boxes.json'), `${JSON.stringify({ ...boxes, source, sweeps }, null, 1)}\n`);
|
||||
await browser.close();
|
||||
if (errors.length) throw new Error(`page errors:\n ${[...new Set(errors)].join('\n ')}`);
|
||||
const n = 9 + Object.values(sweeps).reduce((k, f) => k + f.length, 0);
|
||||
const n = 14 + Object.values(sweeps).reduce((k, f) => k + f.length, 0);
|
||||
log(`${n} frames (${Object.entries(sweeps).map(([k, f]) => `${k} sweep: ${f.length} days`).join(', ')}) + boxes.json -> ${OUT}`);
|
||||
} finally {
|
||||
vite.kill();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue