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
|
|
@ -1,6 +1,6 @@
|
|||
// The teaser video (media/teaser, v6): every CLI line it shows is real output of the command it names, every desktop
|
||||
// frame is a real capture of the app (desktop.ts), each scene holds 4-5 s, each caption is fully on screen for at least
|
||||
// 3 s, the whole cut is 50-60 s, and it is 16:9 only.
|
||||
// The teaser video (media/teaser, v7): every CLI line it shows is real output of the command it names, every desktop
|
||||
// frame is a real capture of the app (desktop.ts), each scene holds 4-5 s (a whole number of beats), each caption is
|
||||
// fully on screen for at least 3 s, the whole cut is 60-65 s, and it is 16:9 only.
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { generateKeyPairSync } from 'node:crypto';
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
|
|
@ -25,9 +25,13 @@ interface Teaser {
|
|||
COPY_FILE: string; COPY: { lang: string; title: string[] }[]; SHOT_SIZES: { id: string }[]; SHOT_LANGS: string[]; SHOTS_PILL: string;
|
||||
PRODUCT_LINES: { text: string; cmd?: boolean }[]; PRODUCTS_PILL: { text: string };
|
||||
DECL_LINES: { what: string; value: string; where: string }[]; DECL_ASK: string; DECL_TOTAL: number;
|
||||
CONSOLE_CMD: string; CONSOLE_LINES: { text: string; head?: boolean; final?: boolean }[]; CONSOLE_STEPS: number; CONSOLE_ANSWERS: number;
|
||||
CONSOLES_PILL: { text: string }; CONSOLES_SWAP: number[];
|
||||
SCAN_CMD: string; RISKS_LINE: string; RESCAN_LINE: string;
|
||||
FINDINGS: { sev: string; guideline: string; title: string; where: string; why: string }[];
|
||||
DESK: Record<string, { groups: { frame: string; sweep?: { seq: string; at: number; d: number } }[]; swap?: number[]; callouts: { at: number; until: number; target: string; text: string }[] }>;
|
||||
DESK: Record<string, { groups: { frame: string; sweep?: { seq: string; at: number; d: number } }[]; swap?: number[]; show?: number[]; hide?: number[];
|
||||
callouts: { at: number; until: number; target: string; text: string }[]; pointer?: { at: number; d: number; target: string; until?: number } }>;
|
||||
SUBMIT: { logoAt: number };
|
||||
DESK_SCENES: string[]; DESK_FRAMES: string[]; DESK_SWEEPS: string[];
|
||||
}
|
||||
const T = (await import(join(ROOT, 'media/teaser/script.js'))) as Teaser;
|
||||
|
|
@ -100,6 +104,27 @@ describe('teaser: every CLI line is real output', () => {
|
|||
} finally { writeFileSync(cfg, src); }
|
||||
});
|
||||
|
||||
test('consoles: `storebound console plan apple-app-privacy` (offline): steps, answer sheet, never-press, the owner\'s Publish', () => {
|
||||
expect(T.CONSOLE_CMD).toBe('storebound console plan apple-app-privacy');
|
||||
const r = run(T.CONSOLE_CMD.split(' ').slice(1), dir);
|
||||
expect(r.code).toBe(0);
|
||||
const out = lines(r.out);
|
||||
for (const l of T.CONSOLE_LINES) expect(out, l.text).toContain(l.text);
|
||||
// the lines keep the order of the real plan
|
||||
const idx = T.CONSOLE_LINES.map(l => out.indexOf(l.text));
|
||||
expect([...idx].sort((a, b) => a - b)).toEqual(idx);
|
||||
// the pill's counts: the numbered steps and the answer-sheet rows of the whole plan
|
||||
const steps = out.slice(out.indexOf('Steps:'), out.indexOf('Answer sheet:')).filter(l => /^\s+\d+\. /.test(l)).length;
|
||||
const answers = out.filter(l => /^ {2}\[(ok|ask|check|agree)\] /.test(l)).length;
|
||||
expect(steps).toBe(T.CONSOLE_STEPS);
|
||||
expect(answers).toBe(T.CONSOLE_ANSWERS);
|
||||
expect(T.CONSOLES_PILL.text).toBe(`*${steps}* steps · *${answers}* answers · stops before *Publish*`);
|
||||
// the final button stays the owner's, and the never-press list holds it
|
||||
const fin = T.CONSOLE_LINES.find(l => l.final)!;
|
||||
expect(fin.text).toBe("Final button (the owner's): Publish");
|
||||
expect(T.CONSOLE_LINES.find(l => l.text.startsWith('Never press:'))!.text).toContain('"Publish"');
|
||||
});
|
||||
|
||||
test('forms: the Data safety answer count of `storebound check`', () => {
|
||||
expect(run(['check'], dir).out).toContain(`data safety: ${T.DECL_TOTAL} CSV rows`);
|
||||
});
|
||||
|
|
@ -159,16 +184,20 @@ describe('teaser: timing and format', () => {
|
|||
expect(render).not.toMatch(/1080\s*x\s*1920|w: 1080, h: 1920|vertical/i);
|
||||
expect(render).toContain('scale=1280:720');
|
||||
});
|
||||
test('scenes are contiguous, 4-5 s each, 50-60 s in total', () => {
|
||||
test('scenes are contiguous, 4-5 s each (whole half seconds: each cut on a beat of music.js), 60-65 s in total', () => {
|
||||
expect(T.SCENES[0]!.start).toBe(0);
|
||||
for (let i = 1; i < T.SCENES.length; i++) expect(T.SCENES[i]!.start).toBe(T.SCENES[i - 1]!.end);
|
||||
for (const s of T.SCENES) {
|
||||
expect(s.end - s.start, s.id).toBeGreaterThanOrEqual(4);
|
||||
expect(s.end - s.start, s.id).toBeLessThanOrEqual(5);
|
||||
expect(Number.isInteger(s.start * 2), s.id).toBe(true);
|
||||
}
|
||||
expect(T.DURATION).toBe(T.SCENES.at(-1)!.end);
|
||||
expect(T.DURATION).toBeGreaterThanOrEqual(50);
|
||||
expect(T.DURATION).toBeLessThanOrEqual(60);
|
||||
expect(T.DURATION).toBeGreaterThanOrEqual(60);
|
||||
expect(T.DURATION).toBeLessThanOrEqual(65);
|
||||
const music = readFileSync(join(ROOT, 'media/teaser/music.js'), 'utf8');
|
||||
expect(music).toContain('export const STEP = 0.5;');
|
||||
for (const s of T.SCENES) expect(music, s.id).toMatch(new RegExp(`SCENE_CHORD = \\{[^}]*\\b${s.id}: '`));
|
||||
});
|
||||
test('one caption per scene, fully on screen for at least 3 s, inside its scene', () => {
|
||||
const c = T.captionTimes();
|
||||
|
|
@ -186,7 +215,8 @@ describe('teaser: timing and format', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// The desktop sequence: four scenes of the real app (fixture mode), captured by media/teaser/desktop.ts. The frames are
|
||||
// The desktop sequence: the real app (fixture mode) in the consoles scene and the last five, captured by
|
||||
// media/teaser/desktop.ts. The frames are
|
||||
// gitignored output; the file checks run wherever they were captured (render.ts captures them before every render).
|
||||
const DESK_DIR = join(ROOT, 'media/teaser/out/desktop');
|
||||
const captured = existsSync(join(DESK_DIR, 'boxes.json'));
|
||||
|
|
@ -203,15 +233,46 @@ function jpegSize(buf: Buffer): { w: number; h: number } | null {
|
|||
}
|
||||
|
||||
describe('teaser: the desktop scenes are real captures of the app', () => {
|
||||
test('four desktop scenes in a row before the submit card, the last one is v5\'s (timeline, then 0 blockers)', () => {
|
||||
expect(T.DESK_SCENES).toEqual(['portfolio', 'charts', 'catalog', 'desktop']);
|
||||
test('the app in the consoles scene and the last five: the plan panel, portfolio, charts, prices, resubmit, submit', () => {
|
||||
expect(T.DESK_SCENES).toEqual(['consoles', 'portfolio', 'charts', 'catalog', 'resubmit', 'submit']);
|
||||
const ids = T.SCENES.map(s => s.id);
|
||||
expect(ids.slice(-5)).toEqual([...T.DESK_SCENES, 'submit']);
|
||||
expect(T.DESK.desktop!.groups.map(g => g.frame)).toEqual(['releases', 'ready']);
|
||||
expect(T.CAPTIONS.find(c => c.id === 'desktop')!.lines.join(' ').replace(/\*/g, '')).toBe('Both stores, one view. Ready for review.');
|
||||
for (const id of T.DESK_SCENES) expect(T.DESK[id]!.callouts.length, id).toBe(1 + (id === 'desktop' ? 1 : 0));
|
||||
expect(ids).toEqual(['hook', 'init', 'plan', 'store', 'products', 'forms', 'consoles', 'rejections', 'portfolio', 'charts', 'catalog', 'resubmit', 'submit']);
|
||||
const cap = (id: string) => T.CAPTIONS.find(c => c.id === id)!.lines.join(' ').replace(/\*/g, '');
|
||||
expect(cap('consoles')).toBe('No API? Your AI fills the console. It stops at Publish.');
|
||||
expect(cap('resubmit')).toBe('Rejected? Fix it, resubmit in one click.');
|
||||
expect(cap('submit')).toBe('You press Submit. That\'s it.');
|
||||
// the terminal gives way to the app's plan panel, which comes in as the terminal goes
|
||||
expect(T.DESK.consoles!.groups.map(g => g.frame)).toEqual(['console-plan']);
|
||||
expect(T.DESK.consoles!.show).toEqual(T.CONSOLES_SWAP);
|
||||
expect(T.DESK.resubmit!.groups.map(g => g.frame)).toEqual(['resubmit']);
|
||||
// submit: the dry-run plan with Continue, then waiting for review; the window fades out as the end card comes in
|
||||
expect(T.DESK.submit!.groups.map(g => g.frame)).toEqual(['submit-plan', 'submit-waiting']);
|
||||
expect(T.DESK.submit!.hide![0]).toBe(T.SUBMIT.logoAt);
|
||||
expect(T.DESK.submit!.pointer!.target).toBe('oaContinue');
|
||||
expect(T.DESK.resubmit!.pointer!.target).toBe('resubmitBtn');
|
||||
// the v6 desktop scene (timeline, readiness) is gone: v7 is 62 s with resubmit and submit
|
||||
expect(ids).not.toContain('desktop');
|
||||
for (const id of T.DESK_SCENES) {
|
||||
const s = T.SCENES.find(x => x.id === id)!;
|
||||
expect(T.DESK[id]!.callouts.length, id).toBeGreaterThanOrEqual(1);
|
||||
expect(T.DESK[id]!.callouts.length, id).toBeLessThanOrEqual(2);
|
||||
// callouts follow one another, never two at once, and each holds long enough to read
|
||||
const c = T.DESK[id]!.callouts;
|
||||
for (let i = 0; i < c.length; i++) {
|
||||
expect(Math.min(c[i]!.until, s.end - s.start) - c[i]!.at, `${id} ${c[i]!.text}`).toBeGreaterThanOrEqual(0.9);
|
||||
if (i) expect(c[i]!.at, id).toBeGreaterThanOrEqual(c[i - 1]!.until);
|
||||
}
|
||||
}
|
||||
expect(T.DESK_SWEEPS).toEqual(['growth', 'health']);
|
||||
});
|
||||
test('the console plan panel shows the real plan (desktop.ts serves `console plan apple-app-privacy --json`)', () => {
|
||||
const src = readFileSync(join(ROOT, 'media/teaser/desktop.ts'), 'utf8');
|
||||
expect(src).toContain("realConsolePlan('apple-app-privacy')");
|
||||
expect(src).toContain("['console', 'plan', form, '--json']");
|
||||
// never a drawn macOS dialog: the submit scene shows the app's own plan panel before the system dialog
|
||||
const html = readFileSync(join(ROOT, 'media/teaser/index.html'), 'utf8');
|
||||
expect(html).not.toMatch(/Submit for Review<span class="hov">|id="sbtn"/);
|
||||
});
|
||||
test('every frame a scene names is one desktop.ts captures from the app (no mock frames)', () => {
|
||||
const src = readFileSync(join(ROOT, 'media/teaser/desktop.ts'), 'utf8');
|
||||
expect(src).toContain("VITE_FIXTURES: '1'");
|
||||
|
|
@ -244,5 +305,6 @@ describe('teaser: the desktop scenes are real captures of the app', () => {
|
|||
// every sweep frame differs from the others (the tooltip moved): no copied frames
|
||||
expect(sizes.size).toBeGreaterThan(files.length * 0.9);
|
||||
for (const id of T.DESK_SCENES) for (const c of T.DESK[id]!.callouts) expect(boxes[c.target], c.target).toBeDefined();
|
||||
for (const id of T.DESK_SCENES) if (T.DESK[id]!.pointer) expect(boxes[T.DESK[id]!.pointer!.target], id).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue