// 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(); }); });