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:
parent
30488c512d
commit
e59539fec8
26 changed files with 532 additions and 67 deletions
74
tests/browser/landscape.browser.ts
Normal file
74
tests/browser/landscape.browser.ts
Normal 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));
|
||||
});
|
||||
|
|
@ -170,6 +170,22 @@ describe('App Store Connect: a second run writes nothing', () => {
|
|||
expect(count('appScreenshots')).toBe(12);
|
||||
});
|
||||
|
||||
test('apple screenshots --platform macos: the Mac set (APP_DESKTOP) on the macOS version only', async () => {
|
||||
const files: Record<string, Uint8Array> = {};
|
||||
for (const l of ['en', 'de']) for (const n of [1, 2]) files[`store/renders/${l}/mac/0${n}.png`] = png(n + (l === 'de' ? 40 : 30));
|
||||
const p = project({ ...CONFIG, apple: { ...CONFIG.apple!, mac: true } }, files);
|
||||
const ios = s.asc.put({ type: 'appStoreVersions', id: 'ver-ios', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
|
||||
const mac = s.asc.put({ type: 'appStoreVersions', id: 'ver-mac', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'MAC_OS' }, rels: { app: APP } });
|
||||
for (const v of [ios, mac]) for (const loc of ['en-US', 'de-DE']) s.asc.put({ type: 'appStoreVersionLocalizations', id: `loc-${v.id}-${loc}`, attributes: { locale: loc }, rels: { appStoreVersion: v.id } });
|
||||
const r = await twice(() => appleScreenshots(p.ctx(['--platform', 'macos'])));
|
||||
expect(r.first.filter(x => x === 'POST /v1/appScreenshotSets')).toHaveLength(2);
|
||||
expect(r.second).toEqual([]);
|
||||
const sets = s.asc.all('appScreenshotSets');
|
||||
expect(sets.map(x => x.attributes.screenshotDisplayType)).toEqual(['APP_DESKTOP', 'APP_DESKTOP']);
|
||||
expect(sets.every(x => JSON.stringify(x).includes('loc-ver-mac-'))).toBe(true);
|
||||
expect(count('appScreenshots')).toBe(4);
|
||||
});
|
||||
|
||||
test('apple cpp create, events create, testflight groups, review-details, bundle, offer-codes, ppo create', async () => {
|
||||
const p = project({ ...CONFIG, shots: { variants: { b: {} } } as never }, { '.storebound/credentials.json': JSON.stringify({ reviewContact: { firstName: 'Ex', lastName: 'Ample', phone: '+1 555 0100', email: 'review@example.com' } }) });
|
||||
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
|
||||
|
|
|
|||
105
tests/mac-landscape.test.ts
Normal file
105
tests/mac-landscape.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Mac App Store screenshots (APP_DESKTOP, 16:10), the landscape layout's sizes, the safe-margin rule and the contact
|
||||
// sheet timeout. The page-level render of the landscape layout and the margins: tests/browser/landscape.browser.ts.
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { checkImage, IMAGE_SPECS, imageInfo, macNote } from '../cli/shots/spec';
|
||||
import { defaultSizes, marginViolations, renderDims, SAFE_MARGIN_MIN, sheetLayout, sheetTimeoutMs, SIZES } from '../cli/shots/lib';
|
||||
import { marginIssues, requiredSets } from '../cli/shots/report';
|
||||
import { displaySets, MAC_DISPLAY } from '../cli/apple/screenshots';
|
||||
import { chromebookSkip } from '../cli/play/images';
|
||||
import { APPLE_SHOT_DIR } from '../cli/lib/art-pull';
|
||||
import { validateConfig } from '../cli/lib/config';
|
||||
import { loadFactor } from '../cli/shots/render';
|
||||
import { png } from './fixtures';
|
||||
|
||||
describe('Mac sizes', () => {
|
||||
test('the four 16:10 sizes Apple lists, landscape only, APP_DESKTOP', () => {
|
||||
expect(IMAGE_SPECS.mac!.sizes).toEqual([[2880, 1800], [2560, 1600], [1440, 900], [1280, 800]]);
|
||||
expect(IMAGE_SPECS.mac!.type).toBe('APP_DESKTOP');
|
||||
expect(SIZES.mac).toMatchObject({ w: 2880, h: 1800, device: 'landscape' });
|
||||
for (const [w, h] of IMAGE_SPECS.mac!.sizes) expect(checkImage(imageInfo(png(w, h)), 'mac')).toEqual([]);
|
||||
expect(checkImage(imageInfo(png(1800, 2880)), 'mac').map(i => i.code)).toEqual(['size']);
|
||||
expect(checkImage(imageInfo(png(1920, 1080)), 'mac').map(i => i.code)).toEqual(['size']);
|
||||
expect(checkImage(imageInfo(png(2880, 1800, { colorType: 6 })), 'mac').map(i => i.code)).toEqual(['alpha']);
|
||||
});
|
||||
test('opt-in with apple.mac; required then', () => {
|
||||
expect(defaultSizes({ apple: { bundleId: 'x' } } as never)).not.toContain('mac');
|
||||
expect(defaultSizes({ apple: { bundleId: 'x', mac: true } } as never)).toContain('mac');
|
||||
expect(requiredSets({ apple: { mac: true } }).mac).toBe(true);
|
||||
expect(requiredSets({ apple: {} }).mac).toBeUndefined();
|
||||
});
|
||||
test('uploads: the Mac set only to a macOS version', () => {
|
||||
expect(MAC_DISPLAY).toEqual({ mac: 'APP_DESKTOP' });
|
||||
expect(displaySets({ mac: true }, 'MAC_OS')).toEqual({ mac: 'APP_DESKTOP' });
|
||||
expect(displaySets({ mac: true }, 'IOS')).not.toHaveProperty('mac');
|
||||
expect(displaySets({}, 'MAC_OS')).toEqual({});
|
||||
expect(APPLE_SHOT_DIR.APP_DESKTOP).toBe('mac');
|
||||
});
|
||||
test('validate note: a macOS version without Mac screenshots', () => {
|
||||
expect(macNote({ macVersion: true, mac: false })).toContain('apple.mac: true');
|
||||
expect(macNote({ macVersion: true, mac: true })).toBeNull();
|
||||
expect(macNote({ macVersion: false, mac: false })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('landscape layout sizes', () => {
|
||||
test('mac: window frame by default; chromebook and Play tablets: bare', () => {
|
||||
expect(renderDims('mac', undefined)).toEqual({ w: 2880, h: 1800, landscape: true, frame: 'window' });
|
||||
expect(renderDims('chromebook', undefined)).toEqual({ w: 1920, h: 1080, landscape: true, frame: 'none' });
|
||||
expect(renderDims('tab10', undefined)).toMatchObject({ w: 1440, h: 2560, landscape: false });
|
||||
expect(renderDims('tab10', { landscape: { playTablets: true } })).toMatchObject({ w: 2560, h: 1440, landscape: true, frame: 'none' });
|
||||
expect(renderDims('mac', { landscape: { frame: 'none' } }).frame).toBe('none');
|
||||
for (const k of ['tab7', 'tab10']) { const d = renderDims(k, { landscape: { playTablets: true } }); expect(checkImage(imageInfo(png(d.w, d.h)), k)).toEqual([]); }
|
||||
});
|
||||
test('chromebook: opt-in with play.chromebook, a check, and a console note instead of an upload', () => {
|
||||
expect(defaultSizes({ play: { packageName: 'x' } } as never)).not.toContain('chromebook');
|
||||
expect(defaultSizes({ play: { packageName: 'x', chromebook: true } } as never)).toContain('chromebook');
|
||||
expect(checkImage(imageInfo(png(1920, 1080)), 'chromebook')).toEqual([]);
|
||||
expect(chromebookSkip(true)).toContain('Main store listing');
|
||||
expect(chromebookSkip(false)).toBeNull();
|
||||
});
|
||||
test('config: frame and side are validated', () => {
|
||||
const errs = (landscape: unknown) => validateConfig({ app: { name: 'X', profile: 'game' }, languages: [{ id: 'en' }],
|
||||
shots: { copy: 'c.json', frames: 'f', out: 'o', scenes: [{ id: 'a', frames: [{ src: 'a.png' }] }], landscape } } as never).filter(e => e.startsWith('shots.landscape'));
|
||||
expect(errs({ frame: 'window', side: 'right', playTablets: true })).toEqual([]);
|
||||
expect(errs({ frame: 'macbook' })).toEqual(["shots.landscape.frame: 'window' | 'none'"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safe margins', () => {
|
||||
const W = 1320, H = 2868;
|
||||
test('boxes inside the margins pass; text at the edge fails', () => {
|
||||
expect(marginViolations([{ el: 'title', x: W * .07, y: H * .05, w: W * .86, h: 200 }], W, H)).toEqual([]);
|
||||
const v = marginViolations([{ el: 'title', x: 12, y: 300, w: 1290, h: 200 }, { el: 'logo', x: 200, y: 20, w: 900, h: 300 }], W, H);
|
||||
expect(v).toHaveLength(2);
|
||||
expect(v[0]).toContain('title is too close to the edge (left 12 px, right 18 px');
|
||||
expect(v[1]).toContain('logo is too close to the edge (top 20 px');
|
||||
expect(Math.round(W * SAFE_MARGIN_MIN.x)).toBe(79);
|
||||
});
|
||||
test('shots check: an error per box, none without layout data', () => {
|
||||
const e = marginIssues({ overflow: [], boxes: [{ el: 'subtitle', x: 0, y: 900, w: 500, h: 60 }] }, W, H);
|
||||
expect(e.map(i => [i.level, i.code])).toEqual([['error', 'margin']]);
|
||||
expect(marginIssues({ overflow: [] }, W, H)).toEqual([]);
|
||||
expect(marginIssues(undefined, W, H)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('contact sheet timeout', () => {
|
||||
test('scales with images, sizes and load; never a flat 30 s', () => {
|
||||
const nine = sheetTimeoutMs(9 * 5, 9);
|
||||
expect(nine).toBeGreaterThan(30_000 * 3);
|
||||
expect(sheetTimeoutMs(90, 9)).toBeGreaterThan(nine);
|
||||
expect(sheetTimeoutMs(45, 9, 3)).toBe(nine * 3);
|
||||
expect(sheetTimeoutMs(45, 9, 50)).toBe(nine * 5);
|
||||
expect(sheetTimeoutMs(1, 1, 0.2)).toBe(sheetTimeoutMs(1, 1));
|
||||
});
|
||||
test('load factor: forced by env, clamped 1..5', () => {
|
||||
expect(loadFactor({ STOREBOUND_LOAD_FACTOR: '3' })).toBe(3);
|
||||
expect(loadFactor({}, [40, 20, 0], 10)).toBe(4);
|
||||
expect(loadFactor({}, [0, 0, 0], 10)).toBe(1);
|
||||
expect(loadFactor({}, [500, 0, 0], 10)).toBe(5);
|
||||
});
|
||||
test('landscape tiles take half height on the sheet', () => {
|
||||
const l = sheetLayout([{ size: 'mac', files: ['a'], dims: { w: 2880, h: 1800 } }]);
|
||||
expect(l.tiles[0]).toMatchObject({ h: 450, w: 720 });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue