teaser: the reject shot redone (7-11 s, 4 s): the common rejections storebound catches as real scan finding cards landing one per beat (2.1(a) demo account, 5.1.1(v) account deletion, 5.1.2(i) ATT from tests/fixtures/scan/teaser; ITMS-91064 from the new teaser-manifest fixture), guideline + one plain line each, then our own REJECTED stamp hits the stack on the beat at 9.5 s, no shake (one 4 px 1-frame jolt), a 3% push, the picture darkens into the cut; caption 'Rejected. Again.'; later shots +1.5 s (42.5 s, every cut on a beat, music follows the scenes, a thud per card and a hit on the stamp); render.ts --animatic --range a,b and --sheet a,b,fps, and the reject cards' boxes per frame in animatic-checks.json; tests: real finding lines, beat timing, and every card inside 6% safe margins at every frame

This commit is contained in:
CRTkafa 2026-10-14 05:22:47 +08:00
parent 511716bfd5
commit 20a764b3ee
11 changed files with 205 additions and 86 deletions

View file

@ -0,0 +1,13 @@
// !$*UTF8*$!
{
archiveVersion = 1;
objects = {
C1B2C3D4E5F6000000000002 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
INFOPLIST_FILE = Planner/Info.plist;
PRODUCT_BUNDLE_IDENTIFIER = com.example.planner;
};
};
};
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDisplayName</key>
<string>Planner</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTrackingDomains</key>
<array/>
</dict>
</plist>

View file

@ -0,0 +1,4 @@
# teaser-manifest
The teaser reject shot's ITMS-91064 card: an app privacy manifest with NSPrivacyTracking true and an empty
NSPrivacyTrackingDomains (tests/teaser.test.ts checks the line).

View file

