// shots.layout 'bleed' (the frame fills the image, a corner caption with a text shadow, no device) and // shots.orientation 'landscape', plus the Mac App Store size (apple.mac). Pure checks, no browser. import { describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { type ShipConfig, validateConfig } from '../cli/lib/config'; import { SHOTS_PAGES } from '../cli/lib/embedded'; import { checkFrameFolder } from '../cli/shots/check'; import { renderDims } from '../cli/shots/lib'; import { checkImage } from '../cli/shots/spec'; function png(w: number, h: number): Uint8Array { const b = new Uint8Array(8 + 25 + 12); b.set([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); const dv = new DataView(b.buffer); dv.setUint32(8, 13); b.set([0x49, 0x48, 0x44, 0x52], 12); dv.setUint32(16, w); dv.setUint32(20, h); b[24] = 8; b[25] = 2; dv.setUint32(33, 0); b.set([0x49, 0x45, 0x4e, 0x44], 37); return b; } const base = (shots: Partial = {}, apple: ShipConfig['apple'] = { bundleId: 'com.example.game', mac: true }): ShipConfig => ({ app: { name: 'Example Game' }, languages: [{ id: 'en', apple: 'en-US', play: 'en-US' }], apple, play: { packageName: 'com.example.game', tablets: false }, shots: { copy: 'c.json', frames: 'frames', out: 'out', layout: 'bleed', orientation: 'landscape', scenes: [{ id: 'a', palette: 'night', frames: [{ src: 'a.png' }], corner: 'bl' }], feature: { frame: 'f.png' }, ...shots }, } as ShipConfig); describe('landscape sizes', () => { const dims = (k: string, o?: 'landscape' | 'portrait') => { const d = renderDims(k, { orientation: o }); return { w: d.w, h: d.h }; }; test('portrait sizes swap, feature, mac and chromebook stay', () => { expect(dims('ios69', 'landscape')).toEqual({ w: 2868, h: 1320 }); expect(dims('ipad13', 'landscape')).toEqual({ w: 2752, h: 2064 }); expect(dims('play', 'landscape')).toEqual({ w: 1920, h: 1080 }); expect(dims('ios69')).toEqual({ w: 1320, h: 2868 }); expect(dims('feature', 'landscape')).toEqual({ w: 1024, h: 500 }); expect(dims('mac', 'landscape')).toEqual({ w: 2880, h: 1800 }); expect(dims('chromebook', 'landscape')).toEqual({ w: 1920, h: 1080 }); }); test('stores accept the landscape renders', () => { for (const [k, w, h] of [['ios69', 2868, 1320], ['ipad13', 2752, 2064], ['mac', 2880, 1800], ['play', 1920, 1080], ['tab7', 1920, 1200], ['tab10', 2560, 1600]] as const) expect(checkImage({ format: 'png', w, h, alpha: false, palette: false, bytes: 1000, depth: 8 }, k)).toEqual([]); }); test('orientation landscape needs the bleed layout', () => { expect(validateConfig(base({ layout: 'device' })).join(' ')).toContain("needs shots.layout: 'bleed'"); expect(validateConfig(base()).join(' ')).not.toContain('orientation'); }); }); describe('bleed layout', () => { test('config checks', () => { const bad = base({ layout: 'full' as never, orientation: 'sideways' as never, scenes: [{ id: 'a', palette: 'night', frames: [{ src: 'a.png' }], corner: 'mid' as never, captionScale: 3 }] }); const e = validateConfig(bad).join('\n'); expect(e).toContain('shots.layout: device | bleed'); expect(e).toContain('shots.orientation: portrait | landscape'); expect(e).toContain('corner tl | tr | bl | br'); expect(e).toContain('captionScale 0.6-1.4'); }); test('the pages ship embedded and draw no band behind the caption', () => { for (const p of ['template-bleed.html', 'feature-bleed.html']) { const html = SHOTS_PAGES[p]!; expect(html).toBeTruthy(); expect(html).toContain('text-shadow'); // owner rule: legibility from a text shadow and glow, never a dark band, scrim or box behind the words const css = html.slice(html.indexOf('')); expect(css).not.toMatch(/\.(cap|name)\b[^}]*background/); expect(css).not.toMatch(/linear-gradient|scrim|band/); } }); test('frames per size at native pixels', () => { const dir = mkdtempSync(join(tmpdir(), 'sb-bleed-')); try { const c = base(); const put = (rel: string, w: number, h: number) => { mkdirSync(join(dir, rel, '..'), { recursive: true }); writeFileSync(join(dir, rel), png(w, h)); }; const missing = checkFrameFolder(c, dir, ['en']).map(i => i.file); expect(missing).toContain('en/ios69/a.png'); expect(missing).toContain('en/mac/a.png'); expect(missing).toContain('en/feature/f.png'); put('en/ios69/a.png', 2868, 1320); put('en/ios65/a.png', 2778, 1284); put('en/mac/a.png', 2880, 1800); put('en/play/a.png', 1920, 1080); put('en/feature/f.png', 2048, 1000); expect(checkFrameFolder(c, dir, ['en'])).toEqual([]); put('en/play/a.png', 1080, 1920); expect(checkFrameFolder(c, dir, ['en'])[0]!.msg).toContain('aspect'); put('en/play/a.png', 960, 540); expect(checkFrameFolder(c, dir, ['en'])[0]!.msg).toContain('native resolution'); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('render serves the bleed pages and names the size in the URL', () => { const src = readFileSync(join(import.meta.dir, '..', 'cli', 'shots', 'render.ts'), 'utf8'); expect(src).toContain("bleedLayout ? 'template-bleed'"); expect(src).toContain('&size=${encodeURIComponent(size)}'); expect(SHOTS_PAGES['template-bleed.html']).toContain('/frames/${encodeURIComponent(LANG)}/${encodeURIComponent(SIZE)}/'); }); });