|
|
import { describe, it, expect, vi, beforeEach } from "vitest"; |
|
|
import { render, screen, waitFor } from "@testing-library/react"; |
|
|
import { NiiVueViewer } from "../NiiVueViewer"; |
|
|
|
|
|
|
|
|
const mockLoadVolumes = vi.fn().mockResolvedValue(undefined); |
|
|
const mockCleanup = vi.fn(); |
|
|
const mockAttachToCanvas = vi.fn(); |
|
|
const mockLoseContext = vi.fn(); |
|
|
|
|
|
|
|
|
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(); |
|
|
|
|
|
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} />); |
|
|
|
|
|
|
|
|
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} />); |
|
|
|
|
|
|
|
|
await waitFor(() => { |
|
|
expect(onError).toHaveBeenCalledWith(errorMessage); |
|
|
}); |
|
|
}); |
|
|
|
|
|
it("ignores errors from stale loads after URL change", async () => { |
|
|
const onError = vi.fn(); |
|
|
|
|
|
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} |
|
|
/>, |
|
|
); |
|
|
|
|
|
|
|
|
rerender( |
|
|
<NiiVueViewer |
|
|
backgroundUrl="http://localhost/second.nii.gz" |
|
|
onError={onError} |
|
|
/>, |
|
|
); |
|
|
|
|
|
|
|
|
rerender( |
|
|
<NiiVueViewer |
|
|
backgroundUrl="http://localhost/third.nii.gz" |
|
|
onError={onError} |
|
|
/>, |
|
|
); |
|
|
|
|
|
|
|
|
rejectSecondLoad!(new Error("Stale load error")); |
|
|
|
|
|
|
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 0)); |
|
|
expect(onError).not.toHaveBeenCalledWith("Stale load error"); |
|
|
}); |
|
|
}); |
|
|
|