SPB Git

spb/cancerindex

Public
3.6 KB · 91 lines typescript
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
Files · 62fc60aa4e1575ebe6a0d752121ad91fa3ee22be
Menu

Theme