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:
parent
511716bfd5
commit
20a764b3ee
11 changed files with 205 additions and 86 deletions
13
tests/fixtures/scan/teaser-manifest/Planner.xcodeproj/project.pbxproj
vendored
Normal file
13
tests/fixtures/scan/teaser-manifest/Planner.xcodeproj/project.pbxproj
vendored
Normal 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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
10
tests/fixtures/scan/teaser-manifest/Planner/Info.plist
vendored
Normal file
10
tests/fixtures/scan/teaser-manifest/Planner/Info.plist
vendored
Normal 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>
|
||||
10
tests/fixtures/scan/teaser-manifest/Planner/PrivacyInfo.xcprivacy
vendored
Normal file
10
tests/fixtures/scan/teaser-manifest/Planner/PrivacyInfo.xcprivacy
vendored
Normal 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>
|
||||
4
tests/fixtures/scan/teaser-manifest/README.md
vendored
Normal file
4
tests/fixtures/scan/teaser-manifest/README.md
vendored
Normal 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).
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue