80 lines
5.1 KiB
TypeScript
80 lines
5.1 KiB
TypeScript
// Releases > send for review: the submission state read from `apple status --json` / `play status --json`, builds and
|
|
// versionCodes picked by number, and the progress the job toasts read from upload lines.
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { appleSubmitState, planBlockers, playSubmitState } from '../src/lib/submit';
|
|
import { keepLines, progressOf } from '../src/lib/progress';
|
|
import { deletionView } from '../src/screens/tabs/AppInfo';
|
|
import { APPLE_STATUS_READY, APPLE_STATUS_UNRESOLVED, PLAY_STATUS } from '../src/dev/submit-fixtures';
|
|
|
|
describe('App Store submission state', () => {
|
|
test('ready: no open submission, the editable version, VALID builds pickable, processing ones not', () => {
|
|
const s = appleSubmitState(APPLE_STATUS_READY);
|
|
expect(s.unresolved).toBeNull();
|
|
expect(s.busy).toBeNull();
|
|
expect(s.editable).toMatchObject({ version: '2.4.0', state: 'PREPARE_FOR_SUBMISSION' });
|
|
expect(s.builds.map(b => [b.number, b.version, b.valid])).toEqual([['53', '2.4.0', false], ['52', '2.4.0', true], ['48', '2.3.0', true]]);
|
|
expect(s.builds.find(b => b.number === '48')!.attachedTo).toBe('2.3.0');
|
|
// an empty draft is labelled, never a blocker
|
|
expect(s.submissions.map(x => [x.id, x.kind])).toEqual([['rs-empty-1', 'emptyDraft'], ['rs-old-1', 'done']]);
|
|
});
|
|
test('UNRESOLVED_ISSUES: its items with REJECTED marked, the held version is the one to resubmit', () => {
|
|
const s = appleSubmitState(APPLE_STATUS_UNRESOLVED);
|
|
expect(s.unresolved).toMatchObject({ id: 'rs-back-1', kind: 'unresolved' });
|
|
expect(s.unresolved!.items.map(i => [i.label, i.state, i.rejected])).toEqual([['app version 2.4.0', 'REJECTED', true], ['in-app purchase iapv-coins', 'READY_FOR_REVIEW', false]]);
|
|
expect(s.editable).toMatchObject({ version: '2.4.0', state: 'REJECTED' });
|
|
expect(s.busy).toBeNull();
|
|
});
|
|
test('waiting for review: busy, nothing can be sent next to it', () => {
|
|
const raw = { ...APPLE_STATUS_READY, subs: [{ id: 'rs-w', attributes: { state: 'WAITING_FOR_REVIEW', submittedDate: '2026-10-11T10:00:00Z' } }], emptyDrafts: [] };
|
|
expect(appleSubmitState(raw).busy).toMatchObject({ id: 'rs-w', kind: 'waiting', date: '2026-10-11T10:00:00Z' });
|
|
});
|
|
test('odd or old answers never throw', () => {
|
|
for (const raw of [null, undefined, 'x', [], { subs: 'no' }, { builds: [null, 1, { id: 'b' }] }, { versions: [{ attributes: null }] }])
|
|
expect(() => appleSubmitState(raw)).not.toThrow();
|
|
expect(appleSubmitState({}).builds).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('Google Play production', () => {
|
|
test('versionCodes by number from every track, newest first; one in production (not a draft) cannot be picked again', () => {
|
|
const s = playSubmitState(PLAY_STATUS);
|
|
expect(s.codes.map(c => [c.code, c.tracks.join(','), c.inProduction])).toEqual([['24', 'beta,internal', false], ['23', 'production', true], ['22', 'internal', false]]);
|
|
expect(s.production).toEqual([{ status: 'completed', codes: ['23'], name: '2.3.0 (23)', fraction: null }]);
|
|
expect(playSubmitState(null)).toEqual({ codes: [], production: [], category: null });
|
|
});
|
|
});
|
|
|
|
test('plan blockers from the --json result', () => {
|
|
expect(planBlockers({ blockers: ['build 53 is PROCESSING; wait until VALID'] })).toEqual(['build 53 is PROCESSING; wait until VALID']);
|
|
expect(planBlockers(undefined)).toEqual([]);
|
|
});
|
|
|
|
describe('progress from upload and render lines', () => {
|
|
test('MB share, percent, parts', () => {
|
|
expect(progressOf('12:00:00 [upload] still uploading, 4m30s, 312/890 MB (35%), part 4/12')).toEqual({ done: 312 * 1048576, total: 890 * 1048576 });
|
|
expect(progressOf('processing (40%)')).toEqual({ done: 40, total: 100 });
|
|
expect(progressOf('[upload] part 3/12 done')).toEqual({ done: 3, total: 12 });
|
|
expect(progressOf('[render] [7/48] en ios69 01')).toEqual({ done: 7, total: 48 });
|
|
expect(progressOf('version 1.2/3 something')).toBeNull();
|
|
expect(progressOf('12/5 MB')).toBeNull();
|
|
});
|
|
test('the toast log keeps the last lines', () => {
|
|
let l: string[] | undefined;
|
|
for (let i = 0; i < 50; i++) l = keepLines(l, `line ${i}`);
|
|
expect(l!.length).toBe(40);
|
|
expect(l!.at(-1)).toBe('line 49');
|
|
});
|
|
});
|
|
|
|
describe('Data safety: the deletion question', () => {
|
|
const base = { collectsData: true, encryptedInTransit: true, accountCreation: ['PSL_ACM_USER_ID_PASSWORD'], types: [], extra: 0, csv: '', rows: 0, uploaded: null };
|
|
test('the effective answer from a newer sidecar, with where it comes from', () => {
|
|
expect(deletionView({ ...base, partialDeletion: null, dataDeletion: 'yes', dataDeletionSource: 'accounts.deletionUrl', dataDeletionUrl: 'https://example.com/delete' }))
|
|
.toEqual({ answer: 'yes', source: 'accounts.deletionUrl', url: 'https://example.com/delete' });
|
|
expect(deletionView({ ...base, partialDeletion: null, dataDeletion: null })).toEqual({ answer: null, source: null, url: null });
|
|
});
|
|
test('an older sidecar (partialDeletion only)', () => {
|
|
expect(deletionView({ ...base, partialDeletion: 'auto90' })).toMatchObject({ answer: 'auto90' });
|
|
expect(deletionView({ ...base, partialDeletion: null })).toMatchObject({ answer: null });
|
|
});
|
|
});
|