import { describe, expect, test } from 'bun:test'; import { displaySets, duoDirs } from '../cli/apple/screenshots'; import { duoGroups, duoUpToDate } from '../cli/apple/duo-library'; import { appleRules } from '../cli/lib/validate-rules'; import { checkCreate, createExperimentReq, defaultName, experimentFromApi, pickExperiment, planTreatmentLocales, planTreatments, startStopReq } from '../cli/lib/ppo'; import { validateConfig } from '../cli/lib/config'; import { FORBIDDEN_ARGS, TOOLS, toArgv, validateInput } from '../cli/mcp-tools'; import { requiredSets } from '../cli/shots/report'; import { defaultSizes, SIZES } from '../cli/shots/lib'; import { checkImage, IMAGE_SPECS, imageInfo, IPHONE_DUO_DISPLAY, iphoneDuoNote } from '../cli/shots/spec'; import { png } from './fixtures'; describe('product page optimization', () => { test('create options', () => { expect(checkCreate(['b'], 30, ['b', 'c'])).toEqual([]); expect(checkCreate([], 30, ['b']).join()).toContain('--variant'); expect(checkCreate(['b', 'c', 'd', 'e'], 30, ['b', 'c', 'd', 'e']).join()).toContain('at most 3'); expect(checkCreate(['z'], 30, ['b']).join()).toContain('unknown variant "z"'); expect(checkCreate(['b'], 0, ['b']).join()).toContain('--traffic'); expect(defaultName(['b', 'c'])).toBe('storebound b + c'); expect(createExperimentReq('app1', 'T', 30).body).toEqual({ data: { type: 'appStoreVersionExperiments', attributes: { name: 'T', platform: 'IOS', trafficProportion: 30 }, relationships: { app: { data: { type: 'apps', id: 'app1' } } } } }); }); test('picking a test', () => { const list = [ experimentFromApi({ id: 'e1', attributes: { name: 'old', state: 'COMPLETED' } }), experimentFromApi({ id: 'e2', attributes: { name: 'new', state: 'PREPARE_FOR_SUBMISSION', trafficProportion: 30 } }), ]; expect(pickExperiment(list)!.id).toBe('e2'); expect(pickExperiment(list, 'new')!.trafficProportion).toBe(30); expect(pickExperiment(list, 'missing')).toBeNull(); expect(() => pickExperiment(list, 'old')).toThrow('cannot be restarted'); expect(() => pickExperiment([...list, experimentFromApi({ id: 'e3', attributes: { name: 'x', state: 'READY_FOR_REVIEW' } })])).toThrow('--experiment'); }); test('treatments and localizations', () => { const t = planTreatments('e2', [{ id: 't1', name: 'b' }], ['b', 'c']); expect(t).toHaveLength(1); expect(t[0]!.body).toEqual({ data: { type: 'appStoreVersionExperimentTreatments', attributes: { name: 'c' }, relationships: { appStoreVersionExperimentV2: { data: { type: 'appStoreVersionExperiments', id: 'e2' } } } } }); expect(() => planTreatments('e2', [{ id: '1', name: 'a' }, { id: '2', name: 'b' }, { id: '3', name: 'c' }], ['d'])).toThrow('at most 3'); const l = planTreatmentLocales('t1', ['en-US'], ['en-US', 'tr']); expect(l.map(r => (r.body as { data: { attributes: { locale: string } } }).data.attributes.locale)).toEqual(['tr']); expect((l[0]!.body as { data: { relationships: unknown } }).data.relationships).toEqual({ appStoreVersionExperimentTreatment: { data: { type: 'appStoreVersionExperimentTreatments', id: 't1' } } }); }); test('start / stop', () => { const e = experimentFromApi({ id: 'e2', attributes: { name: 'new', state: 'PREPARE_FOR_SUBMISSION' } }); expect(startStopReq(e, true)).toMatchObject({ method: 'PATCH', path: '/v2/appStoreVersionExperiments/e2', body: { data: { type: 'appStoreVersionExperiments', id: 'e2', attributes: { started: true } } } }); expect(() => startStopReq({ ...e, state: 'STOPPED' }, false)).toThrow('already STOPPED'); expect(() => startStopReq({ ...e, state: 'COMPLETED' }, true)).toThrow('cannot start'); }); test('mcp: status / create / upload, never start or stop', () => { const t = (n: string) => TOOLS.find(x => x.name === n)!; expect(t('apple_ppo_status').write).toBe(false); expect(toArgv(t('apple_ppo_create'), { variants: 'b,c', traffic: 20 }).argv).toEqual(['apple', 'ppo', 'create', '--variant', 'b,c', '--traffic', '20', '--dry']); expect(toArgv(t('apple_ppo_upload'), { variant: 'b', only: 'en-US' }).argv).toEqual(['apple', 'ppo', 'upload', '--variant', 'b', '--only', 'en-US', '--dry']); expect(validateInput(t('apple_ppo_create'), { variants: 'a,b,c,d' })[0]).toContain('does not match'); expect(FORBIDDEN_ARGS).toEqual(expect.arrayContaining(['start', 'stop'])); }); }); describe('optional 6.3" iPhone size (iphone-medium)', () => { const base = { app: { name: 'X', privacyPolicyUrl: 'https://example.com/p', supportUrl: 'https://example.com', website: 'https://example.com', contactEmail: 'a@example.com' }, languages: [{ id: 'en', apple: 'en-US' }] }; test('off by default, on with apple.iphoneMedium', () => { expect(SIZES['iphone-medium']).toEqual({ w: 1206, h: 2622 }); expect(defaultSizes({ apple: { bundleId: 'x' } })).not.toContain('iphone-medium'); expect(defaultSizes({ apple: { bundleId: 'x', iphoneMedium: true } })).toContain('iphone-medium'); expect(defaultSizes({ play: { packageName: 'x' } })).not.toContain('iphone-medium'); expect(Object.keys(displaySets({}))).toEqual(['ios69', 'ios65']); expect(displaySets({ iphoneMedium: true, ipad: true })).toEqual({ ios69: 'APP_IPHONE_67', ios65: 'APP_IPHONE_65', 'iphone-medium': 'APP_IPHONE_61', ipad13: 'APP_IPAD_PRO_3GEN_129' }); expect(requiredSets({ apple: {} })['iphone-medium']).toBeUndefined(); expect(requiredSets({ apple: { iphoneMedium: true } })['iphone-medium']).toBe(true); expect(validateConfig({ ...base, apple: { bundleId: 'com.example.game', iphoneMedium: true } })).toEqual([]); }); test('accepted pixel sizes', () => { expect(IMAGE_SPECS['iphone-medium']!.type).toBe('APP_IPHONE_61'); expect(checkImage(imageInfo(png(1206, 2622)), 'iphone-medium')).toEqual([]); expect(checkImage(imageInfo(png(1179, 2556)), 'iphone-medium')).toEqual([]); expect(checkImage(imageInfo(png(1320, 2868)), 'iphone-medium').map(i => i.code)).toEqual(['size']); }); }); describe('iPhone Duo (foldable) sizes', () => { test('device table: outer and inner, portrait and landscape; opt-in with apple.iphoneDuo', () => { expect(SIZES['iphone-duo-outer']).toMatchObject({ w: 1398, h: 2034 }); expect(SIZES['iphone-duo-inner']).toMatchObject({ w: 2007, h: 2853 }); expect(defaultSizes({ apple: { bundleId: 'x' } })).not.toContain('iphone-duo-outer'); expect(defaultSizes({ apple: { bundleId: 'x', iphoneDuo: true } })).toEqual(expect.arrayContaining(['iphone-duo-outer', 'iphone-duo-inner'])); expect(defaultSizes({ play: { packageName: 'x' }, apple: undefined })).not.toContain('iphone-duo-inner'); }); test('shots check: the 4 exact sizes pass, others and alpha fail', () => { for (const [k, w, h] of [['iphone-duo-outer', 1398, 2034], ['iphone-duo-outer', 2034, 1398], ['iphone-duo-inner', 2007, 2853], ['iphone-duo-inner', 2853, 2007]] as const) expect(checkImage(imageInfo(png(w, h)), k)).toEqual([]); expect(checkImage(imageInfo(png(1398, 2035)), 'iphone-duo-outer').map(i => i.code)).toEqual(['size']); expect(checkImage(imageInfo(png(2007, 2853)), 'iphone-duo-outer').map(i => i.code)).toEqual(['size']); expect(checkImage(imageInfo(png(2007, 2853, { colorType: 6 })), 'iphone-duo-inner').map(i => i.code)).toEqual(['alpha']); }); test('upload: no ScreenshotDisplayType; Duo goes through the App Asset Library (IPHONE_DUO group)', () => { expect(Object.values(IPHONE_DUO_DISPLAY).every(t => t === null)).toBe(true); expect(Object.keys(displaySets({ iphoneDuo: true }))).not.toContain('iphone-duo-outer'); expect(duoDirs({})).toEqual([]); expect(duoDirs({ iphoneDuo: true })).toEqual(['iphone-duo-outer', 'iphone-duo-inner']); const ref = [{ attributes: { placementProfileGroups: [ { placementProfileGroupId: 'g-67', platform: 'IOS', displayClassId: 'IPHONE_DYNAMIC_ISLAND_LARGE_DISPLAY' }, { placementProfileGroupId: 'g-duo', platform: 'IOS', displayClassId: 'IPHONE_DUO' }, { placementProfileGroupId: 'g-duo', platform: 'IOS', displayClassId: 'IPHONE_DUO' }, { placementProfileGroupId: 'g-mac', platform: 'MAC_OS', displayClassId: 'IPHONE_DUO' }] } }]; expect(duoGroups(ref)).toEqual(['g-duo']); expect(duoGroups([])).toEqual([]); expect(duoGroups([{ attributes: { placementProfileGroups: [{ placementProfileGroupId: 'IPHONE_DUO_PROFILE', platform: 'IPHONE_APP_STORE', displayClassId: 'IPHONE_DUO' }] } }])).toEqual(['IPHONE_DUO_PROFILE']); expect(duoUpToDate(['a', 'b'], ['a', 'b'])).toBe(true); expect(duoUpToDate(['b', 'a'], ['a', 'b'])).toBe(false); expect(duoUpToDate(['a', undefined], ['a', 'b'])).toBe(false); }); test('check / validate note: info before April 2027, never a blocker; points Play to the tablet sets', () => { const before = new Date('2026-10-18'), after = new Date('2027-04-02'); expect(iphoneDuoNote({ apple: true, duo: true })).toBeNull(); expect(iphoneDuoNote({ apple: false, duo: false })).toBeNull(); const n = iphoneDuoNote({ apple: true, duo: false, play: true, today: before })!; expect(n.level).toBe('info'); expect(n.msg).toContain('optional now, required from April 2027 for iOS 27.1 SDK builds'); expect(n.msg).toContain('tablet screenshots'); expect(n.msg).toContain('apple screenshots uploads them through the App Asset Library'); expect(iphoneDuoNote({ apple: true, duo: false, today: after })!.level).toBe('warn'); const base = { version: null, build: null, locales: [], expectedLocales: [], requiredDisplayTypes: [], reviewDetail: null, ageRating: null, priceSchedule: true, availability: true, iaps: [], expectedProducts: [], privacyUrl: { url: 'https://example.com/privacy', status: 200 } }; const f = appleRules({ ...base, today: before } as unknown as Parameters[0]).find(x => x.code === 'screenshots-duo')!; expect(f.level).toBe('ok'); expect(appleRules({ ...base, iphoneDuo: true, today: before } as unknown as Parameters[0]).some(x => x.code === 'screenshots-duo')).toBe(false); }); test('validate: the medium display class is required; a 6.9" set fills it (scaled down), none is a blocker', () => { const base = { version: null, build: null, expectedLocales: [], requiredDisplayTypes: ['APP_IPHONE_61'], reviewDetail: null, ageRating: null, priceSchedule: true, availability: true, iaps: [], expectedProducts: [], privacyUrl: { url: 'https://example.com/privacy', status: 200 } }; const run = (screenshots: Record) => appleRules({ ...base, locales: [{ locale: 'en-US', description: 'd', keywords: 'k', subtitle: 's', screenshots }] } as unknown as Parameters[0]); expect(run({ APP_IPHONE_67: 5 }).find(x => x.code === 'screenshots-size')!.level).toBe('ok'); expect(run({}).find(x => x.code === 'screenshots')!.level).toBe('blocker'); }); });