spb/cancerindex
Public
1import { describe, expect, it } from 'vitest';2import { csvCell, fmtGrowth, intelTotals, isIntelSortKey, reasonLabel, sortIntel, type IntelSortable } from './trial-intel';34const row = (cancer_name: string, p: Partial<IntelSortable> = {}): IntelSortable => ({5 cancer_name,6 total_trials: 0,7 active_trials: 0,8 recruiting_trials: 0,9 phase3_active: 0,10 phase3_recruiting: 0,11 trial_growth_yoy: null,12 avg_enrollment: null,13 industry_share: null,14 sponsor_hhi: null,15 distinct_countries: 0,16 us_share: null,17 termination_share: null,18 trials_per_1000_deaths: null,19 ...p,20});2122describe('sortIntel', () => {23 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 })];24 it('sorts numerically desc/asc', () => {25 expect(sortIntel(rows, 'active', 'desc').map((r) => r.cancer_name)).toEqual(['A', 'C', 'B']);26 expect(sortIntel(rows, 'active', 'asc').map((r) => r.cancer_name)).toEqual(['B', 'C', 'A']);27 });28 it('puts nulls last in both directions', () => {29 expect(sortIntel(rows, 'growth', 'desc').map((r) => r.cancer_name)).toEqual(['B', 'C', 'A']);30 expect(sortIntel(rows, 'growth', 'asc').map((r) => r.cancer_name)).toEqual(['C', 'B', 'A']);31 });32 it('breaks ties by cancer name and sorts by name', () => {33 const tied = [row('Zeta', { active_trials: 5 }), row('Alpha', { active_trials: 5 })];34 expect(sortIntel(tied, 'active', 'desc').map((r) => r.cancer_name)).toEqual(['Alpha', 'Zeta']);35 expect(sortIntel(rows, 'name', 'asc').map((r) => r.cancer_name)).toEqual(['A', 'B', 'C']);36 expect(sortIntel(rows, 'name', 'desc').map((r) => r.cancer_name)).toEqual(['C', 'B', 'A']);37 });38 it('does not mutate the input', () => {39 const copy = [...rows];40 sortIntel(rows, 'active', 'desc');41 expect(rows).toEqual(copy);42 });43});4445describe('isIntelSortKey', () => {46 it('whitelists keys', () => {47 expect(isIntelSortKey('active')).toBe(true);48 expect(isIntelSortKey('trialsPer1000Deaths')).toBe(true);49 expect(isIntelSortKey('drop table')).toBe(false);50 });51});5253describe('intelTotals', () => {54 it('sums the count columns', () => {55 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 })]);56 expect(t).toEqual({ entities: 2, total: 15, active: 5, recruiting: 3, phase3Active: 1, phase3Recruiting: 1 });57 });58 it('is zero for no rows', () => {59 expect(intelTotals([]).entities).toBe(0);60 });61});6263describe('csvCell', () => {64 it('quotes commas, quotes and newlines; empties nulls; serialises objects', () => {65 expect(csvCell('a,b')).toBe('"a,b"');66 expect(csvCell('say "hi"')).toBe('"say ""hi"""');67 expect(csvCell('x\ny')).toBe('"x\ny"');68 expect(csvCell(null)).toBe('');69 expect(csvCell(3.5)).toBe('3.5');70 expect(csvCell({ a: 1 })).toBe('"{""a"":1}"');71 });72});7374describe('fmtGrowth', () => {75 it('formats signed percentages', () => {76 expect(fmtGrowth(0.215)).toBe('+21.5%');77 expect(fmtGrowth(-0.036)).toBe('−3.6%');78 expect(fmtGrowth(0)).toBe('0%');79 expect(fmtGrowth(null)).toBe('—');80 expect(fmtGrowth(Number.NaN)).toBe('—');81 });82});8384describe('reasonLabel', () => {85 it('labels known categories and falls back to a humanised key', () => {86 expect(reasonLabel('enrollment')).toBe('Enrollment / accrual');87 expect(reasonLabel('not_stated')).toBe('Not stated');88 expect(reasonLabel('some_other')).toBe('some other');89 });90});91