// Review gallery (data + HTML), A/B variant paths, contact sheet layout. Files are tiny generated headers; no browser. import { describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { galleryData, galleryHtml, scriptJson } from '../cli/shots/gallery'; import { renderDir, sheetLayout, variantRoot } from '../cli/shots/lib'; import { scanRenders } from '../cli/shots/report'; import { jpeg, png } from './fixtures'; function fixture(): string { const dir = mkdtempSync(join(tmpdir(), 'sb-gallery-')); const put = (rel: string, b: Uint8Array) => { mkdirSync(join(dir, rel, '..'), { recursive: true }); writeFileSync(join(dir, rel), b); }; put('en/ios69/01.png', png(1320, 2868)); put('en/ios69/02.png', png(1320, 2868, { colorType: 6 })); put('en/play/01.png', png(1080, 1920)); put('en/play/01.jpg', jpeg(1080, 1920, 8 * 1024 * 1024 + 1)); put('en/play/02.png', png(1080, 1920)); put('en/feature/1024x500.png', png(1024, 500)); put('tr/ios69/01.png', png(1320, 2868)); put('b/en/ios69/01.png', png(1320, 2868)); put('en/preview/iphone/01.mp4', new Uint8Array(10)); put('en/preview/iphone/01-poster.png', png(886, 1920)); writeFileSync(join(dir, 'report.json'), JSON.stringify({ 'tr/ios69/01.png': { overflow: ['title line 1 shrunk to 60 %: consider shorter words'], fonts: { used: { 'Paytone One': 20 }, fallback: [], missing: [] } } })); return dir; } describe('gallery', () => { const probe = () => ({ bytes: 1000, duration: 10, video: { codec: 'h264', w: 886, h: 1920, fps: 30 }, audio: { codec: 'aac', channels: 2, sampleRate: 48000 } }); test('data: variants, rows in size order, png/jpeg merged, issues on tiles and sets', () => { const dir = fixture(); try { const scan = scanRenders(dir, { langs: ['en', 'tr'], variants: ['b', 'missing'], required: { ios69: true, play: true, feature: true }, probe }); const d = galleryData(scan, { at: dir, now: new Date('2026-10-10T12:00:00Z'), exists: f => f.endsWith('-poster.png') }); expect(d.variants.map(v => v.label)).toEqual(['base', 'variant b']); const en = d.variants[0]!.langs.find(l => l.lang === 'en')!; expect(en.rows.map(r => r.size)).toEqual(['ios69', 'play', 'feature', 'preview/iphone']); const play = en.rows.find(r => r.size === 'play')!; expect(play.tiles.map(t => t.src)).toEqual(['en/play/01.png', 'en/play/02.png']); expect(play.tiles[0]!.issues.map(i => i.msg)).toEqual([expect.stringMatching(/^JPEG copy: 8\.0 MB > 8 MB/)]); expect(en.rows.find(r => r.size === 'ios69')!.tiles[1]!.issues[0]!.code).toBe('alpha'); const vid = en.rows.find(r => r.size === 'preview/iphone')!.tiles[0]!; expect(vid).toMatchObject({ kind: 'video', poster: 'en/preview/iphone/01-poster.png', w: 886, h: 1920 }); expect(vid.issues.some(i => i.msg.includes('15-30 s'))).toBe(true); const tr = d.variants[0]!.langs.find(l => l.lang === 'tr')!; expect(tr.rows.find(r => r.size === 'ios69')!.tiles[0]!.issues[0]).toMatchObject({ level: 'warn', code: 'overflow' }); expect(tr.rows.find(r => r.size === 'play')!.issues[0]!.code).toBe('count'); expect(d.variants[1]!.langs.map(l => l.lang)).toEqual(['en', 'tr']); expect(d.totals.images).toBe(8); expect(d.totals.errors).toBeGreaterThanOrEqual(4); expect(d.generated).toBe('2026-10-10 12:00'); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('html: self-contained, escaped, data embedded', () => { const dir = fixture(); try { const scan = scanRenders(dir, { langs: ['en'], probe }); const html = galleryHtml(galleryData(scan, { at: dir, title: 'Game & renders' })); expect(html.startsWith('')).toBe(true); expect(html).toContain('