// App sections (docs IA): old tab ids and section links resolve to one place, the sections an app has, the "what's // next" line of each section and the sidebar's status line. import { describe, expect, test } from 'bun:test'; import type { StoreRelease } from '../src/api'; import type { Attention } from '../src/lib/model'; import { isGameSummary, sideStatus } from '../src/lib/model'; import { listingProblems, type SectionInput, sectionNext } from '../src/lib/sections'; import { APP_TABS, parseRouteJson, parseStart, placeOf, SECTION_TABS, SECTIONS, sectionOf, sectionsFor, subsOf } from '../src/nav'; const rel = (store: 'ios' | 'android', version: string, phase: StoreRelease['phase'], o: Partial = {}): StoreRelease => ({ store, version, phase, fraction: null, track: null, ...o }) as StoreRelease; const base: SectionInput = { project: true, att: [], all: [], next: null, summary: null, catalog: null, unsaved: 0, listings: null, reviews: null, games: null }; const att = (tone: Attention['tone'], tab: Attention['tab'], text = 'x'): Attention => ({ app: 'A', projectId: 'a', tone, text, tab }); describe('places', () => { test('every old tab id is in exactly one section', () => { for (const tab of APP_TABS) expect(SECTIONS.filter(s => SECTION_TABS[s].includes(tab))).toHaveLength(1); }); test('old tab ids, sections and section/sub links', () => { expect(placeOf('prices')).toBe('prices'); expect(placeOf('revenue')).toBe('catalog'); expect(placeOf('store/art')).toBe('art'); expect(placeOf('store/prices')).toBe('listings'); expect(placeOf('health')).toBe('health'); expect(placeOf('nope')).toBe('releases'); expect(parseStart('app:players/games:2', ['a', 'b'])).toEqual({ screen: 'app', appId: 'b', tab: 'games' }); expect(parseStart('app:release', ['a'])).toEqual({ screen: 'app', appId: 'a', tab: 'releases' }); expect(parseRouteJson('{"screen":"app","appId":"k","tab":"store/appinfo"}')).toEqual({ screen: 'app', appId: 'k', tab: 'appinfo' }); expect(sectionOf('testflight')).toBe('release'); }); test('store-only apps have no Revenue; Game services only for games with a folder', () => { expect(sectionsFor(false)).toEqual(['release', 'store', 'players', 'health']); expect(subsOf('players', true, false)).toEqual(['reviews']); expect(subsOf('players', true, true)).toEqual(['reviews', 'games']); expect(subsOf('players', false, true)).toEqual(['reviews']); expect(subsOf('store', false)).toEqual(['listings', 'art']); }); test('a game: profile, games config or a Play game category', () => { expect(isGameSummary({ profile: 'app' } as never)).toBe(false); expect(isGameSummary({ profile: 'game' } as never)).toBe(true); expect(isGameSummary({ profile: 'app', games: { gameCenter: true, playGames: false, leaderboards: 0, achievements: 0 } } as never)).toBe(true); expect(isGameSummary({ profile: 'app', playRelease: { managedPublishing: null, countries: null, category: 'Puzzle' } } as never)).toBe(true); }); }); describe('next step per section', () => { test('release: rejected first, then blockers, rollout, review (managed), upload, live', () => { expect(sectionNext('release', { ...base, all: [rel('ios', '2.1', 'rejected')], att: [att('bad', 'checks')] })).toMatchObject({ tone: 'bad', tab: 'releases' }); expect(sectionNext('release', { ...base, att: [att('bad', 'checks'), att('bad', 'checks')] })).toMatchObject({ text: '2 blockers before submit', tab: 'checks' }); expect(sectionNext('release', { ...base, all: [rel('android', '3.0', 'rollout', { fraction: 0.2 })] }).text).toContain('20'); expect(sectionNext('release', { ...base, all: [rel('ios', '2.1', 'review', { managed: true })] }).text).toContain('Publishing overview'); expect(sectionNext('release', base)).toMatchObject({ tab: 'releases', text: 'Upload a first build' }); expect(sectionNext('release', { ...base, all: [rel('ios', '2.1', 'live')] })).toMatchObject({ tone: 'ok', tab: null }); }); test('store, revenue, players, health', () => { const listings = { files: { play: null, apple: null }, problems: [], locales: [{ lang: 'en', rtl: false, screenshots: null, complete: 0.5, apple: null, play: { code: 'en-US', fields: [{ field: 'title', length: 0, limit: 30, required: true, state: 'missing' }] } }] } as never; expect(listingProblems(listings)).toBe(1); expect(sectionNext('store', { ...base, listings })).toMatchObject({ text: '1 listing problem', tab: 'listings' }); expect(sectionNext('revenue', base).tab).toBe('catalog'); expect(sectionNext('revenue', { ...base, catalog: { checked: 'live', ok: false, products: 2, subscriptions: 0, onlyIos: ['a'], onlyAndroid: [], missingIos: [], missingAndroid: ['b'] } })).toMatchObject({ tone: 'warn', text: '2 products differ between the stores' }); expect(sectionNext('players', { ...base, reviews: [{}, { reply: { text: 'hi' } }, {}] })).toMatchObject({ text: '2 reviews without a reply', tab: 'reviews' }); expect(sectionNext('players', { ...base, reviews: [], games: { changes: 0, blockers: 0, apiOff: true } })).toMatchObject({ tone: 'bad', tab: 'games' }); expect(sectionNext('players', { ...base, reviews: [] }).tone).toBe('ok'); expect(sectionNext('health', { ...base, att: [att('bad', 'health', 'Crash rate over the line')] })).toMatchObject({ text: 'Crash rate over the line', tab: 'health' }); expect(sectionNext('health', { ...base, att: [att('bad', 'checks')] })).toMatchObject({ text: '1 problem to fix first', tab: 'checks' }); }); }); describe('sidebar status line', () => { const o = { att: [] as Attention[], all: [] as StoreRelease[], read: true, missing: false, storeOnly: false }; test('groups and lines', () => { expect(sideStatus({ ...o, all: [rel('ios', '2.1', 'rejected')] })).toEqual({ group: 'attention', text: 'Rejected · iOS 2.1', tone: 'bad' }); expect(sideStatus({ ...o, att: [att('bad', 'checks'), att('bad', 'checks')] })).toMatchObject({ group: 'attention', text: '2 blockers' }); expect(sideStatus({ ...o, all: [rel('ios', '2.1', 'review'), rel('android', '2.0', 'live')] })).toMatchObject({ group: 'progress', text: 'In review · iOS 2.1' }); expect(sideStatus({ ...o, all: [rel('ios', '2.1', 'live'), rel('android', '2.1', 'live')] })).toEqual({ group: 'live', text: 'Live · 2.1', tone: 'ok' }); expect(sideStatus({ ...o, read: false })).toMatchObject({ group: 'draft', text: 'Not read yet' }); expect(sideStatus({ ...o, missing: true })).toMatchObject({ group: 'attention', text: 'Folder missing' }); }); });