shots: Mac App Store screenshots (mac size, APP_DESKTOP to the macOS version), a landscape layout (Mac window or bare; Mac, Play Chromebook, Play tablets), safe margins in every template checked by shots check from the render's layout boxes, a contact sheet timeout that scales with images, sizes and load; pitfall 80

This commit is contained in:
CRTkafa 2026-10-20 03:29:19 +07:00
parent 30488c512d
commit e59539fec8
26 changed files with 532 additions and 67 deletions

View file

@ -0,0 +1,74 @@
// 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('</style>', `${o.css}</style>`);
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<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 === '/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));
});