validate: removed-from-sale IAPs are no blockers; shots capture --device landscape (Mac / Chromebook frames from a web build); playwright lookup says why each place failed
This commit is contained in:
parent
cdd9321794
commit
d0f568ebc2
13 changed files with 230 additions and 48 deletions
101
tests/capture-landscape.test.ts
Normal file
101
tests/capture-landscape.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
// `shots capture --device landscape`: desktop frames for Mac App Store / Chromebook screenshots from a web build.
|
||||
// Dry runs and pure planning only: no browser, no simulator.
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { rmSync } from 'node:fs';
|
||||
import type { ShipConfig } from '../cli/lib/config';
|
||||
import { planWeb } from '../cli/shots/capture-web';
|
||||
import { run as captureRun } from '../cli/shots/capture';
|
||||
import { shotsFor, validateCapture } from '../cli/shots/devices';
|
||||
import { frameDims, landscapeDims, rendersLandscape } from '../cli/shots/lib';
|
||||
import { BASE, capture, project, restore } from './helpers/store-mock';
|
||||
|
||||
afterEach(restore);
|
||||
|
||||
const cap = { source: 'web', baseUrl: 'http://127.0.0.1:5173', shots: {
|
||||
hero: { path: '/?scene=hero' }, wide: { path: '/?scene=wide', device: 'landscape' }, pad: { path: '/pad', device: 'tablet' }, both: { path: '/both', device: 'both' }, ph: { path: '/ph', device: 'phone' },
|
||||
} } as const;
|
||||
const cfg = (over: Record<string, unknown> = {}, shots: Record<string, unknown> = {}): ShipConfig =>
|
||||
({ ...BASE, ...over, shots: { frames: 'frames', copy: 'copy.json', scenes: [], capture: cap, ...shots } }) as unknown as ShipConfig;
|
||||
const names = (c: Parameters<typeof shotsFor>[0], d: 'phone' | 'tablet' | 'landscape', all = false) => shotsFor(c, d, [], all).map(([n]) => n);
|
||||
|
||||
describe('landscape device selection', () => {
|
||||
test('phone / tablet unchanged; landscape only for explicit shots unless the project renders landscape', () => {
|
||||
expect(names(cap, 'phone')).toEqual(['hero', 'both', 'ph']);
|
||||
expect(names(cap, 'tablet')).toEqual(['pad', 'both']);
|
||||
expect(names(cap, 'landscape')).toEqual(['wide']);
|
||||
expect(names(cap, 'landscape', true)).toEqual(['hero', 'wide', 'both']); // not the explicit phone / tablet ones
|
||||
});
|
||||
test('validateCapture accepts landscape and names it in the error', () => {
|
||||
expect(validateCapture({ ...cap, shots: { a: { path: '/', device: 'landscape' } } })).toEqual([]);
|
||||
expect(validateCapture({ ...cap, shots: { a: { path: '/', device: 'desktop' as 'both' } } }).join()).toContain('phone | tablet | both | landscape');
|
||||
});
|
||||
test('rendersLandscape follows the render rules (mac, chromebook, playTablets)', () => {
|
||||
expect(rendersLandscape(cfg())).toBe(false);
|
||||
expect(rendersLandscape(cfg({ apple: { ...BASE.apple!, mac: true } }))).toBe(true);
|
||||
expect(rendersLandscape(cfg({ apple: undefined, play: { ...BASE.play!, chromebook: true } }))).toBe(true);
|
||||
expect(rendersLandscape(cfg({}, { landscape: { playTablets: true } }))).toBe(true);
|
||||
});
|
||||
test('landscapeDims: 1440 x 900 @2x default, shots.landscape.size overrides', () => {
|
||||
expect(landscapeDims(cfg())).toEqual({ w: 1440, h: 900, dpr: 2, bar: 0 });
|
||||
expect(landscapeDims(cfg({}, { landscape: { size: { w: 1280, h: 800 } } }))).toEqual({ w: 1280, h: 800, dpr: 2, bar: 0 });
|
||||
expect(landscapeDims(cfg({}, { landscape: { size: { w: 1920, h: 1080, dpr: 1 } } }))).toEqual({ w: 1920, h: 1080, dpr: 1, bar: 0 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('planWeb with landscape', () => {
|
||||
test('jobs target <lang>/landscape/<name>.png and the hash tracks the landscape viewport', () => {
|
||||
const c = cfg();
|
||||
const base = { langs: ['en'], devices: ['landscape' as const], only: [], framesDir: '/f', build: 'b', allLandscape: true };
|
||||
const a = planWeb(cap, { ...base, dims: { ...frameDims(c), landscape: landscapeDims(c) } });
|
||||
expect(a.map(j => j.frame)).toEqual(['en/landscape/hero.png', 'en/landscape/wide.png', 'en/landscape/both.png']);
|
||||
const b = planWeb(cap, { ...base, dims: { ...frameDims(c), landscape: { w: 1280, h: 800, dpr: 2, bar: 0 } } });
|
||||
expect(b[0]!.hash).not.toBe(a[0]!.hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe('capture --dry', () => {
|
||||
const dry = async (config: ShipConfig, argv: string[]) => {
|
||||
const p = project(config);
|
||||
try { return await capture(() => captureRun(p.ctx(['--dry', ...argv], ['dry']))); } finally { rmSync(p.root, { recursive: true, force: true }); }
|
||||
};
|
||||
test('--device landscape lists landscape targets, no phone / tablet', async () => {
|
||||
const r = await dry(cfg(), ['--device', 'landscape', '--lang', 'en']);
|
||||
expect(r.out).toContain('en/landscape/wide.png <- http://127.0.0.1:5173/?scene=wide');
|
||||
expect(r.out).toContain('en/landscape/hero.png'); // --device landscape captures every shot without a phone / tablet device
|
||||
expect(r.out).not.toContain('en/phone/');
|
||||
expect(r.out).not.toContain('en/tablet/');
|
||||
expect(r.out).toContain('capture planned (dry run): 3 frames from web');
|
||||
});
|
||||
test('default devices add landscape only when the project renders a landscape size', async () => {
|
||||
const plain = await dry(cfg(), ['--lang', 'en']);
|
||||
expect(plain.out).not.toContain('en/landscape/hero.png');
|
||||
expect(plain.out).toContain('en/phone/hero.png');
|
||||
const mac = await dry(cfg({ apple: { ...BASE.apple!, mac: true } }), ['--lang', 'en']);
|
||||
expect(mac.out).toContain('en/phone/hero.png');
|
||||
expect(mac.out).toContain('en/tablet/pad.png');
|
||||
for (const n of ['hero', 'wide', 'both']) expect(mac.out).toContain(`en/landscape/${n}.png`);
|
||||
expect(mac.out).not.toContain('en/landscape/pad.png');
|
||||
expect(mac.out).not.toContain('en/landscape/ph.png');
|
||||
});
|
||||
test('shot selectors accept a landscape key and print the landscape viewport', async () => {
|
||||
const c = cfg({ apple: { ...BASE.apple!, mac: true } }, { capture: { ...cap, shots: { hero: { path: '/', waitSelector: { phone: '#m', landscape: '#desk' } } } } });
|
||||
const r = await dry(c, ['--lang', 'en']);
|
||||
expect(r.out).toContain('waitSelector on landscape 1440x900: #desk');
|
||||
expect(r.out).toContain('waitSelector on phone 440x902: #m');
|
||||
});
|
||||
test('ios-sim / android-emu skip landscape with a warning', async () => {
|
||||
const mac = { apple: { ...BASE.apple!, mac: true } };
|
||||
const sim = await dry(cfg(mac, { capture: { source: 'ios-sim', ios: { bundleId: 'com.example.app', device: 'iPhone 17 Pro Max' }, shots: { hero: { path: '/', device: 'landscape' } } } }), ['--device', 'landscape', '--lang', 'en']);
|
||||
expect(sim.err + sim.out).toContain('landscape frames (Mac, Chromebook) are not supported from ios-sim');
|
||||
expect(sim.out).toContain('0 frames');
|
||||
const and = await dry(cfg(mac, { capture: { source: 'android-emu', android: { package: 'com.example.app' }, shots: { hero: { path: '/' } } } }), ['--lang', 'en']);
|
||||
expect(and.err + and.out).toContain('not supported from android-emu');
|
||||
expect(and.out).not.toContain('landscape/hero');
|
||||
});
|
||||
test('an unknown device is refused', async () => {
|
||||
const p = project(cfg());
|
||||
try {
|
||||
await expect(captureRun(p.ctx(['--dry', '--device', 'desktop'], ['dry']))).rejects.toThrow('--device: phone | tablet | landscape');
|
||||
} finally { rmSync(p.root, { recursive: true, force: true }); }
|
||||
});
|
||||
});
|
||||
|
|
@ -290,6 +290,15 @@ describe('validate rules', () => {
|
|||
expect(f.find(x => x.code === 'availability')!.fix).toContain('Pricing and Availability');
|
||||
expect(f.findIndex(x => x.level === 'ok')).toBeGreaterThan(f.findLastIndex(x => x.level === 'blocker'));
|
||||
});
|
||||
test('apple: products removed from sale are not blockers, unless the config still sells them', () => {
|
||||
const s = appleOk();
|
||||
s.iaps = [...s.iaps, { productId: 'old1', state: 'DEVELOPER_REMOVED_FROM_SALE', reviewScreenshot: false }, { productId: 'old2', state: 'REMOVED_FROM_SALE', reviewScreenshot: false }];
|
||||
expect(appleRules(s).filter(f => f.level !== 'ok')).toEqual([]);
|
||||
expect(appleRules(s).find(f => f.code === 'iap')!.msg).toBe('1 in-app purchases ready (2 removed from sale, not checked)');
|
||||
s.expectedProducts = ['p1', 'old1'];
|
||||
const f = appleRules(s).find(x => x.code === 'iap')!;
|
||||
expect([f.level, f.msg]).toEqual(['warn', 'old1: DEVELOPER_REMOVED_FROM_SALE, but the config still sells it']);
|
||||
});
|
||||
test('apple: non-exempt needs an approved declaration', () => {
|
||||
const s = appleOk();
|
||||
s.build = { ...s.build!, usesNonExemptEncryption: true, declarationState: 'IN_REVIEW' };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue