storebound/apps/desktop/tests/complete.test.ts

77 lines
5.1 KiB
TypeScript

// Full CLI coverage, window side: export formats (the prices CSV re-imports), price matrix, diff summary, chart
// series, store art filters and the "how to fix it" hints.
import { describe, expect, test } from 'bun:test';
import type { PricesCurrent } from '../src/api';
import { csv, mdTable, pricesCsv, reviewsCsv } from '../src/lib/exports';
import { chartSeries, diffSummary, fixHint, indexSeries, priceMatrix, priceStats, regionRows } from '../src/lib/views';
import { indexAxis } from '../src/ui/LineChart';
import { parsePriceCsv } from '../../../cli/lib/prices';
const P: PricesCurrent = {
csv: '/x/store/prices.csv', products: ['a', 'b'], stores: ['apple', 'play'], missing: [{ product: 'b', store: 'play' }],
rows: [
{ product: 'a', store: 'apple', region: 'US', currency: 'USD', price: 0.99, kind: 'manual' },
{ product: 'a', store: 'apple', region: 'TR', currency: 'TRY', price: 39.99, kind: 'auto' },
{ product: 'a', store: 'apple', region: 'TR', currency: 'TRY', price: 29.99, kind: 'manual' },
{ product: 'a', store: 'play', region: 'US', currency: 'USD', price: 0.99 },
{ product: 'a', store: 'play', region: 'TR', currency: 'TRY', price: 19.99 },
{ product: 'a', store: 'apple', region: 'BR', currency: 'BRL', price: 6.9, startDate: '2030-01-01', kind: 'manual' },
{ product: 'b', store: 'apple', region: 'US', currency: 'USD', price: 4.99, kind: 'manual' },
],
};
describe('exports', () => {
test('CSV quoting and formula guard', () => {
expect(csv(['a', 'b'], [['x,y', '=SUM(A1)'], [-1, null]])).toBe('a,b\n"x,y",\'=SUM(A1)\n-1,\n');
expect(mdTable(['a'], [['x|y']])).toBe('| a |\n| --- |\n| x\\|y |\n');
});
test('the prices CSV is the `prices import` sheet', () => {
const rows = parsePriceCsv(pricesCsv(P), { products: ['a', 'b'], today: '2026-01-01' });
expect(rows.filter(r => r.kind !== 'auto').length).toBe(6);
expect(rows.filter(r => r.region === 'TR' && r.store === 'apple').map(r => r.price)).toEqual([29.99]);
expect(rows.find(r => r.region === 'BR')?.startDate).toBe('2030-01-01');
});
test('reviews CSV', () => {
expect(reviewsCsv([{ store: 'play', id: '1', rating: 2, body: 'Crash, again', date: '2026-10-01T10:00:00Z', where: 'en' }]).split('\n')[1]).toBe('Google Play,2026-10-01,2,en,,,"Crash, again",no,');
});
});
describe('views', () => {
test('region rows: manual wins over equalized, scheduled apart, gap in one currency', () => {
const r = regionRows(P, 'a');
expect(r.map(x => x.region)).toEqual(['US', 'BR', 'TR']);
const tr = r.find(x => x.region === 'TR')!;
expect(tr.apple).toEqual({ price: 29.99, currency: 'TRY', kind: 'manual' });
expect(Math.round(tr.gapPct!)).toBe(33);
expect(r.find(x => x.region === 'BR')!.scheduled?.startDate).toBe('2030-01-01');
expect(priceMatrix(P).regions).toEqual(['US', 'BR', 'TR']);
expect(priceStats(P).gaps).toBe(1);
});
test('diff summary', () => {
expect(diffSummary([{ product: 'a', store: 'play', region: 'TR', currency: 'TRY', from: 10, to: 12, change: 'change' }, { product: 'b', store: 'play', region: 'KR', currency: 'KRW', from: null, to: 1, change: 'new' },
{ product: 'a', store: 'apple', region: 'US', currency: 'USD', from: 1, to: 1, change: 'same' }], ['a|play', 'b|play'])).toEqual({ changed: 1, added: 1, same: 1, up: 1, down: 0, products: ['a', 'b'], pairs: 2 });
});
test('chart series and events', () => {
const s = chartSeries({ snapshots: 1, since: '2026-01-01', series: { apple: [{ date: '2026-01-01', impressions: 100, uniqueImpressions: 0, pageViews: 0, uniquePageViews: 0, downloads: 5, firstTimeDownloads: 0 }, { date: '2026-01-02', impressions: 0, uniqueImpressions: 0, pageViews: 0, uniquePageViews: 0, downloads: 0, firstTimeDownloads: 0 }], play: [] },
effects: [{ event: { date: '2026-01-02', commit: 'abc', subject: 'x', files: [] }, store: 'apple', before: {}, after: {}, daysBefore: 1, daysAfter: 1 }] }, 'apple');
expect(s.conversion).toEqual([0.05, null]);
expect(s.events).toEqual([{ date: '2026-01-02', subject: 'x', commit: 'abc' }]);
});
test('chart index: every line on one axis, 100 = its own average', () => {
// impressions ~4000 and downloads ~150 a day: on a count axis downloads lie flat; indexed both use the height
const imp = [3800, 4200, 4000, 4000], dl = [120, 150, 180, 150];
expect(indexSeries(imp)).toEqual([95, 105, 100, 100]);
expect(indexSeries(dl)).toEqual([80, 100, 120, 100]);
expect(indexSeries([null, 0.04, 0.06])).toEqual([null, 80, 120]);
expect(indexSeries([0, 0, null])).toEqual([null, null, null]);
expect(indexAxis([[80, 120], [95, 105]])).toEqual({ max: 150, ticks: [0, 50, 100, 150] });
expect(indexAxis([[40, 230]])).toEqual({ max: 300, ticks: [0, 100, 200, 300] });
expect(indexAxis([[900]]).max).toBeGreaterThanOrEqual(900);
});
test('errors that say how to fix them', () => {
expect(fixHint('PLAY_SERVICE_ACCOUNT is not set (env ...)')).toBe('hint.keys');
expect(fixHint('no snapshots in /x/.storebound/analytics (storebound analytics snapshot)')).toBe('hint.snapshot');
expect(fixHint('ffmpeg not found')).toBe('hint.ffmpeg');
expect(fixHint('something else')).toBeNull();
});
});