iPhone Duo screenshots (render, check, upload skip with console link while the API has no display type) and the medium display class as the required iPhone set, labels as in the console

This commit is contained in:
CRTkafa 2026-10-18 13:07:15 +07:00
parent 0b32df57f0
commit 3381673ae6
17 changed files with 163 additions and 16 deletions

View file

@ -1,11 +1,12 @@
import { describe, expect, test } from 'bun:test';
import { displaySets } from '../cli/apple/screenshots';
import { displaySets, skippedDuoSets } from '../cli/apple/screenshots';
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 } from '../cli/shots/spec';
import { checkImage, IMAGE_SPECS, imageInfo, IPHONE_DUO_DISPLAY, IPHONE_DUO_NOT_IN_API, iphoneDuoNote } from '../cli/shots/spec';
import { png } from './fixtures';
describe('product page optimization', () => {
@ -74,3 +75,47 @@ describe('optional 6.3" iPhone size (iphone-medium)', () => {
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 API display type yet, so the sets are skipped with the message', () => {
expect(Object.values(IPHONE_DUO_DISPLAY).every(t => t === null)).toBe(true);
expect(Object.keys(displaySets({ iphoneDuo: true }))).not.toContain('iphone-duo-outer');
expect(skippedDuoSets({})).toBeNull();
expect(skippedDuoSets({ iphoneDuo: true })).toEqual({ dirs: ['iphone-duo-outer', 'iphone-duo-inner'], msg: expect.stringContaining(`iphone-duo-outer, iphone-duo-inner: ${IPHONE_DUO_NOT_IN_API}. Add them in App Store Connect > the iOS version page > App Previews and Screenshots > iPhone Duo: https://`) });
expect(skippedDuoSets({ iphoneDuo: true, appId: '1234567890' })!.msg).toContain('/apps/1234567890/distribution/ios/version/inflight');
});
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(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<typeof appleRules>[0]).find(x => x.code === 'screenshots-duo')!;
expect(f.level).toBe('ok');
expect(appleRules({ ...base, iphoneDuo: true, today: before } as unknown as Parameters<typeof appleRules>[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<string, number>) => appleRules({ ...base, locales: [{ locale: 'en-US', description: 'd', keywords: 'k', subtitle: 's', screenshots }] } as unknown as Parameters<typeof appleRules>[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');
});
});

View file

@ -105,8 +105,8 @@ describe('13" iPad', () => {
expect(displaySets({})).toEqual(DISPLAY);
expect(displaySets(undefined)).toEqual(DISPLAY);
expect(Object.keys(displaySets({ ipad: true }))).toEqual(['ios69', 'ios65', 'ipad13']);
expect(requiredDisplayTypes({})).toEqual(['APP_IPHONE_67']);
expect(requiredDisplayTypes({ ipad: true })).toEqual(['APP_IPHONE_67', 'APP_IPAD_PRO_3GEN_129']);
expect(requiredDisplayTypes({})).toEqual(['APP_IPHONE_61']);
expect(requiredDisplayTypes({ ipad: true })).toEqual(['APP_IPHONE_61', 'APP_IPAD_PRO_3GEN_129']);
expect(requiredDisplayTypes({ ipad: true, screenshotTypes: ['APP_IPHONE_65'] })).toEqual(['APP_IPHONE_65']);
expect(defaultSizes({ apple: { bundleId: 'x' } })).not.toContain('ipad13');
expect(defaultSizes({ apple: { bundleId: 'x', ipad: true } })).toContain('ipad13');