@ -19,14 +19,16 @@ const MOCK = join(import.meta.dir, 'helpers', 'teaser-store-mock.ts');
interface Scene { id: string; start: number; end: number; x: number }
type Line = { at: number; text: string; cmd?: boolean; head?: boolean; final?: boolean };
interface Teaser {
FORMAT: { w: number; h: number }; DURATION: number; SCENES: Scene[]; MONTAGE: string[];
FORMAT: { w: number; h: number }; DURATION: number; FPS: number; SCENES: Scene[]; MONTAGE: string[];
CAPTIONS: { id: string; pos: 'top' | 'edge'; text: string; at: number; until: number }[];
captionTimes(): { id: string; text: string; pos: string; at: number; from: number; to: number; gone: number; hold: number }[];
veil(t: number): number;
GAME_SCREENS: string[]; GAME_SPEED: number;
PRIVACY_ANSWERS: string[]; PRIVACY_STEPS: string[]; DATA_SAFETY_CSV: string[]; DECL_TOTAL: number; PRICE_ROWS: string[]; PRICE_CMD: string;
PILE: { at: number; kind: string }[]; PILE_TITLES: Record<string, string>;
REJECT: { head: string; at: string }; PROMPT: string;
PROMPT: string; REJECTS: { fixture: string; rule: string; title: string; head: string }[]; REJECT_STEP: number; SAFE: number;
STAMP: { at: number; d: number; jolt: number }; REJECT_LAND: { d: number }; REJECT_PUSH: [number, number]; REJECT_DARK: number;
ticks: () => { t: number; kind: string }[];
BUILD_LINES: Line[]; SHOT_SIZES: { id: string }[]; SHOT_LANGS: string[]; PRODUCT_LINES: Line[];
DECL_LINES: { what: string; value: string; where: string }[]; DECL_ASK: string;
CONSOLE_CMD: string; CONSOLE_LINES: Line[]; CONSOLES_SWAP: number[];
@ -37,6 +39,7 @@ interface Teaser {
END: { markAt: number; wordAt: number; cmdAt: number; cps: number; urlAt: number; d: number; full: number }; END_CMD: string; END_URL: string;
SHOT_NOTES: Record<string, string>;
}
type Box = { x: number; y: number; w: number; h: number };
const T = (await import(join(ROOT, 'media/teaser/script.js'))) as Teaser;
/** Output lines without the clock prefix and the "(0s)" timer, as the video shows them. */
@ -153,11 +156,15 @@ describe('teaser: the scan text is real', () => {
let before = '', after = '';
beforeAll(() => { before = scanText('teaser', false); after = scanText('teaser-fixed', true); });
test('reject: the account-deletion finding, its own header and evidence line', () => {
const out = lines(before);
expect(out).toContain(` 2. ${T.REJECT.head}`);
expect(out).toContain(` ${T.REJECT.at}`);
expect(T.REJECT.head).toContain('5.1.1(v)');
test('reject: each card is a real finding line of `storebound scan` (teaser, teaser-manifest), its rule number in it', () => {
const manifest = run(['scan', '--dir', join(FX, 'teaser-manifest')], ROOT, {}).out;
for (const f of T.REJECTS) {
const out = f.fixture === 'teaser' ? before : manifest;
expect(out, f.rule).toMatch(new RegExp(`^ \\d\\. ${f.head.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'm'));
expect(f.head, f.rule).toContain(f.rule.replace('Guideline ', ''));
expect(f.title.split(' ').length, f.title).toBeLessThanOrEqual(6);
}
expect(T.REJECTS.map(f => f.rule)).toEqual(['Guideline 2.1(a)', 'Guideline 5.1.1(v)', 'Guideline 5.1.2(i)', 'ITMS-91064']);
});
test('scan: the risks line, all five findings in order, and the clean re-scan of teaser-fixed', () => {
expect(T.SCAN_CMD).toBe('storebound scan');
@ -239,7 +246,7 @@ describe('teaser: timing and format', () => {
}
for (let i = 1; i < c.length; i++) expect(c[i]!.at, c[i]!.text).toBeGreaterThanOrEqual(c[i - 1]!.gone);
for (const x of c) expect(x.pos, x.id).toBe(T.MONTAGE.includes(x.id) ? 'edge' : 'top');
expect(c.map(x => x.text)).toEqual(['Your game is done.', 'Then come the stores.', 'One line. Rejected.', 'So my AI did it.',
expect(c.map(x => x.text)).toEqual(['Your game is done.', 'Then come the stores.', 'Rejected. Again.', 'So my AI did it.',
'Builds.', 'Screenshots, every size.', 'Products, 175 countries.', 'Forms.', 'Consoles.', 'Nothing left to reject.', 'You press Submit.']);
});
test('the cold open moves on frame 0: no fade from black, the screens already rushing past', () => {
@ -257,6 +264,37 @@ describe('teaser: timing and format', () => {
const html = readFileSync(join(ROOT, 'media/teaser/index.html'), 'utf8');
expect(html).toMatch(/#brand \.cmdl \{[^}]*font: 800 7\dpx/);
});
test('reject: 4 s, the cards land one per beat and all read >= 1 s before the REJECTED stamp hits on a beat, then it darkens', () => {
const r = T.SCENES.find(s => s.id === 'reject')!;
expect(r.end - r.start).toBe(4);
expect(T.REJECTS.length).toBeLessThanOrEqual(5);
T.REJECTS.forEach((_, i) => expect(Number.isInteger((r.start + i * T.REJECT_STEP) / 0.5)).toBe(true));
expect(T.STAMP.at - T.STAMP.d - ((T.REJECTS.length - 1) * T.REJECT_STEP + T.REJECT_LAND.d)).toBeGreaterThanOrEqual(0.7);
expect(Number.isInteger((r.start + T.STAMP.at) / 0.5)).toBe(true);
expect(T.ticks().some(x => x.kind === 'stamp' && x.t === r.start + T.STAMP.at)).toBe(true);
expect(T.REJECT_DARK).toBeGreaterThan(T.STAMP.at);
expect(T.STAMP.jolt).toBeLessThanOrEqual(6); // no shake: one tiny 1-frame jolt at most
expect(readFileSync(join(ROOT, 'media/teaser/teaser.js'), 'utf8')).not.toMatch(/shake/);
});
// The render's layout data: `render.ts --animatic` writes every reject card's box at every frame of the shot.
const CHECKS = join(ROOT, 'media/teaser/out/animatic-checks.json');
test.skipIf(!existsSync(CHECKS))('reject: every card and the stamp stay inside the 6% safe margins at every frame (animatic layout data)', () => {
const j = JSON.parse(readFileSync(CHECKS, 'utf8')) as { duration: number; rejectLayout?: { t: number; cards: Box[]; stamp: Box | null }[] };
expect(j.duration).toBe(T.DURATION);
const r = T.SCENES.find(s => s.id === 'reject')!;
expect(j.rejectLayout?.length).toBe(Math.round((r.end - r.start) * T.FPS));
const mx = T.SAFE * T.FORMAT.w, my = T.SAFE * T.FORMAT.h;
for (const f of j.rejectLayout!) for (const [k, b] of [...f.cards.map((c, i) => [`card ${i + 1}`, c] as const), ['stamp', f.stamp] as const]) {
if (!b) continue;
const at = `${k} at ${f.t.toFixed(3)} s`;
expect(b.x, at).toBeGreaterThanOrEqual(mx);
expect(b.y, at).toBeGreaterThanOrEqual(my);
expect(b.x + b.w, at).toBeLessThanOrEqual(T.FORMAT.w - mx);
expect(b.y + b.h, at).toBeLessThanOrEqual(T.FORMAT.h - my);
}
expect(j.rejectLayout!.at(-1)!.cards.length).toBe(T.REJECTS.length);
expect(j.rejectLayout!.some(f => f.stamp)).toBe(true);
});
test('the picture darkens with the rejection and lifts on the drop', () => {
const r = T.SCENES.find(s => s.id === 'reject')!, b = T.SCENES.find(s => s.id === 'builds')!;
expect(T.veil(r.start)).toBe(0);