67 lines
4.5 KiB
TypeScript
67 lines
4.5 KiB
TypeScript
// The tray card's data mapping: rejections first, moving releases with rollout share, recent and low reviews, Play
|
|
// crash / ANR alerts, console links only for apps on the card; and the card words in every language.
|
|
import { expect, test } from 'bun:test';
|
|
import type { AppStatus } from '../src/api';
|
|
import { cardSnapshot, healthAlerts, type CardInput } from '../src/card/model';
|
|
import { ago, CARD_TEXT, cardT } from '../src/card/text';
|
|
|
|
const NOW = Date.parse('2026-10-20T12:00:00Z');
|
|
const daysAgo = (n: number) => new Date(NOW - n * 86_400_000).toISOString();
|
|
|
|
function status(all: { store: 'ios' | 'android'; phase: string; version?: string; fraction?: number | null; reason?: string }[]): AppStatus {
|
|
return { release: { source: 'live', current: null, next: null, all: all.map(r => ({ store: r.store, version: r.version ?? '1.0', build: null, state: 'X', phase: r.phase, fraction: r.fraction ?? null, track: null, reason: r.reason ?? null })) } } as unknown as AppStatus;
|
|
}
|
|
|
|
const rows = (rate: number) => [{ date: '2026-10-19', dims: {}, values: { userPerceivedCrashRate: rate, userPerceivedCrashRate28dUserWeighted: rate } }];
|
|
|
|
test('maps releases, rejections, reviews and health', () => {
|
|
const apps: CardInput[] = [
|
|
{ key: 'a', name: 'Alpha', hue: 10, status: status([{ store: 'ios', phase: 'rejected', reason: 'Guideline 2.1\nincomplete' }, { store: 'android', phase: 'rollout', fraction: 0.2 }]), asc: 'https://appstoreconnect.apple.com/apps/1/distribution', play: 'https://play.google.com/console' },
|
|
{ key: 'b', name: 'Beta', hue: 20, status: status([{ store: 'ios', phase: 'sent' }, { store: 'android', phase: 'live' }]),
|
|
reviews: [
|
|
{ store: 'play', id: '1', rating: 1, title: 'Bad', body: 'Crashes', date: daysAgo(1) },
|
|
{ store: 'apple', id: '2', rating: 5, body: 'Great', date: daysAgo(2) },
|
|
{ store: 'apple', id: '3', rating: 2, body: 'Old and low', date: daysAgo(20) },
|
|
] },
|
|
{ key: 'c', name: 'Gamma', hue: 30, status: status([{ store: 'ios', phase: 'approved' }, { store: 'android', phase: 'halted' }]),
|
|
health: { date: '2026-10-19', play: { window: { start: '', end: '', days: 28 }, crash: rows(0.05), anr: [] } } as never },
|
|
{ key: 'd', name: 'Delta', hue: 40, status: status([{ store: 'ios', phase: 'live' }]) },
|
|
];
|
|
const s = cardSnapshot(apps, { at: daysAgo(0), busy: true, muted: false, now: NOW });
|
|
expect(s.busy).toBe(true);
|
|
expect(s.apps).toBe(4);
|
|
expect(s.rejections).toEqual([{ app: 'a', name: 'Alpha', hue: 10, store: 'ios', version: '1.0', reason: 'Guideline 2.1 incomplete' }]);
|
|
expect(s.releases.map(r => `${r.app}:${r.store}:${r.phase}:${r.fraction}`)).toEqual(['c:android:halted:null', 'b:ios:review:null', 'c:ios:approved:null', 'a:android:rollout:0.2']);
|
|
expect(s.reviews.recent).toBe(2);
|
|
expect(s.reviews.low).toEqual([{ app: 'b', name: 'Beta', store: 'play', rating: 1, text: 'Bad · Crashes', date: daysAgo(1) }]);
|
|
expect(s.health).toEqual([{ app: 'c', name: 'Gamma', hue: 30, kinds: ['crash'] }]);
|
|
expect(s.links.map(l => l.key)).toEqual(['a', 'b', 'c']);
|
|
expect(s.links[0]!.asc).toContain('appstoreconnect.apple.com');
|
|
});
|
|
|
|
test('nothing read: an empty, calm snapshot', () => {
|
|
const s = cardSnapshot([{ key: 'x', name: 'X', hue: 0, status: undefined }], { at: null, busy: false, muted: true, now: NOW });
|
|
expect(s).toMatchObject({ at: null, muted: true, rejections: [], releases: [], health: [], reviews: { recent: 0, low: [] }, links: [] });
|
|
});
|
|
|
|
test('health errors and healthy rates raise no alert', () => {
|
|
expect(healthAlerts(null)).toEqual([]);
|
|
expect(healthAlerts({ date: '', play: { error: 'no access' } } as never)).toEqual([]);
|
|
expect(healthAlerts({ date: '', play: { window: { start: '', end: '', days: 28 }, crash: rows(0.001), anr: [] } } as never)).toEqual([]);
|
|
});
|
|
|
|
test('card words: every language has every key, placeholders match', () => {
|
|
const ph = (s: string) => [...s.matchAll(/\{(\w+)\}/g)].map(m => m[1]).sort().join(',');
|
|
for (const [lang, cat] of Object.entries(CARD_TEXT)) {
|
|
for (const [k, v] of Object.entries(CARD_TEXT.en)) {
|
|
const got = (cat as Record<string, string>)[k];
|
|
expect(got, `${lang}.${k}`).toBeTruthy();
|
|
expect(ph(got!), `${lang}.${k}`).toBe(ph(v));
|
|
}
|
|
}
|
|
expect(cardT('tr', 'rolloutPct', { pct: 20 })).toBe('%20 yayılıyor');
|
|
expect(ago('en', null, NOW)).toBe('never');
|
|
expect(ago('en', daysAgo(0), NOW)).toBe('just now');
|
|
expect(ago('en', new Date(NOW - 5 * 60_000).toISOString(), NOW)).toBe('5 min ago');
|
|
expect(ago('tr', new Date(NOW - 3 * 3600_000).toISOString(), NOW)).toBe('3 sa önce');
|
|
});
|