shots: app titles get the weights the templates use (variable axis, used range or static weights; font-weight warning), palette.bg as one colour or a CSS gradient string (validated; contrast guard in shots check), photo backgrounds (palette.image, shots.feature.background) with overlay and cover crop
This commit is contained in:
parent
2ba8278ab7
commit
3a11682a37
21 changed files with 713 additions and 44 deletions
65
tests/browser/palette-bg.browser.ts
Normal file
65
tests/browser/palette-bg.browser.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// 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<ReturnType<typeof titleContrast>>; 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<void> }) => {
|
||||
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));
|
||||
});
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue