import { describe, expect, it } from 'vitest'; import { PRESETS, csvCell, isPlausibleChange, parseScreenerParams, toCsv } from './screener'; describe('parseScreenerParams', () => { it('reads percent inputs as fractions and applies sort defaults', () => { const f = parseScreenerParams({ c30min: '10', spmax: '-15', ddmax: '-30', conf: '75', min: '1,000', size: '500' }); expect(f.change30dMin).toBeCloseTo(0.1); expect(f.spreadMax).toBeCloseTo(-0.15); expect(f.drawdownMax).toBeCloseTo(-0.3); expect(f.confidenceMin).toBe(0.75); // "75" → 0.75 expect(f.rivMin).toBe(1000); expect(f.pageSize).toBe(100); // capped expect(f.sort).toBe('riv'); expect(f.dir).toBe('desc'); }); it('spread and drawdown sort ascending by default (most below first)', () => { expect(parseScreenerParams({ sort: 'spread' }).dir).toBe('asc'); expect(parseScreenerParams({ sort: 'drawdown' }).dir).toBe('asc'); expect(parseScreenerParams({ sort: 'spread', dir: 'desc' }).dir).toBe('desc'); expect(parseScreenerParams({ sort: 'nonsense' }).sort).toBe('riv'); }); it('applies preset filters without overriding explicit values', () => { const f = parseScreenerParams({ preset: 'underpriced' }); expect(f.sort).toBe('opportunity'); expect(f.dir).toBe('asc'); expect(f.spreadMax).toBe(-0.1); expect(f.confidenceMin).toBe(0.5); const g = parseScreenerParams({ preset: 'underpriced', conf: '0.75', sort: 'liquidity' }); expect(g.confidenceMin).toBe(0.75); expect(g.sort).toBe('liquidity'); expect(g.dir).toBe('desc'); }); it('every preset has a valid sort and a description', () => { for (const p of PRESETS) { expect(p.description.length).toBeGreaterThan(10); expect(parseScreenerParams({ preset: p.id }).sort).toBe(p.filters.sort); } expect(PRESETS.map((p) => p.id)).not.toContain('supply'); // not derivable honestly yet, see PRESETS comment }); }); describe('plausibility', () => { it('holds back artefact-sized moves', () => { expect(isPlausibleChange(0.42)).toBe(true); expect(isPlausibleChange(-0.99)).toBe(true); expect(isPlausibleChange(85.17)).toBe(false); // the −8,516 % class of value expect(isPlausibleChange(null)).toBe(false); }); }); describe('csv', () => { it('escapes quotes, commas and newlines', () => { expect(csvCell('Charizard, 1st Ed "Holo"')).toBe('"Charizard, 1st Ed ""Holo"""'); expect(csvCell(null)).toBe(''); expect(csvCell(12.5)).toBe('12.5'); expect(csvCell(new Date('2026-09-11T00:00:00Z'))).toBe('2026-09-11T00:00:00.000Z'); }); it('writes comment lines, a header and CRLF rows', () => { const out = toCsv(['a', 'b'], [[1, 'x,y'], [null, 'z']], ['as_of=now']); expect(out).toBe('# as_of=now\r\na,b\r\n1,"x,y"\r\n,z\r\n'); }); });