Spaces:
Running
Running
Download frontend/src/api/sessionOptions.test.ts from yashwrites/blindspot: direct link, hf CLI and curl.
- Browser
- Download file 12.7 kB
-
https://huggingface.co/spaces/yashwrites/blindspot/resolve/main/frontend/src/api/sessionOptions.test.ts
- Command line
-
hf download hf://spaces/yashwrites/blindspot/frontend/src/api/sessionOptions.test.ts
-
curl -L -o sessionOptions.test.ts https://huggingface.co/spaces/yashwrites/blindspot/resolve/main/frontend/src/api/sessionOptions.test.ts
12.7 kB
| import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | |
| import { enoughReads, MIN_READS, readsToGo } from '../dashboard/helpers'; | |
| import { | |
| addLocalReads, availableModalities, caseNoun, forgetLearner, guardAttemptResult, learnerLabel, loadLocalReads, loadModality, loadRemembered, looksLikeCode, | |
| markTestDone, needsTutorial, nextTest, parseLocalReads, parseRemembered, readPath, readsFor, remember, sameLearner, sampleSessionCreate, saveModality, | |
| testAvailable, toSessionCreate, weakSpotsNote, weakSpotsReady, type Remembered, type StartForm, | |
| } from './sessionOptions'; | |
| const form = (over: Partial<StartForm> = {}): StartForm => ({ name: '', practice: 'mixed', finding: 'nodule', count: 10, ...over }); | |
| const fresh = { remembered: null, reads: null }; | |
| const maya: Remembered = { learnerId: 'lrn_1', name: 'Maya', tests: [] }; | |
| describe('start form → SessionCreate', () => { | |
| it('a mixed set is adaptive practice with the chosen length, and level is always "other"', () => { | |
| expect(toSessionCreate(form({ name: ' Maya ', count: 5 }), fresh)).toEqual({ | |
| display_name: 'Maya', participant_code: null, level: 'other', mode: 'practice', settings: { case_count: 5, selection: 'adaptive' }, | |
| }); | |
| }); | |
| it('a blank name stays anonymous', () => { | |
| const b = toSessionCreate(form(), fresh); | |
| expect(b.display_name).toBe('Anonymous'); | |
| expect(b.participant_code).toBeNull(); | |
| expect(b.settings).toEqual({ case_count: 10, selection: 'adaptive' }); | |
| }); | |
| it('a personal code is sent as the participant code', () => { | |
| expect(toSessionCreate(form({ name: 'MK-2041' }), fresh)).toMatchObject({ display_name: 'MK-2041', participant_code: 'MK-2041' }); | |
| expect(looksLikeCode('Maya')).toBe(false); | |
| }); | |
| it('one finding type is a drill on that label', () => { | |
| expect(toSessionCreate(form({ practice: 'finding', finding: 'pneumothorax', count: 20 }), fresh)).toMatchObject({ | |
| mode: 'drill', settings: { case_count: 20, selection: 'adaptive', label: 'pneumothorax' }, | |
| }); | |
| }); | |
| it('weak spots needs 5 reads; before that it falls back to a mixed set', () => { | |
| expect(toSessionCreate(form({ practice: 'weak' }), { remembered: maya, reads: 4 }).settings).toMatchObject({ selection: 'adaptive' }); | |
| const b = toSessionCreate(form({ name: 'Maya', practice: 'weak' }), { remembered: maya, reads: 5 }); | |
| expect(b.mode).toBe('practice'); | |
| expect(b.settings).toEqual({ learner_id: 'lrn_1', case_count: 10, selection: 'weak_areas' }); | |
| }); | |
| it('the test set has no length or selection: A first, B once A is done, then A again', () => { | |
| const a = toSessionCreate(form({ practice: 'test', count: 5 }), fresh); | |
| expect(a.mode).toBe('assess_A'); | |
| expect(a.settings).toEqual({}); | |
| expect(toSessionCreate(form({ name: 'Maya', practice: 'test' }), { remembered: { ...maya, tests: ['A'] }, reads: 0 }).mode).toBe('assess_B'); | |
| expect(toSessionCreate(form({ name: 'Maya', practice: 'test' }), { remembered: { ...maya, tests: ['A', 'B'] }, reads: 0 }).mode).toBe('assess_A'); | |
| expect(nextTest(['A', 'B'])).toEqual({ mode: 'assess_A', set: 'A', repeat: true }); | |
| // Someone else's finished test does not move a new name on to B. | |
| expect(toSessionCreate(form({ name: 'Sam', practice: 'test' }), { remembered: { ...maya, tests: ['A'] }, reads: 0 }).mode).toBe('assess_A'); | |
| }); | |
| it('the remembered learner is resumed only while the name still matches', () => { | |
| expect(toSessionCreate(form({ name: 'maya' }), { remembered: maya, reads: 9 }).settings).toMatchObject({ learner_id: 'lrn_1' }); | |
| expect(toSessionCreate(form({ name: 'Sam' }), { remembered: maya, reads: 9 }).settings).not.toHaveProperty('learner_id'); | |
| const anon: Remembered = { learnerId: 'lrn_2', name: '', tests: [] }; | |
| expect(sameLearner(anon, ' ')).toBe(true); | |
| expect(toSessionCreate(form(), { remembered: anon, reads: 0 }).settings).toMatchObject({ learner_id: 'lrn_2' }); | |
| }); | |
| it('projector mode rides along in settings', () => { | |
| expect(toSessionCreate(form(), { ...fresh, projector: true }).settings).toMatchObject({ projector: true }); | |
| }); | |
| it('the sample set uses the reserved name and nothing else; a learner typing that name does not', () => { | |
| expect(sampleSessionCreate()).toEqual({ display_name: 'Demo', level: 'other', participant_code: null, mode: 'practice', settings: {} }); | |
| expect(toSessionCreate(form({ name: 'demo' }), fresh).display_name).toBe('demo (reader)'); | |
| expect(learnerLabel('Demo')).toBe('Sample set'); | |
| expect(learnerLabel('Anonymous')).toBe('Anonymous reader'); | |
| expect(learnerLabel('Maya')).toBe('Maya'); | |
| }); | |
| }); | |
| describe('fewer than 5 reads', () => { | |
| it('gates the headline numbers and the weak-spots option at the same threshold', () => { | |
| expect(MIN_READS).toBe(5); | |
| for (const n of [null, undefined, Number.NaN, 0, 4]) { | |
| expect(enoughReads(n)).toBe(false); | |
| expect(weakSpotsReady(n)).toBe(false); | |
| } | |
| for (const n of [5, 6, 200]) { | |
| expect(enoughReads(n)).toBe(true); | |
| expect(weakSpotsReady(n)).toBe(true); | |
| } | |
| }); | |
| it('says how many films are left', () => { | |
| expect(readsToGo(0)).toBe(5); | |
| expect(readsToGo(3)).toBe(2); | |
| expect(readsToGo(5)).toBe(0); | |
| expect(readsToGo(null)).toBe(5); | |
| expect(weakSpotsNote(2)).toBe('Opens after 5 films, so there is something to go on. You have read 2; 3 to go.'); | |
| expect(weakSpotsNote(8)).toBe('Films like the ones you have missed most.'); | |
| }); | |
| }); | |
| describe('guards', () => { | |
| it('reads a remembered learner and rejects junk', () => { | |
| expect(parseRemembered('{"learnerId":"x","name":"Maya","tests":["A","C"]}')).toEqual({ learnerId: 'x', name: 'Maya', tests: ['A'] }); | |
| expect(parseRemembered('{"name":"Maya"}')).toBeNull(); | |
| expect(parseRemembered('not json')).toBeNull(); | |
| expect(parseRemembered(null)).toBeNull(); | |
| }); | |
| it('takes a stored result with or without the film and the submitted marks', () => { | |
| const base = { score: 70, success: true, outcomes: [], reveal: { findings: [] }, facts_card: { headline: 'h', lines: ['a', 3] } }; | |
| const bare = guardAttemptResult(base); | |
| expect(bare.film).toBeNull(); | |
| expect(bare.marks).toBeNull(); | |
| expect(bare.result.reveal.marks).toEqual([]); | |
| expect(bare.result.facts_card.lines).toEqual(['a']); | |
| const full = guardAttemptResult({ | |
| ...base, case: { case_id: 'cxd_1', image_url: '/api/cases/cxd_1/image', width: 1024, height: 1024 }, | |
| submitted: { marks: [{ mark_id: 'M1', x: 10, y: 20, label: 'nodule', confidence: 4 }, { x: 'bad' }], declared_normal: false }, | |
| }); | |
| expect(full.film).toEqual({ case_id: 'cxd_1', image_url: '/api/cases/cxd_1/image', width: 1024, height: 1024, volume: null }); | |
| expect(full.marks).toEqual([{ mark_id: 'M1', x: 10, y: 20, label: 'nodule', confidence: 4 }]); | |
| expect(full.declaredNormal).toBe(false); | |
| expect(full.modality).toBeNull(); | |
| expect(full.provenance).toBeNull(); | |
| expect(() => guardAttemptResult({ recorded: true })).toThrow(); | |
| }); | |
| it('a CT read carries its scan type, the reveal provenance and the case volume block through (round 4)', () => { | |
| const prov = { dataset: 'Medical Segmentation Decathlon, Task07 Pancreas', segmented_by: 'an abdominal radiologist (single reader)' }; | |
| const vol = { shape: [16, 64, 64], spacing: [3, 1.5, 1.5], window: { wc: 50, ww: 400 }, data_url: '/api/cases/vol_001/volume' }; | |
| const r = guardAttemptResult({ | |
| score: 80, success: true, outcomes: [], facts_card: { headline: 'h', lines: [] }, | |
| reveal: { findings: [], modality: 'ct', provenance: prov, maskvol_url: '/api/attempts/a/maskvol' }, | |
| case: { case_id: 'vol_001', image_url: '/api/cases/vol_001/image', width: 64, height: 64, modality: 'ct', volume: vol }, | |
| }); | |
| expect(r.modality).toBe('ct'); | |
| expect(r.provenance).toEqual(prov); | |
| expect(r.film?.volume).toEqual(vol); | |
| expect(guardAttemptResult({ score: 1, success: true, outcomes: [], reveal: { findings: [] }, facts_card: { headline: '', lines: [] }, modality: 'mr' }).modality).toBe('mr'); | |
| }); | |
| }); | |
| describe('scan type (round 4)', () => { | |
| const store = new Map<string, string>(); | |
| const fake = { getItem: (k: string) => store.get(k) ?? null, setItem: (k: string, v: string) => void store.set(k, v), removeItem: (k: string) => void store.delete(k) }; | |
| beforeEach(() => { vi.stubGlobal('localStorage', fake); }); | |
| afterEach(() => { store.clear(); vi.unstubAllGlobals(); }); | |
| it('a CT set says so and names the region; an X-ray body is unchanged', () => { | |
| const ct = toSessionCreate(form({ modality: 'ct', count: 5 }), fresh); | |
| expect(ct.settings).toEqual({ case_count: 5, selection: 'adaptive', modality: 'ct', body_region: 'abdomen' }); | |
| const mr = toSessionCreate(form({ modality: 'mr', practice: 'finding', finding: 'brain_tumour' }), fresh); | |
| expect(mr.mode).toBe('drill'); | |
| expect(mr.settings).toMatchObject({ modality: 'mr', body_region: 'brain', label: 'brain_tumour' }); | |
| expect(toSessionCreate(form({ modality: 'cxr' }), fresh).settings).toEqual({ case_count: 10, selection: 'adaptive' }); | |
| }); | |
| it('the test sets are chest films only', () => { | |
| expect(testAvailable('cxr')).toBe(true); | |
| expect(testAvailable(undefined)).toBe(true); | |
| expect(testAvailable('ct')).toBe(false); | |
| }); | |
| it('films become studies off the X-ray', () => { | |
| expect(caseNoun('cxr', 2)).toBe('films'); | |
| expect(caseNoun('ct')).toBe('study'); | |
| expect(caseNoun('mr', 3)).toBe('studies'); | |
| expect(weakSpotsNote(2, 'ct')).toBe('Opens after 5 studies of this scan type, so there is something to go on. You have read 2; 3 to go.'); | |
| expect(weakSpotsNote(5, 'mr')).toBe('Studies like the ones you have missed most.'); | |
| expect(weakSpotsNote(2)).toBe('Opens after 5 films, so there is something to go on. You have read 2; 3 to go.'); | |
| }); | |
| it('the chosen scan type is remembered on this device, and an unknown value is the chest film', () => { | |
| expect(loadModality()).toBe('cxr'); | |
| saveModality('mr'); | |
| expect(loadModality()).toBe('mr'); | |
| localStorage.setItem('bs_modality', 'pet'); | |
| expect(loadModality()).toBe('cxr'); | |
| }); | |
| it('which scan types the library holds comes from health', () => { | |
| expect(availableModalities(undefined)).toEqual(['cxr']); | |
| expect(availableModalities({ cases_by_modality: { cxr: 500, ct: 12, mr: 0 } })).toEqual(['cxr', 'ct']); | |
| expect(availableModalities({ cases_by_modality: null, modalities: ['mr', 'cxr'] })).toEqual(['cxr', 'mr']); | |
| }); | |
| it('reads per scan type: the server count when it sends one, all reads for X-ray on an older server, the local estimate for CT / MR', () => { | |
| expect(readsFor({ n_attempts: 9, n_by_modality: { cxr: 7, ct: 2 } }, 'ct', {})).toEqual({ n: 2, estimated: false }); | |
| expect(readsFor({ n_attempts: 9, n_by_modality: { cxr: 7 } }, 'mr', { mr: 4 })).toEqual({ n: 0, estimated: false }); | |
| expect(readsFor({ n_attempts: 9 }, 'cxr', { ct: 4 })).toEqual({ n: 9, estimated: false }); | |
| expect(readsFor({ n_attempts: 9 }, 'ct', { ct: 4 })).toEqual({ n: 4, estimated: true }); | |
| addLocalReads('ct', 5); | |
| addLocalReads('ct', 5); | |
| expect(loadLocalReads()).toEqual({ ct: 10 }); | |
| expect(parseLocalReads('{"ct": 3, "pet": 2, "mr": -1}')).toEqual({ ct: 3 }); | |
| expect(parseLocalReads('nope')).toEqual({}); | |
| }); | |
| }); | |
| describe('this device remembers the learner', () => { | |
| const store = new Map<string, string>(); | |
| const fake = { getItem: (k: string) => store.get(k) ?? null, setItem: (k: string, v: string) => void store.set(k, v), removeItem: (k: string) => void store.delete(k) }; | |
| afterEach(() => { store.clear(); vi.unstubAllGlobals(); }); | |
| it('keeps the reader and the tests they finished; a different reader starts clean', () => { | |
| vi.stubGlobal('localStorage', fake); | |
| expect(loadRemembered()).toBeNull(); | |
| remember('lrn_1', ' Maya '); | |
| expect(loadRemembered()).toEqual({ learnerId: 'lrn_1', name: 'Maya', tests: [] }); | |
| markTestDone('lrn_1', 'assess_A'); | |
| markTestDone('lrn_1', 'assess_A'); | |
| markTestDone('someone_else', 'assess_B'); | |
| markTestDone('lrn_1', 'practice'); | |
| expect(loadRemembered()?.tests).toEqual(['A']); | |
| expect(remember('lrn_1', 'Maya').tests).toEqual(['A']); | |
| expect(remember('lrn_2', 'Sam').tests).toEqual([]); | |
| forgetLearner(); | |
| expect(loadRemembered()).toBeNull(); | |
| }); | |
| it('asks for the tutorial on the first session only, and never when storage is blocked', () => { | |
| expect(needsTutorial()).toBe(false); // no localStorage at all: do not nag | |
| vi.stubGlobal('localStorage', fake); | |
| expect(readPath()).toBe('/read?tutorial=1'); | |
| store.set('bs_tutorial_done', '1'); | |
| expect(readPath()).toBe('/read'); | |
| }); | |
| }); | |