import { describe, expect, it } from 'vitest' import { nextTick } from 'vue' import { useRackEditor } from '../../composables/useRackEditor' import type { Rack } from '../../types' function makeRacks(): Rack[] { return [ { id: 'r1', name: 'Rack 1', format: 54, slot: 'R', injectionVolume: 5, wells: { A1: { sampleType: 'sample', state: 'filled', metadata: { label: 'S001' } } }, }, { id: 'r2', name: 'Rack 2', format: 96, slot: 'G', injectionVolume: 5, wells: {} }, ] } describe('useRackEditor — PlateEditor intents (7a)', () => { it('should swap two wells on well-move', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['well-move']('r1', 'A1', 'B2') expect(Object.keys(editor.racks.value[0].wells)).toEqual(['B2']) }) it('should set the group of every assigned well', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners.assign('r1', ['A2', 'A3'], 'ctrl') expect(editor.racks.value[0].wells.A3?.group).toBe('ctrl') }) it('should make an assigned empty well a sample', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners.assign('r1', ['A2'], 'ctrl') expect(editor.racks.value[0].wells.A2?.sampleType).toBe('sample') }) it('should keep the type of a well when it is assigned a group', () => { const editor = useRackEditor(makeRacks()) editor.setWellData('r1', 'A2', { state: 'filled', sampleType: 'qc' }) editor.plateEditorListeners.assign('r1', ['A2'], 'ctrl') expect(editor.racks.value[0].wells.A2?.sampleType).toBe('qc') }) it('should keep the group of a well when it is edited', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners.assign('r1', ['A2'], 'ctrl') editor.plateEditorListeners['well-edit']('r1', 'A2', { wellId: 'A2', label: '', sampleType: 'blank', injectionVolume: 5, injectionCount: 1, customMethod: '', }) expect(editor.racks.value[0].wells.A2?.group).toBe('ctrl') }) it('should empty the wells when assign has no group', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners.assign('r1', ['A1'], undefined) expect(editor.racks.value[0].wells).toEqual({}) }) it('should store an edited well as filled with its metadata', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['well-edit']('r1', 'B1', { wellId: 'B1', label: 'QC1', sampleType: 'qc', injectionVolume: 5, injectionCount: 2, customMethod: '', }) expect(editor.racks.value[0].wells.B1).toMatchObject({ state: 'filled', sampleType: 'qc', metadata: { label: 'QC1', injectionCount: 2 } }) }) it('should place a dropped sample under its name', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['sample-drop']('r1', 'C1', { id: 's9', sampleName: 'Liver' }, new Event('drop') as DragEvent) expect(editor.racks.value[0].wells.C1?.metadata?.label).toBe('Liver') }) it('should reorder racks to the emitted id order', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['rack-reorder'](['r2', 'r1']) expect(editor.racks.value.map(rack => rack.id)).toEqual(['r2', 'r1']) }) it('should follow update:activeRackId', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:activeRackId']('r2') expect(editor.activeRackId.value).toBe('r2') }) it('should not share well objects with the initial racks', () => { const racks = makeRacks() const editor = useRackEditor(racks) editor.plateEditorListeners['well-clear']('r1', 'A1') expect(racks[0].wells.A1).toBeDefined() }) }) describe('useRackEditor — clear and move', () => { it('should restore a cleared plate on undo', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners.clear('r1') editor.plateEditorListeners.undo() expect(editor.racks.value[0].wells.A1?.metadata?.label).toBe('S001') }) it('should restamp the position of a swapped well', () => { const editor = useRackEditor(makeRacks()) editor.fillSeries('r1') editor.plateEditorListeners['well-move']('r1', 'A1', 'B2') expect(editor.racks.value[0].wells.A1).toMatchObject({ id: 'A1', row: 0, col: 0 }) }) }) describe('useRackEditor — history (7a)', () => { it('should report nothing to undo when no intent has run', () => { const editor = useRackEditor(makeRacks()) expect(editor.canUndo.value).toBe(false) }) it('should undo a well edit', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['well-clear']('r1', 'A1') editor.plateEditorListeners.undo() expect(editor.racks.value[0].wells.A1?.metadata?.label).toBe('S001') }) it('should redo an undone intent', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'B') editor.plateEditorListeners.undo() editor.plateEditorListeners.redo() expect(editor.racks.value[0].slot).toBe('B') }) it('should drop the redo stack once a new intent runs', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'B') editor.plateEditorListeners.undo() editor.plateEditorListeners['update:slot']('r1', 'Y') expect(editor.canRedo.value).toBe(false) }) it('should record nothing when an intent changes nothing', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'R') expect(editor.canUndo.value).toBe(false) }) it('should keep at most 50 undo steps', () => { const editor = useRackEditor(makeRacks()) for (let i = 0; i < 60; i++) editor.plateEditorListeners['update:format']('r1', i % 2 ? 54 : 96) let steps = 0 while (editor.canUndo.value) { editor.undo(); steps++ } expect(steps).toBe(50) }) it('should select a remaining rack when undo removes the active one', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['rack-add']() editor.undo() expect(editor.activeRackId.value).toBe('r1') }) it('should return to the plate that was active when undo removes an added rack', () => { const editor = useRackEditor(makeRacks()) editor.setActiveRack('r2') editor.plateEditorListeners['rack-add']() editor.undo() expect(editor.activeRackId.value).toBe('r2') }) it('should show the plate an undone edit changes', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['well-clear']('r1', 'A1') editor.setActiveRack('r2') editor.undo() expect(editor.activeRackId.value).toBe('r1') }) it('should undo a direct racks assignment as its own step', async () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'B') editor.racks.value = [{ id: 'imp', name: 'Imported', format: 96, slot: 'R', injectionVolume: 5, wells: {} }] await nextTick() editor.undo() expect(editor.racks.value[0]?.slot).toBe('B') }) it('should drop the redo stack after a direct racks assignment', async () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'B') editor.undo() editor.setWellData('r1', 'C3', { sampleType: 'qc' }) await nextTick() expect(editor.canRedo.value).toBe(false) }) it('should undo only the last of two synchronous public mutator calls', () => { const editor = useRackEditor(makeRacks()) editor.setWellData('r2', 'A1', { sampleType: 'qc' }) editor.setWellData('r2', 'A2', { sampleType: 'qc' }) editor.undo() expect(Object.keys(editor.racks.value[1].wells)).toEqual(['A1']) }) it('should forget the history on reset', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'B') editor.reset() expect(editor.canUndo.value).toBe(false) }) it('should forget the history on clearHistory', () => { const editor = useRackEditor(makeRacks()) editor.plateEditorListeners['update:slot']('r1', 'B') editor.undo() editor.clearHistory() expect(editor.canRedo.value).toBe(false) }) })