/** * screenshot-embeds.test — IO collector: embeds only true failures, honors the * per-image and total caps with explicit notes, skips missing files, keeps * POSIX keys. */ import { describe, it, expect, afterEach } from 'vitest'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { UiRunFileSchema, type UiRunFile } from '../../lib/run-results.js'; import { collectScreenshotEmbeds } from '../screenshot-embeds.js'; const tmpDirs: string[] = []; const makeTmp = (): string => { const dir = mkdtempSync(join(tmpdir(), 'uat-embeds-')); tmpDirs.push(dir); return dir; }; afterEach(() => { while (tmpDirs.length) rmSync(tmpDirs.pop()!, { recursive: true, force: true }); }); const uiFile = (results: unknown[]): UiRunFile => UiRunFileSchema.parse({ kind: 'uat-ui', meta: { runId: 'r1', application: 'a', planPath: 'p', startedAt: 's' }, results, }); const row = (routeId: string, over: Record = {}): Record => ({ routeId, role: 'admin', url: `/x/${routeId}`, expected: 'allowed', actual: 'denied', ok: false, executed: true, screenshot: `screenshots/admin/${routeId}.png`, ...over, }); function writeShot(runDir: string, rel: string, bytes: number): void { const abs = join(runDir, rel); mkdirSync(join(abs, '..'), { recursive: true }); writeFileSync(abs, Buffer.alloc(bytes, 7)); } describe('collectScreenshotEmbeds', () => { it('embeds only executed determinate failures; passes/indeterminates/skips keep links', () => { const runDir = makeTmp(); for (const id of ['FAIL', 'PASS', 'IND', 'SKIP']) writeShot(runDir, `screenshots/admin/${id}.png`, 100); const file = uiFile([ row('FAIL'), row('PASS', { actual: 'allowed', ok: true }), row('IND', { actual: 'indeterminate' }), row('SKIP', { executed: false, reason: 'login_failed' }), ]); const embeds = collectScreenshotEmbeds(runDir, file); expect(Object.keys(embeds)).toEqual(['screenshots/admin/FAIL.png']); // POSIX key, verbatim expect(embeds['screenshots/admin/FAIL.png'].dataUri).toMatch(/^data:image\/png;base64,/); }); it('an over-cap image falls back to a note; the total budget caps the rest', () => { const runDir = makeTmp(); writeShot(runDir, 'screenshots/admin/BIG.png', 2000); writeShot(runDir, 'screenshots/admin/A.png', 600); writeShot(runDir, 'screenshots/admin/B.png', 600); const file = uiFile([row('BIG'), row('A'), row('B')]); const embeds = collectScreenshotEmbeds(runDir, file, { perImage: 1000, total: 1000 }); expect(embeds['screenshots/admin/BIG.png'].note).toContain('cap'); expect(embeds['screenshots/admin/BIG.png'].dataUri).toBeUndefined(); // A fits the budget, B no longer does — artifact order decides deterministically. expect(embeds['screenshots/admin/A.png'].dataUri).toBeDefined(); expect(embeds['screenshots/admin/B.png']).toEqual({ note: 'embed budget reached — linked instead' }); }); it('a missing file yields NO entry (renderer falls back to the relative link)', () => { const runDir = makeTmp(); const embeds = collectScreenshotEmbeds(runDir, uiFile([row('GONE')])); expect(embeds).toEqual({}); }); it('a null uiFile yields an empty map', () => { expect(collectScreenshotEmbeds(makeTmp(), null)).toEqual({}); }); });