// apple.macos: per-platform listing file and review notes for an app with an iOS and a macOS App Store version, the // config check, and the validate warning for iOS-only claims in a macOS version's texts. import { describe, expect, test } from 'bun:test'; import { isMacosPlatform, listingPathFor, reviewNotesFor, validateExtras, type ShipConfig } from '../cli/lib/config'; import { reviewDetailPlan, reviewNotes } from '../cli/lib/review-details'; import { appleRules, macosClaimRules, type AppleSnapshot } from '../cli/lib/validate-rules'; const cfg = (apple: Record) => ({ apple: { bundleId: 'com.example.app', ...apple } }) as unknown as Pick; const contact = { contactFirstName: 'A', contactLastName: 'B', contactPhone: '+10000000000', contactEmail: 'a@example.com' }; describe('platform helpers', () => { test('spellings of macOS', () => { for (const p of ['macos', 'MAC_OS', 'macOS', 'mac']) expect(isMacosPlatform(p)).toBe(true); for (const p of ['ios', 'IOS', 'tvos', '', undefined]) expect(isMacosPlatform(p)).toBe(false); }); test('listingPathFor: macOS override, else apple.listing, else the default', () => { const c = cfg({ listing: 'store/ios.md', macos: { listing: 'store/mac.md' } }); expect(listingPathFor(c, 'macos')).toBe('store/mac.md'); expect(listingPathFor(c, 'MAC_OS')).toBe('store/mac.md'); expect(listingPathFor(c, 'ios')).toBe('store/ios.md'); expect(listingPathFor(c)).toBe('store/ios.md'); expect(listingPathFor(cfg({ macos: {} }), 'macos')).toBe('store/appstore.md'); expect(listingPathFor(cfg({ listing: 'store/ios.md', macos: { reviewNotes: 'x' } }), 'macos')).toBe('store/ios.md'); }); test('reviewNotesFor: macOS override, else apple.info.reviewNotes', () => { const c = cfg({ info: { reviewNotes: 'iOS notes' }, macos: { reviewNotes: 'Mac notes' } }); expect(reviewNotesFor(c, 'macos')).toBe('Mac notes'); expect(reviewNotesFor(c, 'ios')).toBe('iOS notes'); expect(reviewNotesFor(cfg({ info: { reviewNotes: 'iOS notes' }, macos: {} }), 'macos')).toBe('iOS notes'); }); }); describe('review details plan', () => { const c = { ...cfg({ info: { reviewNotes: 'iOS notes: Game Center, IAP' }, macos: { reviewNotes: 'Mac notes: no purchases' } }), reviewAccess: { signInRequired: false, instructions: 'No sign-in.' } } as Pick; test('macos picks the Mac notes, ios and default keep the iOS notes; the shared sign-in line stays', () => { expect(reviewDetailPlan(c, contact, null, 'macos').attributes.notes).toBe('Mac notes: no purchases\n\nSign-in: No sign-in.'); expect(reviewDetailPlan(c, contact, null, 'ios').attributes.notes).toBe('iOS notes: Game Center, IAP\n\nSign-in: No sign-in.'); expect(reviewDetailPlan(c, contact, null).attributes.notes).toBe(reviewNotes(c)); }); }); describe('config check', () => { test('apple.macos shape', () => { expect(validateExtras({ apple: { bundleId: 'com.example.app', macos: { listing: 'a.md', reviewNotes: 'x', gameCenter: true, noIap: true } } } as never)).toEqual([]); const bad = validateExtras({ apple: { bundleId: 'com.example.app', macos: { listing: '', gameCenter: 'yes', noIap: 1 } } } as never); expect(bad.some(m => m.startsWith('apple.macos.listing'))).toBe(true); expect(bad.some(m => m.startsWith('apple.macos.gameCenter'))).toBe(true); expect(bad.some(m => m.startsWith('apple.macos.noIap'))).toBe(true); expect(validateExtras({ apple: { bundleId: 'com.example.app', macos: 'x' } } as never).some(m => m.startsWith('apple.macos:'))).toBe(true); }); }); describe('validate: iOS-only claims on a macOS version', () => { const loc = (o: Record) => [{ locale: 'en-US', screenshots: {}, ...o }]; const snap = (over: Partial): Pick => ({ version: { versionString: '1.0.0', state: 'PREPARE_FOR_SUBMISSION', platform: 'MAC_OS' }, locales: [], reviewDetail: null, ...over }); const codes = (f: { msg: string }[]) => f.map(x => x.msg); test('Game Center, touch words and iPhone mentions warn; nothing blocks', () => { const f = macosClaimRules(snap({ locales: loc({ description: 'Compete on Game Center. Tap and swipe to play on your iPhone.' }) })); expect(f.length).toBe(3); expect(f.every(x => x.level === 'warn' && x.code === 'macos-claims')).toBe(true); expect(codes(f).join('\n')).toContain('en-US description'); }); test('promotional text, what\'s new, keywords and live review notes are read', () => { const f = macosClaimRules(snap({ locales: loc({ promotionalText: 'Use the touch screen', whatsNew: 'Pinch to zoom', keywords: 'ipad' }), reviewDetail: { notes: 'Sign in with Game Center' } })); const all = codes(f).join('\n'); expect(all).toContain('en-US promotionalText'); expect(all).toContain('en-US whatsNew'); expect(all).toContain('en-US keywords'); expect(all).toContain('review notes (on the store)'); }); test('the config review notes the Mac would use are checked too', () => { expect(codes(macosClaimRules(snap({ macos: { notes: 'Game Center leaderboards are in the menu' } }))).join()).toContain('review notes (config)'); }); test('apple.macos.gameCenter silences Game Center; IAP lines only with noIap', () => { const text = loc({ description: 'Game Center scores. Remove ads with an in-app purchase.' }); expect(macosClaimRules(snap({ locales: text, macos: { gameCenter: true } }))).toEqual([]); const f = macosClaimRules(snap({ locales: text, macos: { gameCenter: true, noIap: true } })); expect(f.length).toBe(1); expect(f[0]!.msg).toContain('in-app purchase'); }); test('words that only look similar do not match; iOS versions are never checked', () => { expect(macosClaimRules(snap({ locales: loc({ description: 'Contact the support team; a stapler, a tapestry, an iPhoneless desk.' }) }))).toEqual([]); expect(macosClaimRules(snap({ version: { versionString: '1.0.0', state: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, locales: loc({ description: 'Game Center, tap' }) }))).toEqual([]); }); test('appleRules includes the warning', () => { const base = { expectedLocales: [], requiredDisplayTypes: [], iaps: [], expectedProducts: [], ageRating: null, priceSchedule: false, availability: false, build: null, privacyUrl: { url: 'https://example.com', status: 200 } }; const f = appleRules({ ...base, ...snap({ locales: loc({ description: 'Tap here', screenshots: {} as never }) }) } as AppleSnapshot); expect(f.some(x => x.code === 'macos-claims' && x.level === 'warn')).toBe(true); }); }); describe('apple listing reads the platform file', () => { test('readAppleCopy(ctx, "macos") reads apple.macos.listing, ios reads apple.listing', async () => { const { mkdtempSync, writeFileSync } = await import('node:fs'); const { tmpdir } = await import('node:os'); const { join, resolve } = await import('node:path'); const { readAppleCopy } = await import('../cli/apple/listing'); const { parseArgs } = await import('../cli/lib/args'); const fence = '`'.repeat(3); const sec = (d: string) => ['## en-US', '', '### subtitle', fence, 'Sub', fence, '', '### promotionalText', fence, 'Promo', fence, '', '### keywords', fence, 'run,time', fence, '', '### description', fence, d, fence, ''].join('\n'); const dir = mkdtempSync(join(tmpdir(), 'sb-macos-')); writeFileSync(join(dir, 'ios.md'), sec('Play on iPhone with Game Center.')); writeFileSync(join(dir, 'mac.md'), sec('Play on your Mac.')); const loaded = { config: cfg({ listing: 'ios.md', macos: { listing: 'mac.md' } }), resolve: (p: string) => resolve(dir, p), root: dir }; const ctx = { loaded, args: parseArgs([]) } as never; expect(readAppleCopy(ctx, 'macos')['en-US']?.description).toContain('Mac'); expect(readAppleCopy(ctx, 'ios')['en-US']?.description).toContain('iPhone'); expect(readAppleCopy(ctx)['en-US']?.description).toContain('iPhone'); }); });