- app view: 12 tabs become 5 sections, each with a header (what it is for, the one next step from state already read) and its sub views in a segmented control; 1-5 jump, Ctrl+Shift+[ ] cycle, Ctrl+K and the context menu list sections and sub views; old tab ids, section and section/sub links all resolve - Players > Game services (games only): both stores, one row per leaderboard / achievement with its languages and each store's state, Game Center locale map, Play API-disabled state with the console plan - games status --json: items, localeMap, playApiDisabled; inspect summary: games - sidebar: apps grouped by attention / in progress / live / not on a store, one status line each, studio filter, workspace as a footer icon row - boot check opens places like a person (section, then sub view)
79 lines
6.5 KiB
TypeScript
79 lines
6.5 KiB
TypeScript
// 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> = {}): 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' });
|
|
});
|
|
});
|