// The screenshot templates in a real page: the landscape layout (Mac window or bare frame, copy column) renders with // its device inside the canvas, and every template keeps its text and logo inside the safe margins (7 % of the width // left / right, 5 % of the height at the top), measured from the page's own layout (body.dataset.boxes). A page with // the margins taken away fails the same rule. Gated like feature-fit: skipped without Chromium. import { afterAll, describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { boxOutside, DEFAULT_PHONE, DEFAULT_TABLET, marginViolations, renderDims, type TextBox } from '../../cli/shots/lib'; import { AFTER_ALL_MS, launchTestBrowser, scaled } from '../helpers/browser'; const shared = await launchTestBrowser('playwright-core'); const { browser } = shared; if (!browser) console.warn(`landscape / safe-margin render test skipped: no Chromium installed (${shared.skipWhy}). Run \`bunx playwright install chromium\`.`); afterAll(() => shared.close(), AFTER_ALL_MS); const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64'); const dir = join(import.meta.dir, '..', '..', 'cli', 'shots'); async function render(style: 'game' | 'app', w: number, h: number, o: { landscape?: boolean; frame?: string; hero?: boolean; css?: string; title?: [string, string] } = {}) { const page = await browser.newPage({ viewport: { width: w, height: h } }); try { let html = readFileSync(join(dir, style === 'app' ? 'template-app.html' : 'template.html'), 'utf8'); if (o.css) html = html.replace('', `${o.css}`); const job = { style, phone: DEFAULT_PHONE, tablet: DEFAULT_TABLET, landscape: { w: 1440, h: 900, bar: 0 }, side: 'left', logo: !!o.hero, appName: 'Demo', fonts: { title: 'X', body: 'Y' }, number: 1, scene: { id: 'a', hero: !!o.hero, frames: [{ src: 'a.png' }] }, palette: { bg: ['#123456', '#345678', '#567890'], glow: '#ffffff', ink: '#000000', shadow: 'rgba(0,0,0,.4)' }, copy: { eyebrow: 'Daily puzzles', title: o.title ?? ['Match three', 'Win big'], sub: 'Hundreds of levels, no wifi needed.' } }; 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 === '/logo') return r.fulfill({ contentType: 'image/png', body: PNG }); return r.fulfill({ contentType: 'text/css', body: '' }); }); const dev = o.landscape ? `&device=landscape&frame=${o.frame ?? 'window'}` : ''; await page.goto(`http://t.test/t.html?s=a&w=${w}&h=${h}&lang=en${dev}`); await page.waitForFunction("document.body.dataset.ready === '1'", null, { timeout: scaled(15000) }); const read = async (k: string) => JSON.parse((await page.evaluate(`document.body.dataset.${k}`)) ?? '[]'); return { boxes: await read('boxes') as TextBox[], devices: await read('devices') as { x: number; y: number; w: number; h: number }[], frameUrl: await page.evaluate("document.querySelector('.lwin img, .tel img.app')?.getAttribute('src') ?? ''") as string, titlebar: await page.evaluate("!!document.querySelector('.lwin:not(.bare) .tb')") as boolean }; } finally { await page.close(); } } describe.skipIf(!browser)('landscape layout', () => { for (const style of ['game', 'app'] as const) for (const [size, frame] of [['mac', 'window'], ['chromebook', 'none'], ['tab10', 'none']] as const) test(`${style} ${size} (${frame})`, async () => { const d = renderDims(size, { landscape: { playTablets: true } }); const r = await render(style, d.w, d.h, { landscape: true, frame }); expect(r.frameUrl).toContain('/landscape/'); expect(r.titlebar).toBe(frame === 'window'); expect(r.devices).toHaveLength(1); expect(boxOutside(r.devices[0]!, d.w, d.h)).toEqual([]); // copy on the left, device on the right expect(Math.max(...r.boxes.map(b => b.x + b.w))).toBeLessThan(r.devices[0]!.x); expect(r.boxes.length).toBeGreaterThanOrEqual(3); expect(marginViolations(r.boxes, d.w, d.h)).toEqual([]); }, scaled(30_000)); }); describe.skipIf(!browser)('safe margins on the portrait templates', () => { for (const style of ['game', 'app'] as const) for (const [w, h] of [[1320, 2868], [1080, 1920], [2064, 2752]] as const) test(`${style} ${w}x${h}, hero with logo and a scene`, async () => { for (const hero of [true, false]) { const r = await render(style, w, h, { hero, title: ['Wonderful long words', 'Everywhere'] }); expect(r.boxes.length).toBeGreaterThan(0); if (hero) expect(r.boxes.some(b => b.el === 'logo')).toBe(true); expect(marginViolations(r.boxes, w, h)).toEqual([]); } }, scaled(30_000)); test('a fixture with the margins taken away fails', async () => { const r = await render('game', 1080, 1920, { hero: true, css: '.top { padding: 0 !important; top: 0 !important } .logo { width: 100% !important }' }); const v = marginViolations(r.boxes, 1080, 1920); expect(v.some(m => m.startsWith('logo is too close to the edge (left 0 px, right 0 px, top 0 px'))).toBe(true); }, scaled(30_000)); });