// `shots record --source web` against a local animated page: the clip comes out at the frame's device pixels (CSS size // x dpr), not at CSS size (Chromium's screencast ignores the deviceScaleFactor), 30 fps H.264 of the asked length. // Skips when no Chromium can launch or ffmpeg is missing. Runs the CLI in a child process, like a real run. import { afterAll, describe, expect, test } from 'bun:test'; import { spawnSync } from 'node:child_process'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import type { ShipConfig } from '../../cli/lib/config'; import { BASE } from '../helpers/store-mock'; const CLI = join(import.meta.dir, '..', '..', 'cli', 'index.ts'); const server = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response(` `, { headers: { 'content-type': 'text/html' } }), }); afterAll(() => server.stop(true)); const canLaunch = spawnSync('bun', ['-e', `const { launch } = await import(${JSON.stringify(join(import.meta.dir, '..', '..', 'cli', 'shots', 'browser.ts'))}); await launch(${JSON.stringify(join(import.meta.dir, '..', '..'))}, { headless: true }); process.exit(0);`], { timeout: 60_000 }).status === 0; const hasFfmpeg = spawnSync('ffmpeg', ['-version']).status === 0; if (!canLaunch || !hasFfmpeg) console.warn('record-web browser test skipped: needs a Chromium (bunx playwright install chromium) and ffmpeg'); describe.skipIf(!canLaunch || !hasFfmpeg)('shots record (web)', () => { test('clip at device pixels, 30 fps, asked length', async () => { const root = mkdtempSync(join(tmpdir(), 'sb-rec-')); try { mkdirSync(join(root, 'store/shots'), { recursive: true }); writeFileSync(join(root, 'store/shots/copy.json'), '{}'); const cfg: ShipConfig = { ...BASE, languages: [{ id: 'en' }], shots: { copy: 'store/shots/copy.json', frames: 'store/shots/frames', out: 'store/renders', scenes: [{ id: 'hero', palette: 'fire', frames: [{ src: 'hero.png' }] }], phone: { w: 200, h: 400, dpr: 2 }, capture: { source: 'web', baseUrl: `http://127.0.0.1:${server.port}`, shots: { home: { path: '/' } } }, video: { clips: 'store/video/clips', record: { clips: { play: { seconds: 2, path: '/', waitFor: 'window.ready', delayMs: 50 } } } } } } as ShipConfig; writeFileSync(join(root, 'storebound.config.json'), JSON.stringify(cfg)); const r = Bun.spawn(['bun', CLI, 'shots', 'record'], { cwd: root, stdout: 'pipe', stderr: 'pipe', env: { ...process.env, NO_COLOR: '1' } }); const [out, err, code] = await Promise.all([new Response(r.stdout).text(), new Response(r.stderr).text(), r.exited]); if (code !== 0) console.error(out, err); expect(code).toBe(0); expect(err).not.toContain('asked for'); const probe = spawnSync('ffprobe', ['-v', 'error', '-select_streams', 'v:0', '-show_entries', 'stream=width,height,r_frame_rate:format=duration', '-of', 'json', join(root, 'store/video/clips/en/01-play.mp4')]).stdout.toString(); const j = JSON.parse(probe) as { streams: { width: number; height: number; r_frame_rate: string }[]; format: { duration: string } }; expect(j.streams[0]).toMatchObject({ width: 400, height: 800, r_frame_rate: '30/1' }); expect(Number(j.format.duration)).toBeCloseTo(2, 1); } finally { rmSync(root, { recursive: true, force: true }); } }, 120_000); });