File size: 5,023 Bytes
fbf73ff 1493232 fbf73ff 1493232 fbf73ff 1493232 fbf73ff |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 |
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { NiiVueViewer } from '../NiiVueViewer'
// Store mock function references so tests can verify calls
const mockLoadVolumes = vi.fn().mockResolvedValue(undefined)
const mockCleanup = vi.fn()
const mockAttachToCanvas = vi.fn()
const mockLoseContext = vi.fn()
// Mock the NiiVue module since it requires actual WebGL
vi.mock('@niivue/niivue', () => ({
Niivue: class MockNiivue {
attachToCanvas = mockAttachToCanvas
loadVolumes = mockLoadVolumes
setSliceType = vi.fn()
cleanup = mockCleanup
gl = {
getExtension: vi.fn(() => ({ loseContext: mockLoseContext })),
}
opts = {}
},
}))
describe('NiiVueViewer', () => {
const defaultProps = {
backgroundUrl: 'http://localhost:7860/files/dwi.nii.gz',
}
beforeEach(() => {
vi.clearAllMocks()
})
it('renders canvas element', () => {
render(<NiiVueViewer {...defaultProps} />)
expect(document.querySelector('canvas')).toBeInTheDocument()
})
it('renders container with correct styling', () => {
render(<NiiVueViewer {...defaultProps} />)
const container = document.querySelector('canvas')?.parentElement
expect(container).toHaveClass('bg-gray-900')
})
it('renders help text for controls', () => {
render(<NiiVueViewer {...defaultProps} />)
expect(screen.getByText(/scroll/i)).toBeInTheDocument()
expect(screen.getByText(/drag/i)).toBeInTheDocument()
})
it('attaches NiiVue to canvas on mount', () => {
render(<NiiVueViewer {...defaultProps} />)
expect(mockAttachToCanvas).toHaveBeenCalled()
// Verify it was called with a canvas element
const arg = mockAttachToCanvas.mock.calls[0][0]
expect(arg).toBeInstanceOf(HTMLCanvasElement)
})
it('loads background volume on mount', () => {
render(<NiiVueViewer {...defaultProps} />)
expect(mockLoadVolumes).toHaveBeenCalledWith([
{ url: defaultProps.backgroundUrl, colormap: 'gray', opacity: 1 },
])
})
it('loads both background and overlay when overlayUrl provided', () => {
const overlayUrl = 'http://localhost:7860/files/prediction.nii.gz'
render(
<NiiVueViewer
{...defaultProps}
overlayUrl={overlayUrl}
/>
)
expect(mockLoadVolumes).toHaveBeenCalledWith([
{ url: defaultProps.backgroundUrl, colormap: 'gray', opacity: 1 },
{ url: overlayUrl, colormap: 'red', opacity: 0.5 },
])
})
it('calls cleanup on unmount', () => {
const { unmount } = render(<NiiVueViewer {...defaultProps} />)
unmount()
expect(mockCleanup).toHaveBeenCalled()
expect(mockLoseContext).toHaveBeenCalled()
})
it('sets canvas dimensions', () => {
render(<NiiVueViewer {...defaultProps} />)
const canvas = document.querySelector('canvas')
expect(canvas).toHaveClass('w-full', 'h-[500px]')
})
it('displays error when volume loading fails', async () => {
const errorMessage = 'Network error loading volume'
mockLoadVolumes.mockRejectedValueOnce(new Error(errorMessage))
render(<NiiVueViewer {...defaultProps} />)
// Wait for error to be displayed
const errorElement = await screen.findByText(/failed to load volume/i)
expect(errorElement).toBeInTheDocument()
expect(errorElement).toHaveTextContent(errorMessage)
})
it('calls onError callback when volume loading fails', async () => {
const errorMessage = 'Network error'
const onError = vi.fn()
mockLoadVolumes.mockRejectedValueOnce(new Error(errorMessage))
render(<NiiVueViewer {...defaultProps} onError={onError} />)
// Wait for error callback to be invoked (use RTL's waitFor, not vi.waitFor)
await waitFor(() => {
expect(onError).toHaveBeenCalledWith(errorMessage)
})
})
it('ignores errors from stale loads after URL change', async () => {
const onError = vi.fn()
// First load succeeds, second load fails slowly
let rejectSecondLoad: (error: Error) => void
mockLoadVolumes
.mockResolvedValueOnce(undefined)
.mockImplementationOnce(() => new Promise((_, reject) => {
rejectSecondLoad = reject
}))
const { rerender } = render(
<NiiVueViewer backgroundUrl="http://localhost/first.nii.gz" onError={onError} />
)
// Change URL - starts second load
rerender(
<NiiVueViewer backgroundUrl="http://localhost/second.nii.gz" onError={onError} />
)
// Change URL again - makes second load stale
rerender(
<NiiVueViewer backgroundUrl="http://localhost/third.nii.gz" onError={onError} />
)
// Now reject the second load (stale)
rejectSecondLoad!(new Error('Stale load error'))
// Flush async work (let rejection be processed) before asserting
// Using waitFor with negative assertions is flaky - it passes immediately
await new Promise(resolve => setTimeout(resolve, 0))
expect(onError).not.toHaveBeenCalledWith('Stale load error')
})
})
|