import { describe, expect, it } from 'vitest'; import { csvCell, fmtGrowth, intelTotals, isIntelSortKey, reasonLabel, sortIntel, type IntelSortable } from './trial-intel'; const row = (cancer_name: string, p: Partial = {}): IntelSortable => ({ cancer_name, total_trials: 0, active_trials: 0, recruiting_trials: 0, phase3_active: 0, phase3_recruiting: 0, trial_growth_yoy: null, avg_enrollment: null, industry_share: null, sponsor_hhi: null, distinct_countries: 0, us_share: null, termination_share: null, trials_per_1000_deaths: null, ...p, }); describe('sortIntel', () => { const rows = [row('B', { active_trials: 10, trial_growth_yoy: 0.2 }), row('A', { active_trials: 30, trial_growth_yoy: null }), row('C', { active_trials: 20, trial_growth_yoy: -0.1 })]; it('sorts numerically desc/asc', () => { expect(sortIntel(rows, 'active', 'desc').map((r) => r.cancer_name)).toEqual(['A', 'C', 'B']); expect(sortIntel(rows, 'active', 'asc').map((r) => r.cancer_name)).toEqual(['B', 'C', 'A']); }); it('puts nulls last in both directions', () => { expect(sortIntel(rows, 'growth', 'desc').map((r) => r.cancer_name)).toEqual(['B', 'C', 'A']); expect(sortIntel(rows, 'growth', 'asc').map((r) => r.cancer_name)).toEqual(['C', 'B', 'A']); }); it('breaks ties by cancer name and sorts by name', () => { const tied = [row('Zeta', { active_trials: 5 }), row('Alpha', { active_trials: 5 })]; expect(sortIntel(tied, 'active', 'desc').map((r) => r.cancer_name)).toEqual(['Alpha', 'Zeta']); expect(sortIntel(rows, 'name', 'asc').map((r) => r.cancer_name)).toEqual(['A', 'B', 'C']); expect(sortIntel(rows, 'name', 'desc').map((r) => r.cancer_name)).toEqual(['C', 'B', 'A']); }); it('does not mutate the input', () => { const copy = [...rows]; sortIntel(rows, 'active', 'desc'); expect(rows).toEqual(copy); }); }); describe('isIntelSortKey', () => { it('whitelists keys', () => { expect(isIntelSortKey('active')).toBe(true); expect(isIntelSortKey('trialsPer1000Deaths')).toBe(true); expect(isIntelSortKey('drop table')).toBe(false); }); }); describe('intelTotals', () => { it('sums the count columns', () => { const t = intelTotals([row('A', { total_trials: 10, active_trials: 4, recruiting_trials: 2, phase3_active: 1, phase3_recruiting: 1 }), row('B', { total_trials: 5, active_trials: 1, recruiting_trials: 1, phase3_active: 0, phase3_recruiting: 0 })]); expect(t).toEqual({ entities: 2, total: 15, active: 5, recruiting: 3, phase3Active: 1, phase3Recruiting: 1 }); }); it('is zero for no rows', () => { expect(intelTotals([]).entities).toBe(0); }); }); describe('csvCell', () => { it('quotes commas, quotes and newlines; empties nulls; serialises objects', () => { expect(csvCell('a,b')).toBe('"a,b"'); expect(csvCell('say "hi"')).toBe('"say ""hi"""'); expect(csvCell('x\ny')).toBe('"x\ny"'); expect(csvCell(null)).toBe(''); expect(csvCell(3.5)).toBe('3.5'); expect(csvCell({ a: 1 })).toBe('"{""a"":1}"'); }); }); describe('fmtGrowth', () => { it('formats signed percentages', () => { expect(fmtGrowth(0.215)).toBe('+21.5%'); expect(fmtGrowth(-0.036)).toBe('−3.6%'); expect(fmtGrowth(0)).toBe('0%'); expect(fmtGrowth(null)).toBe('—'); expect(fmtGrowth(Number.NaN)).toBe('—'); }); }); describe('reasonLabel', () => { it('labels known categories and falls back to a humanised key', () => { expect(reasonLabel('enrollment')).toBe('Enrollment / accrual'); expect(reasonLabel('not_stated')).toBe('Not stated'); expect(reasonLabel('some_other')).toBe('some other'); }); });