// Regression: an app-style palette with a string bg ('#15171C' or 'linear-gradient(...)') rendered a WHITE background // (the template indexed bg[0..2] of a string), so white ink headlines vanished. Now the bg goes through palette.ts // bgCss, and `shots render` samples the pixels behind the title (titleContrast) for `shots check`. import { afterAll, describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { DEFAULT_PHONE, DEFAULT_TABLET } from '../../cli/shots/lib'; import { bgCss, contrastIssues, photoCss } from '../../cli/shots/palette'; import { titleContrast } from '../../cli/shots/render'; import { AFTER_ALL_MS, launchTestBrowser, scaled } from '../helpers/browser'; const shared = await launchTestBrowser('playwright-core'); const { browser } = shared; if (!browser) console.warn(`palette bg render test skipped: no Chromium installed (${shared.skipWhy}).`); afterAll(() => shared.close(), AFTER_ALL_MS); const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64'); const html = readFileSync(join(import.meta.dir, '..', '..', 'cli', 'shots', 'template-app.html'), 'utf8'); async function render(bg: unknown, ink: string, photo = false): Promise<{ contrast: Awaited>; bgImage: string; bgColor: string }> { const page = await browser.newPage({ viewport: { width: 540, height: 960 } }); try { const pal = { bg, glow: '#93c5fd', ink, shadow: 'rgba(0,0,0,.4)' }; const css = photo ? photoCss('/bgimg/0', 0.6) + ', ' + bgCss(bg as never) : bgCss(bg as never); const job = { style: 'app', phone: DEFAULT_PHONE, tablet: DEFAULT_TABLET, landscape: { w: 1440, h: 900, bar: 0 }, side: 'left', logo: false, appName: 'Demo', fonts: { title: 'X', body: 'Y' }, number: 1, scene: { id: 'a', frames: [{ src: 'a.png' }] }, palette: { ...pal, bgCss: css, bgTone: photo ? '#000000' : undefined, ...(photo ? { photo: { overlay: 0.6 } } : {}) }, copy: { eyebrow: 'Notes', title: ['Calm notes', 'for busy days'], sub: 'Synced everywhere.' } }; await page.route('http://t.test/**', (r: { request(): { url(): string }; fulfill(o: unknown): Promise }) => { const u = new URL(r.request().url()); if (u.pathname.endsWith('.html')) return r.fulfill({ contentType: 'text/html', body: html }); if (u.pathname === '/job.json') return r.fulfill({ contentType: 'application/json', body: JSON.stringify(job) }); if (u.pathname.startsWith('/frames/') || u.pathname.startsWith('/bgimg/')) return r.fulfill({ contentType: 'image/png', body: PNG }); return r.fulfill({ contentType: 'text/css', body: '' }); }); await page.goto('http://t.test/template-app.html?s=a&w=540&h=960&lang=en'); await page.waitForFunction("document.body.dataset.ready === '1'", null, { timeout: 15000 }); const st = JSON.parse(await page.evaluate("JSON.stringify((s => ({ i: s.backgroundImage, c: s.backgroundColor }))(getComputedStyle(document.getElementById('c'))))")) as { i: string; c: string }; return { contrast: await titleContrast(page, 540, 960), bgImage: st.i, bgColor: st.c }; } finally { await page.close(); } } describe.skipIf(!browser)('app template: palette bg forms and the contrast guard', () => { test('a flat colour string renders that colour, white ink passes', async () => { const r = await render('#15171C', '#ffffff'); expect(r.bgColor).toBe('rgb(21, 23, 28)'); expect(r.contrast.length).toBeGreaterThan(0); expect(contrastIssues(r.contrast)).toEqual([]); }, scaled(30_000)); test('a gradient string passes through', async () => { const r = await render('linear-gradient(160deg, #0b1736, #1d4ed8)', '#ffffff'); expect(r.bgImage).toContain('linear-gradient'); expect(contrastIssues(r.contrast)).toEqual([]); }, scaled(30_000)); test('white ink on a white bg is flagged', async () => { const r = await render(['#ffffff', '#fafafa', '#ffffff'], '#ffffff'); expect(contrastIssues(r.contrast).map(i => i.code)).toContain('contrast'); }, scaled(30_000)); test('a photo under the overlay keeps white ink legible', async () => { const r = await render('#ffffff', '#ffffff', true); expect(r.bgImage).toContain('/bgimg/0'); expect(contrastIssues(r.contrast)).toEqual([]); }, scaled(30_000)); });