77 lines
5.1 KiB
TypeScript
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();
|
|
});
|
|
});
|