test(CornerstoneUtils): Adding unit tests for most of Cornerstone Extension utilities (#5454)
This commit is contained in:
parent
44beac799f
commit
3c421f124e
125
extensions/cornerstone/src/utils/findNearbyToolData.test.ts
Normal file
125
extensions/cornerstone/src/utils/findNearbyToolData.test.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { findNearbyToolData } from './findNearbyToolData';
|
||||
|
||||
describe('findNearbyToolData', () => {
|
||||
const mockCommandsManager = {
|
||||
runCommand: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return undefined when event is null', () => {
|
||||
const result = findNearbyToolData(mockCommandsManager, null);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when event is undefined', () => {
|
||||
const result = findNearbyToolData(mockCommandsManager, undefined);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when event has no detail', () => {
|
||||
const event = {};
|
||||
const result = findNearbyToolData(mockCommandsManager, event);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return undefined when event detail is null', () => {
|
||||
const event = { detail: null };
|
||||
const result = findNearbyToolData(mockCommandsManager, event);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call runCommand with correct parameters when event has valid detail', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
const mockCurrentPoints = { canvas: { x: 100, y: 200 } };
|
||||
const mockToolData = { id: 'annotation-1' };
|
||||
const event = {
|
||||
detail: {
|
||||
element: mockElement,
|
||||
currentPoints: mockCurrentPoints,
|
||||
},
|
||||
};
|
||||
|
||||
mockCommandsManager.runCommand.mockReturnValue(mockToolData);
|
||||
|
||||
const result = findNearbyToolData(mockCommandsManager, event);
|
||||
|
||||
expect(mockCommandsManager.runCommand).toHaveBeenCalledWith(
|
||||
'getNearbyAnnotation',
|
||||
{
|
||||
element: mockElement,
|
||||
canvasCoordinates: mockCurrentPoints.canvas,
|
||||
},
|
||||
'CORNERSTONE'
|
||||
);
|
||||
expect(result).toBe(mockToolData);
|
||||
});
|
||||
|
||||
it('should handle event with element but no currentPoints', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
const event = {
|
||||
detail: {
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
findNearbyToolData(mockCommandsManager, event);
|
||||
|
||||
expect(mockCommandsManager.runCommand).toHaveBeenCalledWith(
|
||||
'getNearbyAnnotation',
|
||||
{
|
||||
element: mockElement,
|
||||
canvasCoordinates: undefined,
|
||||
},
|
||||
'CORNERSTONE'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle event with currentPoints but no canvas coordinates', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
const mockCurrentPoints = {};
|
||||
const event = {
|
||||
detail: {
|
||||
element: mockElement,
|
||||
currentPoints: mockCurrentPoints,
|
||||
},
|
||||
};
|
||||
|
||||
findNearbyToolData(mockCommandsManager, event);
|
||||
|
||||
expect(mockCommandsManager.runCommand).toHaveBeenCalledWith(
|
||||
'getNearbyAnnotation',
|
||||
{
|
||||
element: mockElement,
|
||||
canvasCoordinates: undefined,
|
||||
},
|
||||
'CORNERSTONE'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return result from runCommand', () => {
|
||||
const mockToolData = { id: 'test-annotation', data: 'test-data' };
|
||||
const event = {
|
||||
detail: {
|
||||
element: document.createElement('div'),
|
||||
currentPoints: { canvas: { x: 50, y: 75 } },
|
||||
},
|
||||
};
|
||||
|
||||
mockCommandsManager.runCommand.mockReturnValue(mockToolData);
|
||||
|
||||
const result = findNearbyToolData(mockCommandsManager, event);
|
||||
|
||||
expect(result).toBe(mockToolData);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,319 @@
|
||||
import { generateSegmentationCSVReport } from './generateSegmentationCSVReport';
|
||||
|
||||
// Mock DOM APIs
|
||||
Object.defineProperty(global, 'Blob', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation((content, options) => ({
|
||||
content,
|
||||
options,
|
||||
})),
|
||||
});
|
||||
|
||||
Object.defineProperty(global, 'URL', {
|
||||
writable: true,
|
||||
value: {
|
||||
createObjectURL: jest.fn().mockReturnValue('mock-url'),
|
||||
},
|
||||
});
|
||||
|
||||
const mockDocument = {
|
||||
addEventListener: jest.fn(),
|
||||
createElement: jest.fn().mockReturnValue({
|
||||
setAttribute: jest.fn(),
|
||||
click: jest.fn(),
|
||||
style: {},
|
||||
}),
|
||||
body: {
|
||||
appendChild: jest.fn(),
|
||||
removeChild: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
Object.defineProperty(global, 'document', {
|
||||
writable: true,
|
||||
value: mockDocument,
|
||||
});
|
||||
|
||||
describe('generateSegmentationCSVReport', () => {
|
||||
const mockInfo = {
|
||||
reference: {
|
||||
SeriesNumber: '1',
|
||||
SeriesInstanceUID: 'series-uid-123',
|
||||
StudyInstanceUID: 'study-uid-456',
|
||||
SeriesDate: '20231201',
|
||||
SeriesTime: '120000',
|
||||
SeriesDescription: 'Test Series',
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should generate CSV with basic segmentation data', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
label: 'Test Segmentation',
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(global.Blob).toHaveBeenCalledWith([expect.stringContaining('Segmentation ID,seg-123')], {
|
||||
type: 'text/csv;charset=utf-8;',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle segmentation data without id and label', () => {
|
||||
const segmentationData = {};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(global.Blob).toHaveBeenCalledWith(
|
||||
[expect.stringContaining('Segmentation ID,\nSegmentation Label,')],
|
||||
{ type: 'text/csv;charset=utf-8;' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should include reference information when provided', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
label: 'Test Segmentation',
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(global.Blob).toHaveBeenCalledWith(
|
||||
[expect.stringContaining('reference Series Number,1')],
|
||||
{ type: 'text/csv;charset=utf-8;' }
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip empty reference values', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
label: 'Test Segmentation',
|
||||
};
|
||||
const infoWithEmptyValues = {
|
||||
reference: {
|
||||
SeriesNumber: '1',
|
||||
SeriesInstanceUID: '',
|
||||
StudyInstanceUID: null,
|
||||
SeriesDate: undefined,
|
||||
SeriesTime: '120000',
|
||||
SeriesDescription: 'Test Series',
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, infoWithEmptyValues);
|
||||
|
||||
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
|
||||
expect(csvContent).toContain('reference Series Number,1');
|
||||
expect(csvContent).toContain('reference Series Time,120000');
|
||||
expect(csvContent).not.toContain('reference Series Instance UID');
|
||||
expect(csvContent).not.toContain('reference Study Instance UID');
|
||||
expect(csvContent).not.toContain('reference Series Date');
|
||||
});
|
||||
|
||||
it('should handle segments with basic properties', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
label: 'Test Segmentation',
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
locked: true,
|
||||
active: false,
|
||||
},
|
||||
'2': {
|
||||
segmentIndex: 2,
|
||||
label: 'Segment 2',
|
||||
locked: false,
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
|
||||
expect(csvContent).toContain('Label,Segment 1,Segment 2');
|
||||
expect(csvContent).toContain('Segment Index,1,2');
|
||||
expect(csvContent).toContain('Locked,Yes,No');
|
||||
expect(csvContent).toContain('Active,No,Yes');
|
||||
});
|
||||
|
||||
it('should handle segments with statistics', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
label: 'Test Segmentation',
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
locked: false,
|
||||
active: true,
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
mean: {
|
||||
name: 'mean',
|
||||
label: 'Mean',
|
||||
value: 100.5,
|
||||
unit: 'HU',
|
||||
},
|
||||
volume: {
|
||||
name: 'volume',
|
||||
label: 'Volume',
|
||||
value: 250.75,
|
||||
unit: 'mm³',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
|
||||
expect(csvContent).toContain('Mean (HU),100.5');
|
||||
expect(csvContent).toContain('Volume (mm³),250.75');
|
||||
});
|
||||
|
||||
it('should handle segments with statistics without units', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
count: {
|
||||
name: 'count',
|
||||
label: 'Count',
|
||||
value: 42,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
|
||||
expect(csvContent).toContain('Count,42');
|
||||
});
|
||||
|
||||
it('should handle segments without cachedStats', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
locked: false,
|
||||
active: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(global.Blob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle segments with empty namedStats', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
namedStats: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(global.Blob).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should escape CSV special characters', () => {
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg,with,commas',
|
||||
label: 'Test "quoted" label',
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment\nwith\nnewlines',
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
test: {
|
||||
name: 'test',
|
||||
label: 'Test,Value',
|
||||
value: 'value"with"quotes',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
const csvContent = (global.Blob as jest.Mock).mock.calls[0][0][0];
|
||||
expect(csvContent).toContain('"seg,with,commas"');
|
||||
expect(csvContent).toContain('"Test ""quoted"" label"');
|
||||
expect(csvContent).toContain('"Segment\nwith\nnewlines"');
|
||||
});
|
||||
|
||||
it('should create download link with correct attributes', () => {
|
||||
const mockLink = {
|
||||
setAttribute: jest.fn(),
|
||||
click: jest.fn(),
|
||||
style: {},
|
||||
};
|
||||
mockDocument.createElement.mockReturnValue(mockLink);
|
||||
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
label: 'Test Segmentation',
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(mockLink.setAttribute).toHaveBeenCalledWith('href', 'mock-url');
|
||||
expect(mockLink.setAttribute).toHaveBeenCalledWith(
|
||||
'download',
|
||||
expect.stringMatching(/Test Segmentation_Report_\d{4}-\d{2}-\d{2}\.csv/)
|
||||
);
|
||||
expect(mockLink.click).toHaveBeenCalled();
|
||||
expect(mockDocument.body.appendChild).toHaveBeenCalledWith(mockLink);
|
||||
expect(mockDocument.body.removeChild).toHaveBeenCalledWith(mockLink);
|
||||
});
|
||||
|
||||
it('should use default filename when label is missing', () => {
|
||||
const mockLink = {
|
||||
setAttribute: jest.fn(),
|
||||
click: jest.fn(),
|
||||
style: {},
|
||||
};
|
||||
mockDocument.createElement.mockReturnValue(mockLink);
|
||||
|
||||
const segmentationData = {
|
||||
segmentationId: 'seg-123',
|
||||
};
|
||||
|
||||
generateSegmentationCSVReport(segmentationData, mockInfo);
|
||||
|
||||
expect(mockLink.setAttribute).toHaveBeenCalledWith(
|
||||
'download',
|
||||
expect.stringMatching(/Segmentation_Report_\d{4}-\d{2}-\d{2}\.csv/)
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,70 @@
|
||||
import { getViewportEnabledElement } from './getViewportEnabledElement';
|
||||
import getActiveViewportEnabledElement from './getActiveViewportEnabledElement';
|
||||
|
||||
jest.mock('./getViewportEnabledElement', () => ({
|
||||
getViewportEnabledElement: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getActiveViewportEnabledElement', () => {
|
||||
const mockViewportGridService = {
|
||||
getState: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return enabled element for active viewport', () => {
|
||||
const mockEnabledElement = { viewport: 'test-viewport' };
|
||||
mockViewportGridService.getState.mockReturnValue({ activeViewportId: 'viewport-1' });
|
||||
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getActiveViewportEnabledElement(mockViewportGridService);
|
||||
|
||||
expect(mockViewportGridService.getState).toHaveBeenCalledTimes(1);
|
||||
expect(getViewportEnabledElement).toHaveBeenCalledWith('viewport-1');
|
||||
expect(result).toBe(mockEnabledElement);
|
||||
});
|
||||
|
||||
it('should handle null activeViewportId', () => {
|
||||
const mockEnabledElement = null;
|
||||
mockViewportGridService.getState.mockReturnValue({ activeViewportId: null });
|
||||
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getActiveViewportEnabledElement(mockViewportGridService);
|
||||
|
||||
expect(getViewportEnabledElement).toHaveBeenCalledWith(null);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should handle undefined activeViewportId', () => {
|
||||
const mockEnabledElement = undefined;
|
||||
mockViewportGridService.getState.mockReturnValue({ activeViewportId: undefined });
|
||||
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getActiveViewportEnabledElement(mockViewportGridService);
|
||||
|
||||
expect(getViewportEnabledElement).toHaveBeenCalledWith(undefined);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle empty state object', () => {
|
||||
const mockEnabledElement = undefined;
|
||||
mockViewportGridService.getState.mockReturnValue({});
|
||||
(getViewportEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getActiveViewportEnabledElement(mockViewportGridService);
|
||||
|
||||
expect(getViewportEnabledElement).toHaveBeenCalledWith(undefined);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should handle getViewportEnabledElement returning null', () => {
|
||||
mockViewportGridService.getState.mockReturnValue({ activeViewportId: 'viewport-1' });
|
||||
(getViewportEnabledElement as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const result = getActiveViewportEnabledElement(mockViewportGridService);
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
313
extensions/cornerstone/src/utils/getCenterExtent.test.ts
Normal file
313
extensions/cornerstone/src/utils/getCenterExtent.test.ts
Normal file
@ -0,0 +1,313 @@
|
||||
import { getCenterExtent } from './getCenterExtent';
|
||||
|
||||
describe('getCenterExtent', () => {
|
||||
it('should return default values when points is undefined', () => {
|
||||
const measurement = {};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0, 0, 0],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default values when points is null', () => {
|
||||
const measurement = { points: null };
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0, 0, 0],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default values when points is not an array', () => {
|
||||
const measurement = { points: 'invalid' };
|
||||
|
||||
// @ts-expect-error - purposely passing an invalid type
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0, 0, 0],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return default values when points array is empty', () => {
|
||||
const measurement = { points: [] };
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0, 0, 0],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle single point correctly', () => {
|
||||
const measurement = {
|
||||
points: [[5, 10, 15]],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [5, 10, 15],
|
||||
extent: {
|
||||
min: [5, 10, 15],
|
||||
max: [5, 10, 15],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate center and extent for two points', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[0, 0, 0],
|
||||
[10, 20, 30],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [5, 10, 15],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [10, 20, 30],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should calculate center and extent for multiple points', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[0, 0, 0],
|
||||
[10, 20, 30],
|
||||
[5, 5, 5],
|
||||
[-5, 15, 25],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [2.5, 10, 15],
|
||||
extent: {
|
||||
min: [-5, 0, 0],
|
||||
max: [10, 20, 30],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle negative coordinates', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[-10, -20, -30],
|
||||
[-5, -15, -25],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [-7.5, -17.5, -27.5],
|
||||
extent: {
|
||||
min: [-10, -20, -30],
|
||||
max: [-5, -15, -25],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle mixed positive and negative coordinates', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[-10, -5, 0],
|
||||
[10, 5, 20],
|
||||
[0, 0, -10],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0, 0, 5],
|
||||
extent: {
|
||||
min: [-10, -5, -10],
|
||||
max: [10, 5, 20],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle floating point coordinates', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[1.5, 2.3, 3.1],
|
||||
[4.2, 8.9, 6.3],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [2.85, 5.6, 4.7],
|
||||
extent: {
|
||||
min: [1.5, 2.3, 3.1],
|
||||
max: [4.2, 8.9, 6.3],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle points with same coordinates', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[5, 5, 5],
|
||||
[5, 5, 5],
|
||||
[5, 5, 5],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [5, 5, 5],
|
||||
extent: {
|
||||
min: [5, 5, 5],
|
||||
max: [5, 5, 5],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle zero coordinates', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[0, 0, 0],
|
||||
[0, 0, 0],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0, 0, 0],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle large coordinate values', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[1000000, 2000000, 3000000],
|
||||
[1000001, 2000001, 3000001],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [1000000.5, 2000000.5, 3000000.5],
|
||||
extent: {
|
||||
min: [1000000, 2000000, 3000000],
|
||||
max: [1000001, 2000001, 3000001],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle points in different order', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[10, 20, 30],
|
||||
[0, 0, 0],
|
||||
[5, 10, 15],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [5, 10, 15],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [10, 20, 30],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle points where min and max are not the first point', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[5, 5, 5],
|
||||
[0, 0, 0],
|
||||
[10, 10, 10],
|
||||
[3, 3, 3],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [5, 5, 5],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [10, 10, 10],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle asymmetric extent distribution', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[0, 0, 0],
|
||||
[100, 10, 1],
|
||||
[50, 50, 50],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [50, 25, 25],
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [100, 50, 50],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle points with different ranges per dimension', () => {
|
||||
const measurement = {
|
||||
points: [
|
||||
[0, 100, 1000],
|
||||
[1, 101, 1001],
|
||||
[0.5, 100.5, 1000.5],
|
||||
],
|
||||
};
|
||||
|
||||
const result = getCenterExtent(measurement);
|
||||
|
||||
expect(result).toEqual({
|
||||
center: [0.5, 100.5, 1000.5],
|
||||
extent: {
|
||||
min: [0, 100, 1000],
|
||||
max: [1, 101, 1001],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,59 @@
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
import getCornerstoneBlendMode from './getCornerstoneBlendMode';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
Enums: {
|
||||
BlendModes: {
|
||||
COMPOSITE: 'composite',
|
||||
MAXIMUM_INTENSITY_BLEND: 'mip',
|
||||
MINIMUM_INTENSITY_BLEND: 'minip',
|
||||
AVERAGE_INTENSITY_BLEND: 'avg',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('getCornerstoneBlendMode', () => {
|
||||
it('should return COMPOSITE when blendMode is null', () => {
|
||||
const result = getCornerstoneBlendMode(null);
|
||||
expect(result).toBe(Enums.BlendModes.COMPOSITE);
|
||||
});
|
||||
|
||||
it('should return COMPOSITE when blendMode is undefined', () => {
|
||||
const result = getCornerstoneBlendMode(undefined);
|
||||
expect(result).toBe(Enums.BlendModes.COMPOSITE);
|
||||
});
|
||||
|
||||
it('should return COMPOSITE when blendMode is empty string', () => {
|
||||
const result = getCornerstoneBlendMode('');
|
||||
expect(result).toBe(Enums.BlendModes.COMPOSITE);
|
||||
});
|
||||
|
||||
it('should return MAXIMUM_INTENSITY_BLEND when blendMode is mip', () => {
|
||||
const result = getCornerstoneBlendMode('mip');
|
||||
expect(result).toBe(Enums.BlendModes.MAXIMUM_INTENSITY_BLEND);
|
||||
});
|
||||
|
||||
it('should return MAXIMUM_INTENSITY_BLEND when blendMode is MIP (uppercase)', () => {
|
||||
const result = getCornerstoneBlendMode('MIP');
|
||||
expect(result).toBe(Enums.BlendModes.MAXIMUM_INTENSITY_BLEND);
|
||||
});
|
||||
|
||||
it('should return MINIMUM_INTENSITY_BLEND when blendMode is MINIP (uppercase)', () => {
|
||||
const result = getCornerstoneBlendMode('MINIP');
|
||||
expect(result).toBe(Enums.BlendModes.MINIMUM_INTENSITY_BLEND);
|
||||
});
|
||||
|
||||
it('should return AVERAGE_INTENSITY_BLEND when blendMode is avg', () => {
|
||||
const result = getCornerstoneBlendMode('avg');
|
||||
expect(result).toBe(Enums.BlendModes.AVERAGE_INTENSITY_BLEND);
|
||||
});
|
||||
|
||||
it('should throw error for unsupported blend mode', () => {
|
||||
expect(() => getCornerstoneBlendMode('invalid')).toThrow('Unsupported blend mode: invalid');
|
||||
});
|
||||
|
||||
it('should handle mixed case blend mode', () => {
|
||||
const result = getCornerstoneBlendMode('MiP');
|
||||
expect(result).toBe(Enums.BlendModes.MAXIMUM_INTENSITY_BLEND);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,60 @@
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
import getCornerstoneOrientation from './getCornerstoneOrientation';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
Enums: {
|
||||
OrientationAxis: {
|
||||
AXIAL: 'axial',
|
||||
SAGITTAL: 'sagittal',
|
||||
CORONAL: 'coronal',
|
||||
ACQUISITION: 'acquisition',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('getCornerstoneOrientation', () => {
|
||||
it('should return AXIAL when orientation is axial', () => {
|
||||
const result = getCornerstoneOrientation('axial');
|
||||
expect(result).toBe(Enums.OrientationAxis.AXIAL);
|
||||
});
|
||||
|
||||
it('should return AXIAL when orientation is AXIAL (uppercase)', () => {
|
||||
const result = getCornerstoneOrientation('AXIAL');
|
||||
expect(result).toBe(Enums.OrientationAxis.AXIAL);
|
||||
});
|
||||
|
||||
it('should return SAGITTAL when orientation is sagittal', () => {
|
||||
const result = getCornerstoneOrientation('sagittal');
|
||||
expect(result).toBe(Enums.OrientationAxis.SAGITTAL);
|
||||
});
|
||||
|
||||
it('should return CORONAL when orientation is coronal', () => {
|
||||
const result = getCornerstoneOrientation('coronal');
|
||||
expect(result).toBe(Enums.OrientationAxis.CORONAL);
|
||||
});
|
||||
|
||||
it('should return ACQUISITION for unknown orientation', () => {
|
||||
const result = getCornerstoneOrientation('unknown');
|
||||
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
|
||||
});
|
||||
|
||||
it('should return ACQUISITION when orientation is null', () => {
|
||||
const result = getCornerstoneOrientation(null);
|
||||
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
|
||||
});
|
||||
|
||||
it('should return ACQUISITION when orientation is undefined', () => {
|
||||
const result = getCornerstoneOrientation(undefined);
|
||||
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
|
||||
});
|
||||
|
||||
it('should return ACQUISITION when orientation is empty string', () => {
|
||||
const result = getCornerstoneOrientation('');
|
||||
expect(result).toBe(Enums.OrientationAxis.ACQUISITION);
|
||||
});
|
||||
|
||||
it('should handle mixed case orientation', () => {
|
||||
const result = getCornerstoneOrientation('CoRoNaL');
|
||||
expect(result).toBe(Enums.OrientationAxis.CORONAL);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,91 @@
|
||||
import type { Types } from '@ohif/core';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
import getCornerstoneViewportType from './getCornerstoneViewportType';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
Enums: {
|
||||
ViewportType: {
|
||||
STACK: 'stack',
|
||||
VIDEO: 'video',
|
||||
WHOLE_SLIDE: 'wholeslide',
|
||||
ORTHOGRAPHIC: 'orthographic',
|
||||
VOLUME_3D: 'volume3d',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('getCornerstoneViewportType', () => {
|
||||
it('should return STACK when viewportType is stack', () => {
|
||||
const result = getCornerstoneViewportType('stack');
|
||||
expect(result).toBe(Enums.ViewportType.STACK);
|
||||
});
|
||||
|
||||
it('should return STACK when viewportType is STACK (uppercase)', () => {
|
||||
const result = getCornerstoneViewportType('STACK');
|
||||
expect(result).toBe(Enums.ViewportType.STACK);
|
||||
});
|
||||
|
||||
it('should return VIDEO when viewportType is video', () => {
|
||||
const result = getCornerstoneViewportType('video');
|
||||
expect(result).toBe(Enums.ViewportType.VIDEO);
|
||||
});
|
||||
|
||||
it('should return WHOLE_SLIDE when viewportType is wholeslide', () => {
|
||||
const result = getCornerstoneViewportType('wholeslide');
|
||||
expect(result).toBe(Enums.ViewportType.WHOLE_SLIDE);
|
||||
});
|
||||
|
||||
it('should return ORTHOGRAPHIC when viewportType is volume', () => {
|
||||
const result = getCornerstoneViewportType('volume');
|
||||
expect(result).toBe(Enums.ViewportType.ORTHOGRAPHIC);
|
||||
});
|
||||
|
||||
it('should return ORTHOGRAPHIC when viewportType is orthographic', () => {
|
||||
const result = getCornerstoneViewportType('orthographic');
|
||||
expect(result).toBe(Enums.ViewportType.ORTHOGRAPHIC);
|
||||
});
|
||||
|
||||
it('should return VOLUME_3D when viewportType is volume3d', () => {
|
||||
const result = getCornerstoneViewportType('volume3d');
|
||||
expect(result).toBe(Enums.ViewportType.VOLUME_3D);
|
||||
});
|
||||
|
||||
it('should throw error for invalid viewport type', () => {
|
||||
expect(() => getCornerstoneViewportType('invalid')).toThrow(
|
||||
'Invalid viewport type: invalid. Valid types are: stack, volume, video, wholeslide'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use displaySet viewportType when provided', () => {
|
||||
const displaySets = [{ viewportType: 'stack' }] as Types.DisplaySet[];
|
||||
const result = getCornerstoneViewportType('volume', displaySets);
|
||||
expect(result).toBe(Enums.ViewportType.STACK);
|
||||
});
|
||||
|
||||
it('should use displaySet viewportType with case insensitive matching', () => {
|
||||
const displaySets = [{ viewportType: 'VIDEO' }] as Types.DisplaySet[];
|
||||
const result = getCornerstoneViewportType('stack', displaySets);
|
||||
expect(result).toBe(Enums.ViewportType.VIDEO);
|
||||
});
|
||||
|
||||
it('should fallback to viewportType when displaySet has no viewportType', () => {
|
||||
const displaySets = [{}] as Types.DisplaySet[];
|
||||
const result = getCornerstoneViewportType('volume', displaySets);
|
||||
expect(result).toBe(Enums.ViewportType.ORTHOGRAPHIC);
|
||||
});
|
||||
|
||||
it('should handle empty displaySets array', () => {
|
||||
const result = getCornerstoneViewportType('stack', []);
|
||||
expect(result).toBe(Enums.ViewportType.STACK);
|
||||
});
|
||||
|
||||
it('should handle null displaySets', () => {
|
||||
const result = getCornerstoneViewportType('video', null);
|
||||
expect(result).toBe(Enums.ViewportType.VIDEO);
|
||||
});
|
||||
|
||||
it('should handle undefined displaySets', () => {
|
||||
const result = getCornerstoneViewportType('wholeslide', undefined);
|
||||
expect(result).toBe(Enums.ViewportType.WHOLE_SLIDE);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,85 @@
|
||||
import getInterleavedFrames from './getInterleavedFrames';
|
||||
|
||||
describe('getInterleavedFrames', () => {
|
||||
it('should return single element when input has one element', () => {
|
||||
const imageIds = ['image-1'];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toEqual([{ imageId: 'image-1', imageIdIndex: 0 }]);
|
||||
});
|
||||
|
||||
it('should return correct order for three elements', () => {
|
||||
const imageIds = ['image-1', 'image-2', 'image-3'];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ imageId: 'image-2', imageIdIndex: 1 },
|
||||
{ imageId: 'image-1', imageIdIndex: 0 },
|
||||
{ imageId: 'image-3', imageIdIndex: 2 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should start with middle element', () => {
|
||||
const imageIds = ['image-1', 'image-2', 'image-3', 'image-4', 'image-5'];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result[0]).toEqual({ imageId: 'image-3', imageIdIndex: 2 });
|
||||
});
|
||||
|
||||
it('should interleave elements correctly for even length array', () => {
|
||||
const imageIds = ['image-1', 'image-2', 'image-3', 'image-4', 'image-5', 'image-6'];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ imageId: 'image-4', imageIdIndex: 3 },
|
||||
{ imageId: 'image-3', imageIdIndex: 2 },
|
||||
{ imageId: 'image-5', imageIdIndex: 4 },
|
||||
{ imageId: 'image-2', imageIdIndex: 1 },
|
||||
{ imageId: 'image-6', imageIdIndex: 5 },
|
||||
{ imageId: 'image-1', imageIdIndex: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should interleave elements correctly for odd length array', () => {
|
||||
const imageIds = ['image-1', 'image-2', 'image-3', 'image-4', 'image-5', 'image-6', 'image-7'];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ imageId: 'image-4', imageIdIndex: 3 },
|
||||
{ imageId: 'image-3', imageIdIndex: 2 },
|
||||
{ imageId: 'image-5', imageIdIndex: 4 },
|
||||
{ imageId: 'image-2', imageIdIndex: 1 },
|
||||
{ imageId: 'image-6', imageIdIndex: 5 },
|
||||
{ imageId: 'image-1', imageIdIndex: 0 },
|
||||
{ imageId: 'image-7', imageIdIndex: 6 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle large array correctly', () => {
|
||||
const imageIds = Array.from({ length: 10 }, (_, i) => `image-${i + 1}`);
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toHaveLength(10);
|
||||
expect(result[0]).toEqual({ imageId: 'image-6', imageIdIndex: 5 });
|
||||
expect(result[8]).toEqual({ imageId: 'image-10', imageIdIndex: 9 });
|
||||
expect(result[9]).toEqual({ imageId: 'image-1', imageIdIndex: 0 });
|
||||
});
|
||||
|
||||
it('should handle empty array', () => {
|
||||
const imageIds = [];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle duplicate imageIds with different indices', () => {
|
||||
const imageIds = ['duplicate', 'unique', 'duplicate'];
|
||||
const result = getInterleavedFrames(imageIds);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ imageId: 'unique', imageIdIndex: 1 },
|
||||
{ imageId: 'duplicate', imageIdIndex: 0 },
|
||||
{ imageId: 'duplicate', imageIdIndex: 2 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,17 @@
|
||||
export default function getInterleavedFrames(imageIds) {
|
||||
interface ImageIdToPrefetch {
|
||||
imageId: string;
|
||||
imageIdIndex: number;
|
||||
}
|
||||
|
||||
export default function getInterleavedFrames(imageIds: string[]): ImageIdToPrefetch[] {
|
||||
if (imageIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (imageIds.length === 1) {
|
||||
return [{ imageId: imageIds[0], imageIdIndex: 0 }];
|
||||
}
|
||||
|
||||
const minImageIdIndex = 0;
|
||||
const maxImageIdIndex = imageIds.length - 1;
|
||||
|
||||
@ -8,7 +21,7 @@ export default function getInterleavedFrames(imageIds) {
|
||||
let upperImageIdIndex = middleImageIdIndex;
|
||||
|
||||
// Build up an array of images to prefetch, starting with the current image.
|
||||
const imageIdsToPrefetch = [
|
||||
const imageIdsToPrefetch: ImageIdToPrefetch[] = [
|
||||
{ imageId: imageIds[middleImageIdIndex], imageIdIndex: middleImageIdIndex },
|
||||
];
|
||||
|
||||
145
extensions/cornerstone/src/utils/getNthFrames.test.ts
Normal file
145
extensions/cornerstone/src/utils/getNthFrames.test.ts
Normal file
@ -0,0 +1,145 @@
|
||||
import getNthFrames from './getNthFrames';
|
||||
|
||||
describe('getNthFrames', () => {
|
||||
const createMockImageLoadRequest = (imageId: string, imageIdIndex: number) => ({
|
||||
imageId,
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
imageIdIndex,
|
||||
options: { test: 'option' },
|
||||
});
|
||||
|
||||
it('should return empty array when input is empty', () => {
|
||||
const result = getNthFrames([]);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return same array when input has one element', () => {
|
||||
const imageIds = [createMockImageLoadRequest('image-1', 0)];
|
||||
const result = getNthFrames(imageIds);
|
||||
expect(result).toEqual(imageIds);
|
||||
});
|
||||
|
||||
it('should return same array when input has two elements', () => {
|
||||
const imageIds = [
|
||||
createMockImageLoadRequest('image-1', 0),
|
||||
createMockImageLoadRequest('image-2', 1),
|
||||
];
|
||||
const result = getNthFrames(imageIds);
|
||||
expect(result).toEqual(imageIds);
|
||||
});
|
||||
|
||||
it('should prioritize first two elements', () => {
|
||||
const imageIds = Array.from({ length: 10 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
expect(result[0]).toBe(imageIds[0]);
|
||||
expect(result[1]).toBe(imageIds[1]);
|
||||
});
|
||||
|
||||
it('should prioritize last three elements', () => {
|
||||
const imageIds = Array.from({ length: 10 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
expect(result).toContain(imageIds[7]);
|
||||
expect(result).toContain(imageIds[8]);
|
||||
expect(result).toContain(imageIds[9]);
|
||||
});
|
||||
|
||||
it('should include center elements', () => {
|
||||
const imageIds = Array.from({ length: 20 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
const centerStart = imageIds.length / 2 - 3;
|
||||
const centerEnd = centerStart + 6;
|
||||
|
||||
for (let i = Math.ceil(centerStart); i < centerEnd; i++) {
|
||||
if (i >= 0 && i < imageIds.length) {
|
||||
expect(result).toContain(imageIds[i]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should include nth elements where i % 7 === 2', () => {
|
||||
const imageIds = Array.from({ length: 30 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
if (i % 7 === 2 && i >= 2 && i <= imageIds.length - 4) {
|
||||
const centerStart = imageIds.length / 2 - 3;
|
||||
const centerEnd = centerStart + 6;
|
||||
if (!(i > centerStart && i < centerEnd)) {
|
||||
expect(result).toContain(imageIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should include nth elements where i % 7 === 5', () => {
|
||||
const imageIds = Array.from({ length: 30 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
for (let i = 0; i < imageIds.length; i++) {
|
||||
if (i % 7 === 5 && i >= 2 && i <= imageIds.length - 4) {
|
||||
const centerStart = imageIds.length / 2 - 3;
|
||||
const centerEnd = centerStart + 6;
|
||||
if (!(i > centerStart && i < centerEnd)) {
|
||||
expect(result).toContain(imageIds[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle large arrays correctly', () => {
|
||||
const imageIds = Array.from({ length: 100 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
expect(result.length).toBe(100);
|
||||
expect(result[0]).toBe(imageIds[0]);
|
||||
expect(result[1]).toBe(imageIds[1]);
|
||||
expect(result).toContain(imageIds[97]);
|
||||
expect(result).toContain(imageIds[98]);
|
||||
expect(result).toContain(imageIds[99]);
|
||||
});
|
||||
|
||||
it('should handle arrays where centerStart is negative', () => {
|
||||
const imageIds = Array.from({ length: 3 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
expect(result).toEqual(imageIds);
|
||||
});
|
||||
|
||||
it('should handle arrays where centerEnd exceeds length', () => {
|
||||
const imageIds = Array.from({ length: 4 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
expect(result).toEqual(expect.arrayContaining(imageIds));
|
||||
expect(result.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should preserve object references', () => {
|
||||
const imageIds = Array.from({ length: 10 }, (_, i) =>
|
||||
createMockImageLoadRequest(`image-${i}`, i)
|
||||
);
|
||||
const result = getNthFrames(imageIds);
|
||||
|
||||
result.forEach(item => {
|
||||
expect(imageIds).toContain(item);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -9,12 +9,9 @@
|
||||
* What this does is return the first/center/start objects, as those
|
||||
* are often used first, then a selection of objects scattered over the
|
||||
* instances in order to allow making requests over a set of image instances.
|
||||
*
|
||||
* @param {[]} imageIds
|
||||
* @returns [] reordered to be an nth selection
|
||||
*/
|
||||
export default function getNthFrames(imageIds) {
|
||||
const frames = [[], [], [], [], []];
|
||||
export default function getNthFrames<T>(imageIds: T[]): T[] {
|
||||
const frames: T[][] = [[], [], [], [], []];
|
||||
const centerStart = imageIds.length / 2 - 3;
|
||||
const centerEnd = centerStart + 6;
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
import { getEnabledElement } from '@cornerstonejs/core';
|
||||
import { getEnabledElement as OHIFgetEnabledElement } from '../state';
|
||||
import { getViewportEnabledElement } from './getViewportEnabledElement';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
getEnabledElement: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../state', () => ({
|
||||
getEnabledElement: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getViewportEnabledElement', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return enabled element when OHIF getEnabledElement returns element', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
const mockEnabledElement = { viewport: 'test-viewport' };
|
||||
|
||||
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({ element: mockElement });
|
||||
(getEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getViewportEnabledElement('test-viewport-id');
|
||||
|
||||
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
|
||||
expect(getEnabledElement).toHaveBeenCalledWith(mockElement);
|
||||
expect(result).toBe(mockEnabledElement);
|
||||
});
|
||||
|
||||
it('should return enabled element when OHIF getEnabledElement returns null', () => {
|
||||
const mockEnabledElement = { viewport: 'test-viewport' };
|
||||
|
||||
(OHIFgetEnabledElement as jest.Mock).mockReturnValue(null);
|
||||
(getEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getViewportEnabledElement('test-viewport-id');
|
||||
|
||||
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
|
||||
expect(getEnabledElement).toHaveBeenCalledWith(undefined);
|
||||
expect(result).toBe(mockEnabledElement);
|
||||
});
|
||||
|
||||
it('should return enabled element when OHIF getEnabledElement returns object without element', () => {
|
||||
const mockEnabledElement = { viewport: 'test-viewport' };
|
||||
|
||||
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({});
|
||||
(getEnabledElement as jest.Mock).mockReturnValue(mockEnabledElement);
|
||||
|
||||
const result = getViewportEnabledElement('test-viewport-id');
|
||||
|
||||
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
|
||||
expect(getEnabledElement).toHaveBeenCalledWith(undefined);
|
||||
expect(result).toBe(mockEnabledElement);
|
||||
});
|
||||
|
||||
it('should return null when cornerstone getEnabledElement returns null', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
|
||||
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({ element: mockElement });
|
||||
(getEnabledElement as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const result = getViewportEnabledElement('test-viewport-id');
|
||||
|
||||
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
|
||||
expect(getEnabledElement).toHaveBeenCalledWith(mockElement);
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return undefined when cornerstone getEnabledElement returns undefined', () => {
|
||||
const mockElement = document.createElement('div');
|
||||
|
||||
(OHIFgetEnabledElement as jest.Mock).mockReturnValue({ element: mockElement });
|
||||
(getEnabledElement as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
const result = getViewportEnabledElement('test-viewport-id');
|
||||
|
||||
expect(OHIFgetEnabledElement).toHaveBeenCalledWith('test-viewport-id');
|
||||
expect(getEnabledElement).toHaveBeenCalledWith(mockElement);
|
||||
expect(result).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,50 @@
|
||||
import { getViewportOrientationFromImageOrientationPatient } from './getViewportOrientationFromImageOrientationPatient';
|
||||
|
||||
describe('getViewportOrientationFromImageOrientationPatient', () => {
|
||||
it('should return undefined when imageOrientationPatient is null', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient(null);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when imageOrientationPatient is undefined', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient(undefined);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when imageOrientationPatient has length less than 6', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 1]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when imageOrientationPatient has length greater than 6', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 1, 0, 0]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when imageOrientationPatient is empty array', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return "axial" when imageOrientationPatient matches axial orientation', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 1, 0]);
|
||||
expect(result).toBe('axial');
|
||||
});
|
||||
|
||||
it('should return "sagittal" when imageOrientationPatient matches sagittal orientation', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([0, 1, 0, 0, 0, -1]);
|
||||
expect(result).toBe('sagittal');
|
||||
});
|
||||
|
||||
it('should return "coronal" when imageOrientationPatient matches coronal orientation', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([1, 0, 0, 0, 0, -1]);
|
||||
expect(result).toBe('coronal');
|
||||
});
|
||||
|
||||
it('should return undefined when no orientation matches', () => {
|
||||
const result = getViewportOrientationFromImageOrientationPatient([
|
||||
0.5, 0.5, 0.7, 0.3, 0.8, 0.5,
|
||||
]);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
387
extensions/cornerstone/src/utils/hydrationUtils.test.ts
Normal file
387
extensions/cornerstone/src/utils/hydrationUtils.test.ts
Normal file
@ -0,0 +1,387 @@
|
||||
import { getUpdatedViewportsForSegmentation } from './hydrationUtils';
|
||||
|
||||
describe('getUpdatedViewportsForSegmentation', () => {
|
||||
const mockHangingProtocolService = {
|
||||
getViewportsRequireUpdate: jest.fn(),
|
||||
};
|
||||
|
||||
const mockViewportGridService = {
|
||||
getState: jest.fn(),
|
||||
};
|
||||
|
||||
const mockServicesManager = {
|
||||
services: {
|
||||
hangingProtocolService: mockHangingProtocolService,
|
||||
viewportGridService: mockViewportGridService,
|
||||
},
|
||||
};
|
||||
|
||||
const mockViewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'target-viewport-id',
|
||||
},
|
||||
};
|
||||
|
||||
const mockViewports = new Map([
|
||||
['viewport-1', mockViewport],
|
||||
['active-viewport-id', mockViewport],
|
||||
]);
|
||||
|
||||
const defaultParameters = {
|
||||
viewportId: 'viewport-1',
|
||||
servicesManager: mockServicesManager as unknown as AppTypes.ServicesManager,
|
||||
displaySetInstanceUIDs: ['display-set-1'],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
isHangingProtocolLayout: true,
|
||||
viewports: mockViewports,
|
||||
activeViewportId: 'active-viewport-id',
|
||||
});
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue([]);
|
||||
});
|
||||
|
||||
it('should get updated viewports for segmentation', () => {
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(mockViewportGridService.getState).toHaveBeenCalled();
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
mockViewport.viewportOptions.viewportId,
|
||||
defaultParameters.displaySetInstanceUIDs[0],
|
||||
true
|
||||
);
|
||||
expect(result).toEqual(mockUpdatedViewports);
|
||||
});
|
||||
|
||||
it('should filter out volume3d viewports', () => {
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume3d',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle viewports without viewportOptions', () => {
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
someOtherProperty: 'value',
|
||||
},
|
||||
{
|
||||
viewportOptions: null,
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
someOtherProperty: 'value',
|
||||
},
|
||||
{
|
||||
viewportOptions: null,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should use activeViewportId when viewportId is not provided', () => {
|
||||
const result = getUpdatedViewportsForSegmentation({
|
||||
...defaultParameters,
|
||||
viewportId: null,
|
||||
});
|
||||
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
mockViewport.viewportOptions.viewportId,
|
||||
defaultParameters.displaySetInstanceUIDs[0],
|
||||
true
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should use activeViewportId when viewportId is undefined', () => {
|
||||
const result = getUpdatedViewportsForSegmentation({
|
||||
...defaultParameters,
|
||||
viewportId: undefined,
|
||||
});
|
||||
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
mockViewport.viewportOptions.viewportId,
|
||||
defaultParameters.displaySetInstanceUIDs[0],
|
||||
true
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle isHangingProtocolLayout false', () => {
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
isHangingProtocolLayout: false,
|
||||
viewports: mockViewports,
|
||||
activeViewportId: 'active-viewport-id',
|
||||
});
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
mockViewport.viewportOptions.viewportId,
|
||||
defaultParameters.displaySetInstanceUIDs[0],
|
||||
false
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle multiple displaySetInstanceUIDs by using first one', () => {
|
||||
const result = getUpdatedViewportsForSegmentation({
|
||||
...defaultParameters,
|
||||
displaySetInstanceUIDs: ['display-set-1', 'display-set-2', 'display-set-3'],
|
||||
});
|
||||
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
mockViewport.viewportOptions.viewportId,
|
||||
'display-set-1',
|
||||
true
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle empty displaySetInstanceUIDs array', () => {
|
||||
const result = getUpdatedViewportsForSegmentation({
|
||||
...defaultParameters,
|
||||
displaySetInstanceUIDs: [],
|
||||
});
|
||||
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
mockViewport.viewportOptions.viewportId,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle viewport not found in viewports map', () => {
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
isHangingProtocolLayout: true,
|
||||
viewports: new Map(),
|
||||
activeViewportId: 'non-existent-viewport',
|
||||
});
|
||||
|
||||
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle viewport with missing viewportOptions', () => {
|
||||
const viewportWithoutOptions = {};
|
||||
const viewportsMap = new Map([['viewport-1', viewportWithoutOptions]]);
|
||||
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
isHangingProtocolLayout: true,
|
||||
viewports: viewportsMap,
|
||||
activeViewportId: 'active-viewport-id',
|
||||
});
|
||||
|
||||
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle viewport with null viewportOptions', () => {
|
||||
const viewportWithNullOptions = {
|
||||
viewportOptions: null,
|
||||
};
|
||||
const viewportsMap = new Map([['viewport-1', viewportWithNullOptions]]);
|
||||
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
isHangingProtocolLayout: true,
|
||||
viewports: viewportsMap,
|
||||
activeViewportId: 'active-viewport-id',
|
||||
});
|
||||
|
||||
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle getViewportsRequireUpdate returning null', () => {
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(null);
|
||||
|
||||
expect(() => getUpdatedViewportsForSegmentation(defaultParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle mixed viewport types including volume3d', () => {
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume3d',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume3d',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'orthogonal',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'orthogonal',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle all volume3d viewports', () => {
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume3d',
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'volume3d',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle viewports with undefined viewportType', () => {
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: undefined,
|
||||
},
|
||||
},
|
||||
{
|
||||
viewportOptions: {
|
||||
someOtherProperty: 'value',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(result).toEqual(mockUpdatedViewports);
|
||||
});
|
||||
|
||||
it('should handle complex viewport structure', () => {
|
||||
const complexViewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'complex-viewport-id',
|
||||
viewportType: 'stack',
|
||||
orientation: 'axial',
|
||||
initialImageOptions: {
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
displaySetOptions: {
|
||||
displaySetInstanceUID: 'display-set-1',
|
||||
},
|
||||
};
|
||||
|
||||
const viewportsMap = new Map([['viewport-1', complexViewport]]);
|
||||
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
isHangingProtocolLayout: true,
|
||||
viewports: viewportsMap,
|
||||
activeViewportId: 'active-viewport-id',
|
||||
});
|
||||
|
||||
const mockUpdatedViewports = [
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportType: 'stack',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockHangingProtocolService.getViewportsRequireUpdate.mockReturnValue(mockUpdatedViewports);
|
||||
|
||||
const result = getUpdatedViewportsForSegmentation(defaultParameters);
|
||||
|
||||
expect(mockHangingProtocolService.getViewportsRequireUpdate).toHaveBeenCalledWith(
|
||||
'complex-viewport-id',
|
||||
defaultParameters.displaySetInstanceUIDs[0],
|
||||
true
|
||||
);
|
||||
expect(result).toEqual(mockUpdatedViewports);
|
||||
});
|
||||
});
|
||||
342
extensions/cornerstone/src/utils/initViewTiming.test.ts
Normal file
342
extensions/cornerstone/src/utils/initViewTiming.test.ts
Normal file
@ -0,0 +1,342 @@
|
||||
import { log, Enums } from '@ohif/core';
|
||||
import { EVENTS } from '@cornerstonejs/core';
|
||||
import initViewTiming from './initViewTiming';
|
||||
|
||||
jest.mock('@ohif/core', () => ({
|
||||
log: {
|
||||
timingKeys: {},
|
||||
timeEnd: jest.fn(),
|
||||
},
|
||||
Enums: {
|
||||
TimingEnum: {
|
||||
DISPLAY_SETS_TO_ALL_IMAGES: 'DISPLAY_SETS_TO_ALL_IMAGES',
|
||||
DISPLAY_SETS_TO_FIRST_IMAGE: 'DISPLAY_SETS_TO_FIRST_IMAGE',
|
||||
STUDY_TO_FIRST_IMAGE: 'STUDY_TO_FIRST_IMAGE',
|
||||
SCRIPT_TO_VIEW: 'SCRIPT_TO_VIEW',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
EVENTS: {
|
||||
IMAGE_RENDERED: 'IMAGE_RENDERED',
|
||||
},
|
||||
}));
|
||||
|
||||
describe('initViewTiming', () => {
|
||||
const mockElement = {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultParameters = {
|
||||
element: mockElement,
|
||||
};
|
||||
|
||||
const clearUnevenlyHandledViewportState = () => {
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
imageRenderedListener({
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// @ts-expect-error - restarting the object that will have values set by each test
|
||||
log.timingKeys = {};
|
||||
});
|
||||
|
||||
it('should return early when no timing keys are set', () => {
|
||||
// @ts-expect-error - the idea is to test an invalid state
|
||||
log.timingKeys = {};
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should add event listener when timing keys are present', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.IMAGE_RENDERED,
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
clearUnevenlyHandledViewportState();
|
||||
});
|
||||
|
||||
it('should handle multiple timing keys', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES] = true;
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.IMAGE_RENDERED,
|
||||
expect.any(Function)
|
||||
);
|
||||
|
||||
clearUnevenlyHandledViewportState();
|
||||
});
|
||||
|
||||
it('should return early in imageRenderedListener when viewportStatus is preRender', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
viewportStatus: 'preRender',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(log.timeEnd).not.toHaveBeenCalled();
|
||||
expect(mockElement.removeEventListener).not.toHaveBeenCalled();
|
||||
|
||||
clearUnevenlyHandledViewportState();
|
||||
});
|
||||
|
||||
it('should call timeEnd for timing keys when image is rendered', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.SCRIPT_TO_VIEW);
|
||||
});
|
||||
|
||||
it('should remove event listener after image is rendered', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.IMAGE_RENDERED,
|
||||
imageRenderedListener
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call timeEnd for ALL_IMAGES when viewports are still waiting', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE);
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.SCRIPT_TO_VIEW);
|
||||
expect(log.timeEnd).not.toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
|
||||
clearUnevenlyHandledViewportState();
|
||||
});
|
||||
|
||||
it('should handle multiple viewports finishing in sequence', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
const mockElement2 = {
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
initViewTiming({ element: mockElement2 });
|
||||
|
||||
const imageRenderedListener1 = mockElement.addEventListener.mock.calls[0][1];
|
||||
const imageRenderedListener2 = mockElement2.addEventListener.mock.calls[0][1];
|
||||
|
||||
const evt1 = {
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
const evt2 = {
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement2,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener1(evt1);
|
||||
|
||||
expect(log.timeEnd).not.toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
|
||||
imageRenderedListener2(evt2);
|
||||
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
});
|
||||
|
||||
it('should call timeEnd for ALL_IMAGES when last viewport finishes', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
viewportStatus: 'rendered',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES);
|
||||
});
|
||||
|
||||
it('should handle different viewportStatus values', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
viewportStatus: 'loading',
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle undefined viewportStatus', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {
|
||||
detail: {
|
||||
element: mockElement,
|
||||
},
|
||||
};
|
||||
|
||||
imageRenderedListener(evt);
|
||||
|
||||
expect(log.timeEnd).toHaveBeenCalledWith(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);
|
||||
expect(mockElement.removeEventListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle missing detail object', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
const imageRenderedListener = mockElement.addEventListener.mock.calls[0][1];
|
||||
const evt = {};
|
||||
|
||||
expect(() => imageRenderedListener(evt)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle timing keys with falsy values', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = false;
|
||||
log.timingKeys[Enums.TimingEnum.STUDY_TO_FIRST_IMAGE] = null;
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES] = undefined;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle timing keys with truthy values', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = 'some value';
|
||||
log.timingKeys[Enums.TimingEnum.STUDY_TO_FIRST_IMAGE] = 1;
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES] = {};
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.IMAGE_RENDERED,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should initialize IMAGE_TIMING_KEYS on first call', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty element object', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
const emptyElement = {};
|
||||
|
||||
expect(() => initViewTiming({ element: emptyElement })).toThrow();
|
||||
});
|
||||
|
||||
it('should handle null element', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
expect(() => initViewTiming({ element: null })).toThrow();
|
||||
});
|
||||
|
||||
it('should handle multiple calls with same element', () => {
|
||||
log.timingKeys[Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE] = true;
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
initViewTiming(defaultParameters);
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should handle case where no timing keys match', () => {
|
||||
// @ts-expect-error - the idea is to test an invalid state
|
||||
log.timingKeys = {
|
||||
someOtherKey: true,
|
||||
anotherKey: 'value',
|
||||
};
|
||||
|
||||
initViewTiming(defaultParameters);
|
||||
|
||||
expect(mockElement.addEventListener).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,381 @@
|
||||
import { eventTarget, EVENTS } from '@cornerstonejs/core';
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import { initializeWebWorkerProgressHandler } from './initWebWorkerProgressHandler';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
eventTarget: {
|
||||
addEventListener: jest.fn(),
|
||||
},
|
||||
EVENTS: {
|
||||
WEB_WORKER_PROGRESS: 'WEB_WORKER_PROGRESS',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@cornerstonejs/tools', () => ({
|
||||
Enums: {
|
||||
WorkerTypes: {
|
||||
COMPUTE_STATISTICS: 'COMPUTE_STATISTICS',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('initializeWebWorkerProgressHandler', () => {
|
||||
const mockUINotificationService = {
|
||||
show: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.spyOn(console, 'error').mockImplementation();
|
||||
jest.spyOn(console, 'debug').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should register event listener for WEB_WORKER_PROGRESS', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
expect(eventTarget.addEventListener).toHaveBeenCalledWith(
|
||||
EVENTS.WEB_WORKER_PROGRESS,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip notifications for COMPUTE_STATISTICS worker type', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: cornerstoneTools.Enums.WorkerTypes.COMPUTE_STATISTICS,
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
|
||||
expect(mockUINotificationService.show).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should show notification when progress is 0 for new task', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledWith({
|
||||
id: 'worker-task-test_worker',
|
||||
title: 'TEST_WORKER',
|
||||
message: 'Computing...',
|
||||
autoClose: false,
|
||||
allowDuplicates: false,
|
||||
deduplicationInterval: 60000,
|
||||
promise: expect.any(Promise),
|
||||
promiseMessages: {
|
||||
loading: 'Computing...',
|
||||
success: 'Completed successfully',
|
||||
error: 'Web Worker failed',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should not show duplicate notification for same task type', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id-1',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
eventHandler({ detail: { ...detail, id: 'test-id-2' } });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledTimes(1);
|
||||
expect(console.debug).toHaveBeenCalledWith(
|
||||
'Already tracking a "TEST_WORKER" task, skipping duplicate notification'
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve promise when progress is 100', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
|
||||
const startDetail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail: startDetail });
|
||||
|
||||
const completeDetail = {
|
||||
progress: 100,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail: completeDetail });
|
||||
|
||||
expect(console.debug).toHaveBeenCalledWith('Worker task "TEST_WORKER" completed successfully');
|
||||
});
|
||||
|
||||
it('should handle completion for non-tracked task gracefully', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 100,
|
||||
type: 'UNKNOWN_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
|
||||
expect(console.debug).not.toHaveBeenCalledWith(
|
||||
'Worker task "UNKNOWN_WORKER" completed successfully'
|
||||
);
|
||||
});
|
||||
|
||||
it('should normalize task keys correctly', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'Test Worker Type',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'worker-task-test-worker-type',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle error when setting active worker task', () => {
|
||||
jest.spyOn(global, 'Promise').mockImplementationOnce(() => {
|
||||
throw new Error('Promise creation failed');
|
||||
});
|
||||
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
expect(() => eventHandler({ detail })).not.toThrow();
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error in web worker progress handler for type "TEST_WORKER":',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should handle error when showing notification', () => {
|
||||
mockUINotificationService.show.mockImplementationOnce(() => {
|
||||
throw new Error('Notification service failed');
|
||||
});
|
||||
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
expect(() => eventHandler({ detail })).not.toThrow();
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error showing web worker notification for type "TEST_WORKER":',
|
||||
expect.any(Error)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle missing detail object', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
|
||||
expect(() => eventHandler({})).not.toThrow();
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error in web worker progress handler for type "undefined":',
|
||||
expect.any(Error)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle undefined detail', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
|
||||
expect(() => eventHandler({ detail: undefined })).not.toThrow();
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error in web worker progress handler for type "undefined":',
|
||||
expect.any(Error)
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle cleanup error gracefully', () => {
|
||||
const mockMap = new Map();
|
||||
mockMap.delete = jest.fn(() => {
|
||||
throw new Error('Delete failed');
|
||||
});
|
||||
|
||||
jest.spyOn(global, 'Map').mockImplementationOnce(() => mockMap);
|
||||
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
mockUINotificationService.show.mockImplementationOnce(() => {
|
||||
throw new Error('Notification failed');
|
||||
});
|
||||
|
||||
expect(() => eventHandler({ detail })).not.toThrow();
|
||||
expect(console.error).toHaveBeenCalledWith(
|
||||
'Error cleaning up active worker task for type "TEST_WORKER":',
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should handle intermediate progress values', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
|
||||
const startDetail = {
|
||||
progress: 0,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail: startDetail });
|
||||
|
||||
const intermediateDetail = {
|
||||
progress: 50,
|
||||
type: 'TEST_WORKER',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail: intermediateDetail });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle multiple different worker types simultaneously', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
|
||||
const detail1 = {
|
||||
progress: 0,
|
||||
type: 'WORKER_TYPE_1',
|
||||
id: 'test-id-1',
|
||||
};
|
||||
|
||||
const detail2 = {
|
||||
progress: 0,
|
||||
type: 'WORKER_TYPE_2',
|
||||
id: 'test-id-2',
|
||||
};
|
||||
|
||||
eventHandler({ detail: detail1 });
|
||||
eventHandler({ detail: detail2 });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledTimes(2);
|
||||
expect(mockUINotificationService.show).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({
|
||||
id: 'worker-task-worker_type_1',
|
||||
title: 'WORKER_TYPE_1',
|
||||
})
|
||||
);
|
||||
expect(mockUINotificationService.show).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({
|
||||
id: 'worker-task-worker_type_2',
|
||||
title: 'WORKER_TYPE_2',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should complete multiple worker types independently', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
|
||||
eventHandler({ detail: { progress: 0, type: 'WORKER_1', id: 'id-1' } });
|
||||
eventHandler({ detail: { progress: 0, type: 'WORKER_2', id: 'id-2' } });
|
||||
eventHandler({ detail: { progress: 100, type: 'WORKER_1', id: 'id-1' } });
|
||||
|
||||
expect(console.debug).toHaveBeenCalledWith('Worker task "WORKER_1" completed successfully');
|
||||
|
||||
eventHandler({ detail: { progress: 100, type: 'WORKER_2', id: 'id-2' } });
|
||||
|
||||
expect(console.debug).toHaveBeenCalledWith('Worker task "WORKER_2" completed successfully');
|
||||
});
|
||||
|
||||
it('should handle special characters in worker type names', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: 'Test-Worker_Type@123',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'worker-task-test-worker_type@123',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty worker type', () => {
|
||||
initializeWebWorkerProgressHandler(mockUINotificationService);
|
||||
|
||||
const eventHandler = (eventTarget.addEventListener as jest.Mock).mock.calls[0][1];
|
||||
const detail = {
|
||||
progress: 0,
|
||||
type: '',
|
||||
id: 'test-id',
|
||||
};
|
||||
|
||||
eventHandler({ detail });
|
||||
|
||||
expect(mockUINotificationService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'worker-task-',
|
||||
title: '',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
304
extensions/cornerstone/src/utils/interleave.test.ts
Normal file
304
extensions/cornerstone/src/utils/interleave.test.ts
Normal file
@ -0,0 +1,304 @@
|
||||
import interleave from './interleave';
|
||||
|
||||
describe('interleave', () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(console, 'time').mockImplementation();
|
||||
jest.spyOn(console, 'timeEnd').mockImplementation();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return empty array when input is null', () => {
|
||||
const result = interleave(null);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when input is undefined', () => {
|
||||
const result = interleave(undefined);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return empty array when input is empty array', () => {
|
||||
const result = interleave([]);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return same array when only one list is provided', () => {
|
||||
const input = [[1, 2, 3, 4]];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
expect(result).toBe(input[0]);
|
||||
});
|
||||
|
||||
it('should interleave two arrays of equal length', () => {
|
||||
const input = [
|
||||
[1, 3, 5],
|
||||
[2, 4, 6],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
it('should interleave three arrays of equal length', () => {
|
||||
const input = [
|
||||
[1, 4, 7],
|
||||
[2, 5, 8],
|
||||
[3, 6, 9],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
|
||||
it('should handle arrays of different lengths', () => {
|
||||
const input = [
|
||||
[1, 4],
|
||||
[2, 5, 7, 8],
|
||||
[3, 6],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
});
|
||||
|
||||
it('should handle first array being longer', () => {
|
||||
const input = [
|
||||
[1, 3, 5, 7, 9, 11],
|
||||
[2, 4, 6],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 9, 11]);
|
||||
});
|
||||
|
||||
it('should handle second array being longer', () => {
|
||||
const input = [
|
||||
[1, 3, 5],
|
||||
[2, 4, 6, 8, 10, 12],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 8, 10, 12]);
|
||||
});
|
||||
|
||||
it('should handle empty arrays in the input', () => {
|
||||
const input = [[1, 3, 5], [], [2, 4, 6]];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
|
||||
it('should handle all empty arrays', () => {
|
||||
const input = [[], [], []];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should handle single element arrays', () => {
|
||||
const input = [[1], [2], [3], [4]];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should handle mixed single and multi-element arrays', () => {
|
||||
const input = [[1], [2, 5, 8], [3], [4, 6, 7, 9]];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 8, 7, 9]);
|
||||
});
|
||||
|
||||
it('should handle string arrays', () => {
|
||||
const input = [
|
||||
['a', 'c', 'e'],
|
||||
['b', 'd', 'f'],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual(['a', 'b', 'c', 'd', 'e', 'f']);
|
||||
});
|
||||
|
||||
it('should handle object arrays', () => {
|
||||
const obj1 = { id: 1 };
|
||||
const obj2 = { id: 2 };
|
||||
const obj3 = { id: 3 };
|
||||
const obj4 = { id: 4 };
|
||||
|
||||
const input = [
|
||||
[obj1, obj3],
|
||||
[obj2, obj4],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([obj1, obj2, obj3, obj4]);
|
||||
});
|
||||
|
||||
it('should handle large arrays efficiently', () => {
|
||||
const array1 = Array.from({ length: 1000 }, (_, i) => i * 2);
|
||||
const array2 = Array.from({ length: 1000 }, (_, i) => i * 2 + 1);
|
||||
const input = [array1, array2];
|
||||
|
||||
const startTime = performance.now();
|
||||
const result = interleave(input);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(result).toHaveLength(2000);
|
||||
expect(result[0]).toBe(0);
|
||||
expect(result[1]).toBe(1);
|
||||
expect(result[2]).toBe(2);
|
||||
expect(result[3]).toBe(3);
|
||||
expect(endTime - startTime).toBeLessThan(100);
|
||||
});
|
||||
|
||||
it('should handle very large number of arrays', () => {
|
||||
const input = Array.from({ length: 100 }, (_, i) => [i, i + 100, i + 200]);
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toHaveLength(300);
|
||||
expect(result.slice(0, 100)).toEqual(Array.from({ length: 100 }, (_, i) => i));
|
||||
});
|
||||
|
||||
it('should handle arrays with different data types', () => {
|
||||
const input = [
|
||||
[1, 'a', true],
|
||||
[2, 'b', false],
|
||||
[3, 'c', null],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 2, 3, 'a', 'b', 'c', true, false, null]);
|
||||
});
|
||||
|
||||
it('should not mutate original arrays', () => {
|
||||
const array1 = [1, 3, 5];
|
||||
const array2 = [2, 4, 6];
|
||||
const input = [array1, array2];
|
||||
const originalArray1 = [...array1];
|
||||
const originalArray2 = [...array2];
|
||||
|
||||
interleave(input);
|
||||
|
||||
expect(array1).toEqual(originalArray1);
|
||||
expect(array2).toEqual(originalArray2);
|
||||
});
|
||||
|
||||
it('should handle nested arrays', () => {
|
||||
const input = [
|
||||
[
|
||||
[1, 2],
|
||||
[5, 6],
|
||||
],
|
||||
[
|
||||
[3, 4],
|
||||
[7, 8],
|
||||
],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
[5, 6],
|
||||
[7, 8],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle arrays with undefined and null values', () => {
|
||||
const input = [
|
||||
[1, undefined, 3],
|
||||
[null, 2, 4],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, null, undefined, 2, 3, 4]);
|
||||
});
|
||||
|
||||
it('should handle extremely unbalanced arrays', () => {
|
||||
const input = [[1], Array.from({ length: 1000 }, (_, i) => i + 2)];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toHaveLength(1001);
|
||||
expect(result[0]).toBe(1);
|
||||
expect(result[1]).toBe(2);
|
||||
expect(result[2]).toBe(3);
|
||||
});
|
||||
|
||||
it('should handle performance with many small arrays', () => {
|
||||
const input = Array.from({ length: 1000 }, (_, i) => [i]);
|
||||
|
||||
const startTime = performance.now();
|
||||
const result = interleave(input);
|
||||
const endTime = performance.now();
|
||||
|
||||
expect(result).toHaveLength(1000);
|
||||
expect(result).toEqual(Array.from({ length: 1000 }, (_, i) => i));
|
||||
expect(endTime - startTime).toBeLessThan(50);
|
||||
});
|
||||
|
||||
it('should handle zero values correctly', () => {
|
||||
const input = [
|
||||
[0, 2, 4],
|
||||
[1, 3, 5],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([0, 1, 2, 3, 4, 5]);
|
||||
});
|
||||
|
||||
it('should handle boolean arrays', () => {
|
||||
const input = [
|
||||
[true, false, true],
|
||||
[false, true, false],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([true, false, false, true, true, false]);
|
||||
});
|
||||
|
||||
it('should handle single empty array with non-empty arrays', () => {
|
||||
const input = [[], [1, 2, 3], [4, 5, 6]];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([1, 4, 2, 5, 3, 6]);
|
||||
});
|
||||
|
||||
it('should maintain order within each array', () => {
|
||||
const input = [
|
||||
[10, 20, 30, 40, 50],
|
||||
[15, 25],
|
||||
[12, 22, 32],
|
||||
];
|
||||
|
||||
const result = interleave(input);
|
||||
|
||||
expect(result).toEqual([10, 15, 12, 20, 25, 22, 30, 32, 40, 50]);
|
||||
});
|
||||
});
|
||||
@ -3,28 +3,30 @@
|
||||
* in the returned list, the second items are next etc.
|
||||
* Does this in a O(n) fashion, and return lists[0] if there is only one list.
|
||||
*
|
||||
* @param {[]} lists
|
||||
* @returns [] reordered to be breadth first traversal of lists
|
||||
* @param lists - Array of arrays to interleave
|
||||
* @returns Array reordered to be breadth first traversal of lists
|
||||
*/
|
||||
export default function interleave(lists) {
|
||||
export default function interleave<T>(lists: T[][]): T[] {
|
||||
if (!lists || !lists.length) {
|
||||
return [];
|
||||
}
|
||||
if (lists.length === 1) {
|
||||
return lists[0];
|
||||
}
|
||||
console.time('interleave');
|
||||
|
||||
const useLists = [...lists];
|
||||
const ret = [];
|
||||
for (let i = 0; useLists.length > 0; i++) {
|
||||
for (const list of useLists) {
|
||||
for (let j = 0; j < useLists.length; j++) {
|
||||
const list = useLists[j];
|
||||
if (i >= list.length) {
|
||||
useLists.splice(useLists.indexOf(list), 1);
|
||||
useLists.splice(j, 1);
|
||||
j--; // Adjust index after removal to avoid iterator skipping
|
||||
continue;
|
||||
}
|
||||
ret.push(list[i]);
|
||||
}
|
||||
}
|
||||
console.timeEnd('interleave');
|
||||
|
||||
return ret;
|
||||
}
|
||||
547
extensions/cornerstone/src/utils/interleaveCenterLoader.test.ts
Normal file
547
extensions/cornerstone/src/utils/interleaveCenterLoader.test.ts
Normal file
@ -0,0 +1,547 @@
|
||||
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
|
||||
import getInterleavedFrames from './getInterleavedFrames';
|
||||
import zip from 'lodash.zip';
|
||||
import compact from 'lodash.compact';
|
||||
import flatten from 'lodash.flatten';
|
||||
import interleaveCenterLoader from './interleaveCenterLoader';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
cache: {
|
||||
getVolume: jest.fn(),
|
||||
},
|
||||
imageLoadPoolManager: {
|
||||
addRequest: jest.fn(),
|
||||
},
|
||||
Enums: {
|
||||
RequestType: {
|
||||
Prefetch: 'Prefetch',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./getInterleavedFrames', () => jest.fn());
|
||||
jest.mock('lodash.zip', () => jest.fn());
|
||||
jest.mock('lodash.compact', () => jest.fn());
|
||||
jest.mock('lodash.flatten', () => jest.fn());
|
||||
|
||||
describe('interleaveCenterLoader', () => {
|
||||
const mockVolumeInput = {
|
||||
volumeId: 'test-volume-id',
|
||||
};
|
||||
|
||||
const mockVolume = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid',
|
||||
},
|
||||
getImageLoadRequests: jest.fn(),
|
||||
};
|
||||
|
||||
const mockImageLoadRequest = {
|
||||
imageId: 'test-image-id',
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
imageIdIndex: 0,
|
||||
options: { test: 'option' },
|
||||
};
|
||||
|
||||
const mockDisplaySetsInfo = [
|
||||
{
|
||||
displaySetInstanceUID: 'test-volume-id',
|
||||
displaySetOptions: {},
|
||||
},
|
||||
];
|
||||
|
||||
const mockMatchDetails = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: mockDisplaySetsInfo,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const defaultParameters = {
|
||||
data: {
|
||||
viewportId: 'test-viewport-id',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
displaySetsMatchDetails: {},
|
||||
viewportMatchDetails: mockMatchDetails,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(mockVolume);
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest]);
|
||||
(getInterleavedFrames as jest.Mock).mockReturnValue([{ imageId: 'test-image-id' }]);
|
||||
(zip as jest.Mock).mockImplementation((...args) => args[0]);
|
||||
(compact as jest.Mock).mockImplementation(input => input?.filter(Boolean) ?? []);
|
||||
(flatten as jest.Mock).mockImplementation(input => input?.flat() ?? []);
|
||||
});
|
||||
|
||||
it('should store viewport and volume input array mapping on first call', () => {
|
||||
interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
});
|
||||
|
||||
it('should return early when volume is not found', () => {
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockVolume.getImageLoadRequests).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return early when volume input array size does not match display set UIDs', () => {
|
||||
const mockMatchDetailsWithDifferentSize = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{ displaySetInstanceUID: 'display-set-1' },
|
||||
{ displaySetInstanceUID: 'display-set-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMatchDetailsWithDifferentSize,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should process volume and create interleaved requests', () => {
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
expect(mockVolume.getImageLoadRequests).toHaveBeenCalled();
|
||||
expect(getInterleavedFrames).toHaveBeenCalledWith(['test-image-id']);
|
||||
expect(zip).toHaveBeenCalled();
|
||||
expect(compact).toHaveBeenCalled();
|
||||
expect(flatten).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should add requests to imageLoadPoolManager with correct parameters', () => {
|
||||
interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
Enums.RequestType.Prefetch,
|
||||
mockImageLoadRequest.additionalDetails,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should bind callLoadImage with correct parameters', () => {
|
||||
interleaveCenterLoader(defaultParameters);
|
||||
|
||||
const addRequestCall = (imageLoadPoolManager.addRequest as jest.Mock).mock.calls[0];
|
||||
const boundFunction = addRequestCall[0];
|
||||
|
||||
boundFunction();
|
||||
|
||||
expect(mockImageLoadRequest.callLoadImage).toHaveBeenCalledWith(
|
||||
mockImageLoadRequest.imageId,
|
||||
mockImageLoadRequest.imageIdIndex,
|
||||
mockImageLoadRequest.options
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle volumes without image load requests', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(zip).toHaveBeenCalledWith();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle volumes with empty requests array', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(getInterleavedFrames).not.toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle volumes with null first request', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([null]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(getInterleavedFrames).not.toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle volumes with requests without imageId', () => {
|
||||
const requestWithoutImageId = {
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([requestWithoutImageId]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(getInterleavedFrames).not.toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle multiple volume inputs', () => {
|
||||
const mockVolumeInput2 = {
|
||||
volumeId: 'test-volume-id-2',
|
||||
};
|
||||
|
||||
const mockVolume2 = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid-2',
|
||||
},
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2);
|
||||
|
||||
const mockMatchDetailsWithMultipleVolumes = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{ displaySetInstanceUID: 'test-volume-id' },
|
||||
{ displaySetInstanceUID: 'test-volume-id-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
|
||||
},
|
||||
viewportMatchDetails: mockMatchDetailsWithMultipleVolumes,
|
||||
});
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput2.volumeId);
|
||||
});
|
||||
|
||||
it('should handle duplicate volume IDs by not adding them again', () => {
|
||||
const duplicateVolumeInput = {
|
||||
volumeId: mockVolumeInput.volumeId,
|
||||
};
|
||||
|
||||
interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, duplicateVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should clear internal maps after processing', () => {
|
||||
const firstResult = interleaveCenterLoader(defaultParameters);
|
||||
const secondResult = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(firstResult).toBeInstanceOf(Map);
|
||||
expect(secondResult).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle multiple image load requests', () => {
|
||||
const mockImageLoadRequest2 = {
|
||||
imageId: 'test-image-id-2',
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details-2',
|
||||
imageIdIndex: 1,
|
||||
options: { test: 'option2' },
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
|
||||
|
||||
(getInterleavedFrames as jest.Mock).mockReturnValue([
|
||||
{ imageId: 'test-image-id' },
|
||||
{ imageId: 'test-image-id-2' },
|
||||
]);
|
||||
|
||||
(compact as jest.Mock).mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
|
||||
(flatten as jest.Mock).mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
|
||||
|
||||
interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(getInterleavedFrames).toHaveBeenCalledWith(['test-image-id', 'test-image-id-2']);
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should handle getInterleavedFrames returning empty array', () => {
|
||||
(getInterleavedFrames as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).not.toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle zip returning empty result', () => {
|
||||
(zip as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(compact).toHaveBeenCalledWith([]);
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle compact filtering out null values', () => {
|
||||
(compact as jest.Mock).mockReturnValue([]);
|
||||
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).not.toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should map imageIds to requests correctly', () => {
|
||||
const mockImageLoadRequest2 = {
|
||||
imageId: 'test-image-id-2',
|
||||
callLoadImage: jest.fn(),
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
|
||||
|
||||
(getInterleavedFrames as jest.Mock).mockReturnValue([
|
||||
{ imageId: 'test-image-id-2' },
|
||||
{ imageId: 'test-image-id' },
|
||||
]);
|
||||
|
||||
interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(getInterleavedFrames).toHaveBeenCalledWith(['test-image-id', 'test-image-id-2']);
|
||||
});
|
||||
|
||||
it('should return copy of viewport volume input array map', () => {
|
||||
const result = interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(result.has(defaultParameters.data.viewportId)).toBe(true);
|
||||
expect(result.get(defaultParameters.data.viewportId)).toEqual(
|
||||
defaultParameters.data.volumeInputArray
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple match details with different displaySets', () => {
|
||||
const mockMultipleMatchDetails = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [{ displaySetInstanceUID: 'display-set-1' }],
|
||||
},
|
||||
],
|
||||
[
|
||||
'viewport-2',
|
||||
{
|
||||
displaySetsInfo: [{ displaySetInstanceUID: 'display-set-2' }],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMultipleMatchDetails,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty match details', () => {
|
||||
const emptyMatchDetails = new Map();
|
||||
|
||||
const result = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: emptyMatchDetails,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty volumeInputArray', () => {
|
||||
const result = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle requests with missing properties gracefully', () => {
|
||||
const incompleteRequest = {
|
||||
imageId: 'test-image-id',
|
||||
callLoadImage: jest.fn(),
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([incompleteRequest]);
|
||||
(getInterleavedFrames as jest.Mock).mockReturnValue([{ imageId: 'test-image-id' }]);
|
||||
(compact as jest.Mock).mockReturnValue([incompleteRequest]);
|
||||
(flatten as jest.Mock).mockReturnValue([incompleteRequest]);
|
||||
|
||||
interleaveCenterLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
Enums.RequestType.Prefetch,
|
||||
undefined,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should process different viewport IDs separately', () => {
|
||||
const firstResult = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-1',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
const secondResult = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-2',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstResult.has('viewport-1')).toBe(true);
|
||||
expect(secondResult.has('viewport-2')).toBe(true);
|
||||
});
|
||||
|
||||
it('should accumulate state across multiple calls before processing', () => {
|
||||
const mockVolumeInput2 = {
|
||||
volumeId: 'test-volume-id-2',
|
||||
};
|
||||
|
||||
const mockVolume2 = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid-2',
|
||||
},
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2);
|
||||
|
||||
const firstCall = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-1',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
viewportMatchDetails: new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{ displaySetInstanceUID: 'test-volume-id' },
|
||||
{ displaySetInstanceUID: 'missing-volume-id' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(firstCall).toBeUndefined();
|
||||
|
||||
const secondCall = interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-2',
|
||||
volumeInputArray: [mockVolumeInput2],
|
||||
},
|
||||
viewportMatchDetails: new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [{ displaySetInstanceUID: 'test-volume-id' }],
|
||||
},
|
||||
],
|
||||
[
|
||||
'viewport-2',
|
||||
{
|
||||
displaySetsInfo: [{ displaySetInstanceUID: 'test-volume-id-2' }],
|
||||
},
|
||||
],
|
||||
]),
|
||||
});
|
||||
|
||||
expect(secondCall).toBeInstanceOf(Map);
|
||||
expect(secondCall.has('viewport-1')).toBe(true);
|
||||
expect(secondCall.has('viewport-2')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle volume with undefined metadata', () => {
|
||||
const mockVolumeWithoutMetadata = {
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(mockVolumeWithoutMetadata);
|
||||
|
||||
expect(() => interleaveCenterLoader(defaultParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should handle multiple volumes with same image requests', () => {
|
||||
const mockVolume2 = {
|
||||
metadata: { SeriesInstanceUID: 'test-series-uid-2' },
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
|
||||
};
|
||||
|
||||
const mockVolumeInput2 = { volumeId: 'test-volume-id-2' };
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2);
|
||||
|
||||
const mockMatchDetailsWithBothVolumes = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{ displaySetInstanceUID: 'test-volume-id' },
|
||||
{ displaySetInstanceUID: 'test-volume-id-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
(compact as jest.Mock).mockReturnValue([mockImageLoadRequest]);
|
||||
(flatten as jest.Mock).mockReturnValue([mockImageLoadRequest]);
|
||||
|
||||
interleaveCenterLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
|
||||
},
|
||||
viewportMatchDetails: mockMatchDetailsWithBothVolumes,
|
||||
});
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
487
extensions/cornerstone/src/utils/interleaveTopToBottom.test.ts
Normal file
487
extensions/cornerstone/src/utils/interleaveTopToBottom.test.ts
Normal file
@ -0,0 +1,487 @@
|
||||
import { cache, imageLoadPoolManager, Enums } from '@cornerstonejs/core';
|
||||
import zip from 'lodash.zip';
|
||||
import compact from 'lodash.compact';
|
||||
import flatten from 'lodash.flatten';
|
||||
import interleaveTopToBottom from './interleaveTopToBottom';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
cache: {
|
||||
getVolume: jest.fn(),
|
||||
},
|
||||
imageLoadPoolManager: {
|
||||
addRequest: jest.fn(),
|
||||
},
|
||||
Enums: {
|
||||
RequestType: {
|
||||
Prefetch: 'Prefetch',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('lodash.zip', () => jest.fn());
|
||||
jest.mock('lodash.compact', () => jest.fn());
|
||||
jest.mock('lodash.flatten', () => jest.fn());
|
||||
|
||||
describe('interleaveTopToBottom', () => {
|
||||
const mockVolumeInput = {
|
||||
volumeId: 'test-volume-id',
|
||||
};
|
||||
|
||||
const mockVolume = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid',
|
||||
},
|
||||
getImageLoadRequests: jest.fn(),
|
||||
};
|
||||
|
||||
const mockImageLoadRequest = {
|
||||
imageId: 'test-image-id',
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
imageIdIndex: 0,
|
||||
options: { test: 'option' },
|
||||
};
|
||||
|
||||
const mockDisplaySetsInfo = [
|
||||
{
|
||||
displaySetInstanceUID: 'display-set-1',
|
||||
displaySetOptions: {},
|
||||
},
|
||||
];
|
||||
|
||||
const mockMatchDetails = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: mockDisplaySetsInfo,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const defaultParameters = {
|
||||
data: {
|
||||
viewportId: 'test-viewport-id',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
displaySetsMatchDetails: {},
|
||||
viewportMatchDetails: mockMatchDetails,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(mockVolume);
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest]);
|
||||
(zip as jest.Mock).mockImplementation((...args) => args[0]);
|
||||
(compact as jest.Mock).mockImplementation(input => input?.filter(Boolean) ?? []);
|
||||
(flatten as jest.Mock).mockImplementation(input => input?.flat() ?? []);
|
||||
});
|
||||
|
||||
it('should store viewport and volume input array mapping', () => {
|
||||
interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
});
|
||||
|
||||
it('should return early when volume is not found', () => {
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(null);
|
||||
|
||||
const result = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockVolume.getImageLoadRequests).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should process volume input array and create image load requests', () => {
|
||||
const result = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
expect(mockVolume.getImageLoadRequests).toHaveBeenCalled();
|
||||
expect(zip).toHaveBeenCalled();
|
||||
expect(compact).toHaveBeenCalled();
|
||||
expect(flatten).toHaveBeenCalled();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should process displaySets without skipLoading option', () => {
|
||||
const mockMatchDetailsWithoutSkipLoading = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{
|
||||
displaySetInstanceUID: 'display-set-1',
|
||||
displaySetOptions: { options: { skipLoading: false } },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMatchDetailsWithoutSkipLoading,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should return early when viewport volume display set UIDs size does not match', () => {
|
||||
const mockMatchDetailsWithDifferentSize = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{
|
||||
displaySetInstanceUID: 'display-set-1',
|
||||
},
|
||||
{
|
||||
displaySetInstanceUID: 'display-set-2',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMatchDetailsWithDifferentSize,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple volume inputs', () => {
|
||||
const mockVolumeInput2 = {
|
||||
volumeId: 'test-volume-id-2',
|
||||
};
|
||||
|
||||
const mockVolume2 = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid-2',
|
||||
},
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2);
|
||||
|
||||
const mockMatchDetailsWithMultipleVolumes = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{ displaySetInstanceUID: 'test-volume-id' },
|
||||
{ displaySetInstanceUID: 'test-volume-id-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
|
||||
},
|
||||
viewportMatchDetails: mockMatchDetailsWithMultipleVolumes,
|
||||
});
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput2.volumeId);
|
||||
});
|
||||
|
||||
it('should add requests to imageLoadPoolManager with correct parameters', () => {
|
||||
interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
Enums.RequestType.Prefetch,
|
||||
mockImageLoadRequest.additionalDetails,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should bind callLoadImage with correct parameters', () => {
|
||||
interleaveTopToBottom(defaultParameters);
|
||||
|
||||
const addRequestCall = (imageLoadPoolManager.addRequest as jest.Mock).mock.calls[0];
|
||||
const boundFunction = addRequestCall[0];
|
||||
|
||||
boundFunction();
|
||||
|
||||
expect(mockImageLoadRequest.callLoadImage).toHaveBeenCalledWith(
|
||||
mockImageLoadRequest.imageId,
|
||||
mockImageLoadRequest.imageIdIndex,
|
||||
mockImageLoadRequest.options
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle volumes without image load requests', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue(null);
|
||||
|
||||
const result = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(zip).toHaveBeenCalledWith();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle volumes with empty image load requests', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([]);
|
||||
|
||||
const result = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(zip).toHaveBeenCalledWith();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle volumes with requests without imageId', () => {
|
||||
const requestWithoutImageId = {
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([requestWithoutImageId]);
|
||||
|
||||
const result = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(zip).toHaveBeenCalledWith();
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should reverse the requests before adding to AllRequests', () => {
|
||||
const mockRequests = [
|
||||
{ imageId: 'image-1', callLoadImage: jest.fn() },
|
||||
{ imageId: 'image-2', callLoadImage: jest.fn() },
|
||||
];
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue(mockRequests);
|
||||
|
||||
interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(mockVolume.getImageLoadRequests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle duplicate volume IDs by not adding them again', () => {
|
||||
const duplicateVolumeInput = {
|
||||
volumeId: mockVolumeInput.volumeId,
|
||||
};
|
||||
|
||||
interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, duplicateVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should clear internal maps after processing', () => {
|
||||
const firstResult = interleaveTopToBottom(defaultParameters);
|
||||
const secondResult = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(firstResult).toBeInstanceOf(Map);
|
||||
expect(secondResult).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle multiple match details with different displaySets', () => {
|
||||
const mockMultipleMatchDetails = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [{ displaySetInstanceUID: 'display-set-1' }],
|
||||
},
|
||||
],
|
||||
[
|
||||
'viewport-2',
|
||||
{
|
||||
displaySetsInfo: [{ displaySetInstanceUID: 'display-set-2' }],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMultipleMatchDetails,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle undefined displaySetOptions', () => {
|
||||
const mockMatchDetailsWithUndefinedOptions = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{
|
||||
displaySetInstanceUID: 'display-set-1',
|
||||
displaySetOptions: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMatchDetailsWithUndefinedOptions,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle displaySetOptions without options property', () => {
|
||||
const mockMatchDetailsWithoutOptionsProperty = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{
|
||||
displaySetInstanceUID: 'display-set-1',
|
||||
displaySetOptions: { someOtherProperty: 'value' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: mockMatchDetailsWithoutOptionsProperty,
|
||||
});
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should handle empty match details', () => {
|
||||
const emptyMatchDetails = new Map();
|
||||
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
viewportMatchDetails: emptyMatchDetails,
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle empty volumeInputArray', () => {
|
||||
const result = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle interleavedRequests processing correctly', () => {
|
||||
const mockRequest1 = { imageId: 'image-1', callLoadImage: jest.fn() };
|
||||
const mockRequest2 = { imageId: 'image-2', callLoadImage: jest.fn() };
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockRequest1, mockRequest2]);
|
||||
(compact as jest.Mock).mockReturnValue([mockRequest1, mockRequest2]);
|
||||
(flatten as jest.Mock).mockReturnValue([mockRequest1, mockRequest2]);
|
||||
|
||||
interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should return copy of viewport volume input array map', () => {
|
||||
const result = interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(result.has(defaultParameters.data.viewportId)).toBe(true);
|
||||
expect(result.get(defaultParameters.data.viewportId)).toEqual(
|
||||
defaultParameters.data.volumeInputArray
|
||||
);
|
||||
});
|
||||
|
||||
it('should process different viewport IDs separately', () => {
|
||||
const firstResult = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-1',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
const secondResult = interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-2',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstResult.has('viewport-1')).toBe(true);
|
||||
expect(secondResult.has('viewport-2')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle requests with missing properties gracefully', () => {
|
||||
const incompleteRequest = {
|
||||
imageId: 'test-image-id',
|
||||
callLoadImage: jest.fn(),
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([incompleteRequest]);
|
||||
(compact as jest.Mock).mockReturnValue([incompleteRequest]);
|
||||
(flatten as jest.Mock).mockReturnValue([incompleteRequest]);
|
||||
|
||||
interleaveTopToBottom(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
Enums.RequestType.Prefetch,
|
||||
undefined,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple volumes with same requests', () => {
|
||||
const mockVolume2 = {
|
||||
metadata: { SeriesInstanceUID: 'test-series-uid-2' },
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
|
||||
};
|
||||
|
||||
const mockVolumeInput2 = { volumeId: 'test-volume-id-2' };
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2);
|
||||
|
||||
const mockMatchDetailsWithBothVolumes = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
displaySetsInfo: [
|
||||
{ displaySetInstanceUID: 'test-volume-id' },
|
||||
{ displaySetInstanceUID: 'test-volume-id-2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
(compact as jest.Mock).mockReturnValue([mockImageLoadRequest]);
|
||||
(flatten as jest.Mock).mockReturnValue([mockImageLoadRequest]);
|
||||
|
||||
interleaveTopToBottom({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
|
||||
},
|
||||
viewportMatchDetails: mockMatchDetailsWithBothVolumes,
|
||||
});
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
@ -39,31 +39,6 @@ export default function interleaveTopToBottom({
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMatchDetails = [];
|
||||
const displaySetsToLoad = new Set();
|
||||
|
||||
// Check all viewports that have a displaySet to be loaded. In some cases
|
||||
// (eg: line chart viewports which is not a Cornerstone viewport) the
|
||||
// displaySet is created on the client and there are no instances to be
|
||||
// downloaded. For those viewports the displaySet may have the `skipLoading`
|
||||
// option set to true otherwise it may block the download of all other
|
||||
// instances resulting in blank viewports.
|
||||
Array.from(matchDetails.values()).forEach(curMatchDetails => {
|
||||
const { displaySetsInfo } = curMatchDetails;
|
||||
let numDisplaySetsToLoad = 0;
|
||||
|
||||
displaySetsInfo.forEach(({ displaySetInstanceUID, displaySetOptions }) => {
|
||||
if (!displaySetOptions?.options?.skipLoading) {
|
||||
numDisplaySetsToLoad++;
|
||||
displaySetsToLoad.add(displaySetInstanceUID);
|
||||
}
|
||||
});
|
||||
|
||||
if (numDisplaySetsToLoad) {
|
||||
filteredMatchDetails.push(curMatchDetails);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The following is checking if all the viewports that were matched in the HP has been
|
||||
* successfully created their cornerstone viewport or not. Todo: This can be
|
||||
|
||||
@ -0,0 +1,340 @@
|
||||
import { isMeasurementWithinViewport } from './isMeasurementWithinViewport';
|
||||
import { getCenterExtent } from './getCenterExtent';
|
||||
|
||||
jest.mock('./getCenterExtent', () => ({
|
||||
getCenterExtent: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('isMeasurementWithinViewport', () => {
|
||||
const mockViewport = {
|
||||
getCamera: jest.fn(),
|
||||
};
|
||||
|
||||
const mockMeasurement = {
|
||||
points: [
|
||||
[0, 0, 0],
|
||||
[10, 10, 10],
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return true when measurement is within viewport extent', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 15,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [10, 10, 10],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(mockViewport.getCamera).toHaveBeenCalled();
|
||||
expect(getCenterExtent).toHaveBeenCalledWith(mockMeasurement);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when measurement min point is outside viewport extent', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 4,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [10, 10, 10],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when measurement max point is outside viewport extent', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 4,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [1, 1, 1],
|
||||
max: [15, 15, 15],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when min point exceeds parallelScale in first dimension', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [10, 5, 5],
|
||||
parallelScale: 5,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [8, 8, 8],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when max point exceeds parallelScale in second dimension', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 5,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [3, 3, 3],
|
||||
max: [7, 12, 7],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false when max point exceeds parallelScale in third dimension', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 5,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [3, 3, 3],
|
||||
max: [7, 7, 12],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when measurement extent exactly matches parallelScale', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 5,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [10, 10, 10],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle negative coordinates correctly', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [-5, -5, -5],
|
||||
parallelScale: 10,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [-10, -10, -10],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle zero parallelScale', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [0, 0, 0],
|
||||
parallelScale: 0,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [0, 0, 0],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle zero parallelScale with non-zero extent', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [0, 0, 0],
|
||||
parallelScale: 0,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0, 0, 0],
|
||||
max: [1, 1, 1],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle large parallelScale values', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [0, 0, 0],
|
||||
parallelScale: 1000000,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [-500, -500, -500],
|
||||
max: [500, 500, 500],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle floating point coordinates', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [1.5, 2.7, 3.1],
|
||||
parallelScale: 5.5,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [0.1, 0.2, 0.3],
|
||||
max: [2.9, 5.2, 5.9],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should fail when min point distance equals parallelScale plus epsilon', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 5,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [-0.1, 0, 0],
|
||||
max: [10, 10, 10],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should pass when extent is within parallelScale boundary', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [5, 5, 5],
|
||||
parallelScale: 6,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [-1, -1, -1],
|
||||
max: [11, 11, 11],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle asymmetric extent around focal point', () => {
|
||||
const mockCamera = {
|
||||
focalPoint: [10, 10, 10],
|
||||
parallelScale: 8,
|
||||
};
|
||||
|
||||
const mockExtent = {
|
||||
extent: {
|
||||
min: [5, 5, 5],
|
||||
max: [18, 18, 18],
|
||||
},
|
||||
};
|
||||
|
||||
mockViewport.getCamera.mockReturnValue(mockCamera);
|
||||
(getCenterExtent as jest.Mock).mockReturnValue(mockExtent);
|
||||
|
||||
const result = isMeasurementWithinViewport(mockViewport, mockMeasurement);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
});
|
||||
581
extensions/cornerstone/src/utils/isReferenceViewable.test.ts
Normal file
581
extensions/cornerstone/src/utils/isReferenceViewable.test.ts
Normal file
@ -0,0 +1,581 @@
|
||||
import { vec3 } from 'gl-matrix';
|
||||
import { Enums } from '@cornerstonejs/core';
|
||||
import getClosestOrientationFromIOP, { isReferenceViewable } from './isReferenceViewable';
|
||||
|
||||
jest.mock('gl-matrix', () => ({
|
||||
vec3: {
|
||||
fromValues: jest.fn(),
|
||||
cross: jest.fn(),
|
||||
dot: jest.fn(),
|
||||
create: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
Enums: {
|
||||
OrientationAxis: {
|
||||
AXIAL: 'axial',
|
||||
CORONAL: 'coronal',
|
||||
SAGITTAL: 'sagittal',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('isReferenceViewable', () => {
|
||||
const mockCornerstoneViewportService = {
|
||||
getCornerstoneViewport: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDisplaySetService = {
|
||||
getDisplaySetByUID: jest.fn(),
|
||||
};
|
||||
|
||||
const mockServicesManager = {
|
||||
services: {
|
||||
cornerstoneViewportService: mockCornerstoneViewportService,
|
||||
displaySetService: mockDisplaySetService,
|
||||
},
|
||||
};
|
||||
|
||||
const mockViewport = {
|
||||
isReferenceViewable: jest.fn(),
|
||||
};
|
||||
|
||||
const mockReference = {
|
||||
displaySetInstanceUID: 'test-display-set-uid',
|
||||
referencedImageId: 'test-image-id',
|
||||
};
|
||||
|
||||
const defaultParameters = {
|
||||
servicesManager: mockServicesManager,
|
||||
viewportId: 'test-viewport-id',
|
||||
reference: mockReference,
|
||||
viewportOptions: undefined,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCornerstoneViewportService.getCornerstoneViewport.mockReturnValue(mockViewport);
|
||||
mockViewport.isReferenceViewable.mockReturnValue(true);
|
||||
});
|
||||
|
||||
it('should return viewport isReferenceViewable result when viewportOptions is undefined', () => {
|
||||
mockViewport.isReferenceViewable.mockReturnValue(true);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference
|
||||
);
|
||||
|
||||
expect(mockCornerstoneViewportService.getCornerstoneViewport).toHaveBeenCalledWith(
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
expect(mockViewport.isReferenceViewable).toHaveBeenCalledWith(mockReference, {
|
||||
withNavigation: true,
|
||||
asVolume: true,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when viewport isReferenceViewable returns false', () => {
|
||||
mockViewport.isReferenceViewable.mockReturnValue(false);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should check imageIds inclusion for stack viewport type', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [{ imageId: 'image-1' }, { imageId: 'test-image-id' }, { imageId: 'image-3' }],
|
||||
};
|
||||
|
||||
const viewportOptions = {
|
||||
viewportType: 'stack',
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference,
|
||||
viewportOptions
|
||||
);
|
||||
|
||||
expect(mockDisplaySetService.getDisplaySetByUID).toHaveBeenCalledWith(
|
||||
mockReference.displaySetInstanceUID
|
||||
);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when imageId is not in stack', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [{ imageId: 'image-1' }, { imageId: 'image-2' }, { imageId: 'image-3' }],
|
||||
};
|
||||
|
||||
const viewportOptions = {
|
||||
viewportType: 'stack',
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference,
|
||||
viewportOptions
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should check orientation for volume viewport type', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const viewportOptions = {
|
||||
viewportType: 'volume',
|
||||
orientation: 'axial',
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, 1, 0];
|
||||
const mockScanAxisNormal = [0, 0, 1];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock).mockReturnValue(1);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference,
|
||||
viewportOptions
|
||||
);
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when orientation does not match', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const viewportOptions = {
|
||||
viewportType: 'volume',
|
||||
orientation: 'coronal',
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, 1, 0];
|
||||
const mockScanAxisNormal = [0, 0, 1];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock).mockReturnValue(1);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference,
|
||||
viewportOptions
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle empty instances array for stack viewport', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [],
|
||||
};
|
||||
|
||||
const viewportOptions = {
|
||||
viewportType: 'stack',
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const result = isReferenceViewable(
|
||||
defaultParameters.servicesManager,
|
||||
defaultParameters.viewportId,
|
||||
defaultParameters.reference,
|
||||
viewportOptions
|
||||
);
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getClosestOrientationFromIOP', () => {
|
||||
const mockDisplaySetService = {
|
||||
getDisplaySetByUID: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return undefined when ImageOrientationPatient is not length 6', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 1],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when ImageOrientationPatient is undefined', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: undefined,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when ImageOrientationPatient is null', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return axial orientation for axial-aligned IOP', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, 1, 0];
|
||||
const mockScanAxisNormal = [0, 0, 1];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
const mockCoronalVector = [0, 1, 0];
|
||||
const mockSagittalVector = [1, 0, 0];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector)
|
||||
.mockReturnValueOnce(mockCoronalVector)
|
||||
.mockReturnValueOnce(mockSagittalVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(1.0)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.0);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBe(Enums.OrientationAxis.AXIAL);
|
||||
});
|
||||
|
||||
it('should return coronal orientation for coronal-aligned IOP', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 0, -1],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, 0, -1];
|
||||
const mockScanAxisNormal = [0, 1, 0];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
const mockCoronalVector = [0, 1, 0];
|
||||
const mockSagittalVector = [1, 0, 0];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector)
|
||||
.mockReturnValueOnce(mockCoronalVector)
|
||||
.mockReturnValueOnce(mockSagittalVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(1.0)
|
||||
.mockReturnValueOnce(0.0);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBe(Enums.OrientationAxis.CORONAL);
|
||||
});
|
||||
|
||||
it('should return sagittal orientation for sagittal-aligned IOP', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [0, 1, 0, 0, 0, -1],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [0, 1, 0];
|
||||
const mockColCosineVec = [0, 0, -1];
|
||||
const mockScanAxisNormal = [1, 0, 0];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
const mockCoronalVector = [0, 1, 0];
|
||||
const mockSagittalVector = [1, 0, 0];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector)
|
||||
.mockReturnValueOnce(mockCoronalVector)
|
||||
.mockReturnValueOnce(mockSagittalVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(1.0);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBe(Enums.OrientationAxis.SAGITTAL);
|
||||
});
|
||||
|
||||
it('should handle negative dot products and return correct orientation', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, -1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, -1, 0];
|
||||
const mockScanAxisNormal = [0, 0, -1];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
const mockCoronalVector = [0, 1, 0];
|
||||
const mockSagittalVector = [1, 0, 0];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector)
|
||||
.mockReturnValueOnce(mockCoronalVector)
|
||||
.mockReturnValueOnce(mockSagittalVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(-1.0)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.0);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBe(Enums.OrientationAxis.AXIAL);
|
||||
});
|
||||
|
||||
it('should handle oblique orientations and return closest match', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [0.7071, 0.7071, 0, 0, 0, 1],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [0.7071, 0.7071, 0];
|
||||
const mockColCosineVec = [0, 0, 1];
|
||||
const mockScanAxisNormal = [0.7071, -0.7071, 0];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
const mockCoronalVector = [0, 1, 0];
|
||||
const mockSagittalVector = [1, 0, 0];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector)
|
||||
.mockReturnValueOnce(mockCoronalVector)
|
||||
.mockReturnValueOnce(mockSagittalVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.7071)
|
||||
.mockReturnValueOnce(0.7071);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBe(Enums.OrientationAxis.CORONAL);
|
||||
});
|
||||
|
||||
it('should handle zero dot products', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, 1, 0];
|
||||
const mockScanAxisNormal = [0, 0, 1];
|
||||
const mockAxialVector = [0, 0, 1];
|
||||
const mockCoronalVector = [0, 1, 0];
|
||||
const mockSagittalVector = [1, 0, 0];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec)
|
||||
.mockReturnValueOnce(mockAxialVector)
|
||||
.mockReturnValueOnce(mockCoronalVector)
|
||||
.mockReturnValueOnce(mockSagittalVector);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.0);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('should call vec3 functions with correct parameters', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [1, 0, 0, 0, 1, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [1, 0, 0];
|
||||
const mockColCosineVec = [0, 1, 0];
|
||||
const mockCreatedVec = [];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue(mockCreatedVec);
|
||||
(vec3.cross as jest.Mock).mockReturnValue([0, 0, 1]);
|
||||
(vec3.dot as jest.Mock).mockReturnValue(1.0);
|
||||
|
||||
getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(vec3.fromValues).toHaveBeenNthCalledWith(1, 1, 0, 0);
|
||||
expect(vec3.fromValues).toHaveBeenNthCalledWith(2, 0, 1, 0);
|
||||
expect(vec3.cross).toHaveBeenCalledWith(mockCreatedVec, mockRowCosineVec, mockColCosineVec);
|
||||
});
|
||||
|
||||
it('should handle floating point IOP values', () => {
|
||||
const mockDisplaySet = {
|
||||
instances: [
|
||||
{
|
||||
ImageOrientationPatient: [0.866, 0.5, 0, -0.5, 0.866, 0],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
const mockRowCosineVec = [0.866, 0.5, 0];
|
||||
const mockColCosineVec = [-0.5, 0.866, 0];
|
||||
const mockScanAxisNormal = [0, 0, 1];
|
||||
|
||||
(vec3.fromValues as jest.Mock)
|
||||
.mockReturnValueOnce(mockRowCosineVec)
|
||||
.mockReturnValueOnce(mockColCosineVec);
|
||||
|
||||
(vec3.create as jest.Mock).mockReturnValue([]);
|
||||
(vec3.cross as jest.Mock).mockReturnValue(mockScanAxisNormal);
|
||||
(vec3.dot as jest.Mock)
|
||||
.mockReturnValueOnce(1.0)
|
||||
.mockReturnValueOnce(0.0)
|
||||
.mockReturnValueOnce(0.0);
|
||||
|
||||
const result = getClosestOrientationFromIOP(mockDisplaySetService, 'test-display-set-uid');
|
||||
|
||||
expect(vec3.fromValues).toHaveBeenNthCalledWith(1, 0.866, 0.5, 0);
|
||||
expect(vec3.fromValues).toHaveBeenNthCalledWith(2, -0.5, 0.866, 0);
|
||||
expect(result).toBe(Enums.OrientationAxis.AXIAL);
|
||||
});
|
||||
});
|
||||
317
extensions/cornerstone/src/utils/nthLoader.test.ts
Normal file
317
extensions/cornerstone/src/utils/nthLoader.test.ts
Normal file
@ -0,0 +1,317 @@
|
||||
import { cache, imageLoadPoolManager, Enums as csEnums } from '@cornerstonejs/core';
|
||||
import interleaveNthLoader from './nthLoader';
|
||||
import getNthFrames from './getNthFrames';
|
||||
import interleave from './interleave';
|
||||
|
||||
jest.mock('@cornerstonejs/core', () => ({
|
||||
...jest.requireActual('@cornerstonejs/core'),
|
||||
cache: {
|
||||
getVolume: jest.fn(),
|
||||
},
|
||||
imageLoadPoolManager: {
|
||||
addRequest: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./getNthFrames', () => jest.fn());
|
||||
jest.mock('./interleave', () => jest.fn());
|
||||
|
||||
describe('interleaveNthLoader', () => {
|
||||
const mockVolumeInput = {
|
||||
volumeId: 'test-volume-id',
|
||||
};
|
||||
|
||||
const mockVolume = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid',
|
||||
},
|
||||
getImageLoadRequests: jest.fn(),
|
||||
};
|
||||
|
||||
const mockImageLoadRequest = {
|
||||
imageId: 'test-image-id',
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
imageIdIndex: 0,
|
||||
options: { test: 'option' },
|
||||
};
|
||||
|
||||
const defaultParameters = {
|
||||
data: {
|
||||
viewportId: 'test-viewport-id',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
displaySetsMatchDetails: {},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(mockVolume);
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest]);
|
||||
(getNthFrames as jest.Mock).mockImplementation(input => input);
|
||||
(interleave as jest.Mock).mockImplementation(input => input.flat());
|
||||
});
|
||||
|
||||
it('should store viewport and volume input array mapping', () => {
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
});
|
||||
|
||||
it('should return early when volume is not found', () => {
|
||||
(cache.getVolume as jest.Mock).mockReturnValue(null);
|
||||
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
|
||||
const result = interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith("interleaveNthLoader::No volume, can't load it");
|
||||
expect(result).toBeUndefined();
|
||||
expect(mockVolume.getImageLoadRequests).not.toHaveBeenCalled();
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should process volume input array and create image load requests', () => {
|
||||
const result = interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
expect(mockVolume.getImageLoadRequests).toHaveBeenCalled();
|
||||
expect(getNthFrames).toHaveBeenCalledWith([mockImageLoadRequest]);
|
||||
expect(interleave).toHaveBeenCalledWith([[mockImageLoadRequest]]);
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
});
|
||||
|
||||
it('should add requests to imageLoadPoolManager with correct parameters', () => {
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
csEnums.RequestType.Prefetch,
|
||||
mockImageLoadRequest.additionalDetails,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple volume inputs', () => {
|
||||
const mockVolumeInput2 = {
|
||||
volumeId: 'test-volume-id-2',
|
||||
};
|
||||
|
||||
const mockVolume2 = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid-2',
|
||||
},
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([mockImageLoadRequest]),
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2)
|
||||
.mockReturnValueOnce(mockVolume)
|
||||
.mockReturnValueOnce(mockVolume2);
|
||||
|
||||
interleaveNthLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, mockVolumeInput2],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput2.volumeId);
|
||||
expect(mockVolume.getImageLoadRequests).toHaveBeenCalled();
|
||||
expect(mockVolume2.getImageLoadRequests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle volumes without getImageLoadRequests method', () => {
|
||||
const mockVolumeWithoutMethod = {
|
||||
metadata: {
|
||||
SeriesInstanceUID: 'test-series-uid',
|
||||
},
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock)
|
||||
.mockReturnValueOnce(mockVolumeWithoutMethod)
|
||||
.mockReturnValueOnce(mockVolumeWithoutMethod);
|
||||
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(getNthFrames).not.toHaveBeenCalled();
|
||||
expect(interleave).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should handle volumes with null getImageLoadRequests return', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue(null);
|
||||
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(getNthFrames).not.toHaveBeenCalled();
|
||||
expect(interleave).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should filter out requests without imageId in first request', () => {
|
||||
const requestWithoutImageId = {
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details',
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests
|
||||
.mockReturnValueOnce([requestWithoutImageId, mockImageLoadRequest])
|
||||
.mockReturnValueOnce([mockImageLoadRequest]);
|
||||
|
||||
interleaveNthLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, { volumeId: 'test-volume-id-2' }],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getNthFrames).toHaveBeenCalledWith([mockImageLoadRequest]);
|
||||
});
|
||||
|
||||
it('should handle empty image load requests', () => {
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([]);
|
||||
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(getNthFrames).not.toHaveBeenCalled();
|
||||
expect(interleave).toHaveBeenCalledWith([]);
|
||||
});
|
||||
|
||||
it('should bind callLoadImage with correct parameters', () => {
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
const addRequestCall = (imageLoadPoolManager.addRequest as jest.Mock).mock.calls[0];
|
||||
const boundFunction = addRequestCall[0];
|
||||
|
||||
boundFunction();
|
||||
|
||||
expect(mockImageLoadRequest.callLoadImage).toHaveBeenCalledWith(
|
||||
mockImageLoadRequest.imageId,
|
||||
mockImageLoadRequest.imageIdIndex,
|
||||
mockImageLoadRequest.options
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle duplicate volume IDs by not adding them again', () => {
|
||||
const duplicateVolumeInput = {
|
||||
volumeId: mockVolumeInput.volumeId,
|
||||
};
|
||||
|
||||
interleaveNthLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
...defaultParameters.data,
|
||||
volumeInputArray: [mockVolumeInput, duplicateVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cache.getVolume).toHaveBeenCalledTimes(3);
|
||||
expect(cache.getVolume).toHaveBeenCalledWith(mockVolumeInput.volumeId);
|
||||
});
|
||||
|
||||
it('should clear internal maps after processing', () => {
|
||||
const firstResult = interleaveNthLoader(defaultParameters);
|
||||
const secondResult = interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(firstResult).toBeInstanceOf(Map);
|
||||
expect(secondResult).toBeInstanceOf(Map);
|
||||
expect(firstResult.get(defaultParameters.data.viewportId)).toEqual(
|
||||
defaultParameters.data.volumeInputArray
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple image load requests', () => {
|
||||
const mockImageLoadRequest2 = {
|
||||
imageId: 'test-image-id-2',
|
||||
callLoadImage: jest.fn(),
|
||||
additionalDetails: 'test-details-2',
|
||||
imageIdIndex: 1,
|
||||
options: { test: 'option2' },
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
|
||||
|
||||
(interleave as jest.Mock).mockReturnValue([mockImageLoadRequest, mockImageLoadRequest2]);
|
||||
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledTimes(2);
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.any(Function),
|
||||
csEnums.RequestType.Prefetch,
|
||||
mockImageLoadRequest.additionalDetails,
|
||||
0
|
||||
);
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.any(Function),
|
||||
csEnums.RequestType.Prefetch,
|
||||
mockImageLoadRequest2.additionalDetails,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle volume with undefined metadata', () => {
|
||||
const mockVolumeWithoutMetadata = {
|
||||
getImageLoadRequests: jest.fn().mockReturnValue([]),
|
||||
};
|
||||
|
||||
(cache.getVolume as jest.Mock).mockReturnValueOnce(mockVolumeWithoutMetadata);
|
||||
|
||||
expect(() => interleaveNthLoader(defaultParameters)).toThrow();
|
||||
});
|
||||
|
||||
it('should return copy of viewport volume input array map', () => {
|
||||
const result = interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(result.has(defaultParameters.data.viewportId)).toBe(true);
|
||||
expect(result.get(defaultParameters.data.viewportId)).toEqual(
|
||||
defaultParameters.data.volumeInputArray
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle requests with missing properties gracefully', () => {
|
||||
const incompleteRequest = {
|
||||
imageId: 'test-image-id',
|
||||
callLoadImage: jest.fn(),
|
||||
};
|
||||
|
||||
mockVolume.getImageLoadRequests.mockReturnValue([incompleteRequest]);
|
||||
(interleave as jest.Mock).mockReturnValue([incompleteRequest]);
|
||||
|
||||
interleaveNthLoader(defaultParameters);
|
||||
|
||||
expect(imageLoadPoolManager.addRequest).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
csEnums.RequestType.Prefetch,
|
||||
undefined,
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('should process different viewport IDs separately', () => {
|
||||
const firstResult = interleaveNthLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-1',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
const secondResult = interleaveNthLoader({
|
||||
...defaultParameters,
|
||||
data: {
|
||||
viewportId: 'viewport-2',
|
||||
volumeInputArray: [mockVolumeInput],
|
||||
},
|
||||
});
|
||||
|
||||
expect(firstResult.has('viewport-1')).toBe(true);
|
||||
expect(secondResult.has('viewport-2')).toBe(true);
|
||||
expect(firstResult.has('viewport-2')).toBe(false);
|
||||
});
|
||||
});
|
||||
490
extensions/cornerstone/src/utils/promptHydrationDialog.test.ts
Normal file
490
extensions/cornerstone/src/utils/promptHydrationDialog.test.ts
Normal file
@ -0,0 +1,490 @@
|
||||
import promptHydrationDialog, { HydrationType } from './promptHydrationDialog';
|
||||
|
||||
describe('promptHydrationDialog', () => {
|
||||
const mockUIViewportDialogService = {
|
||||
show: jest.fn(),
|
||||
hide: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCustomizationService = {
|
||||
getCustomization: jest.fn(),
|
||||
};
|
||||
|
||||
const mockExtensionManager = {
|
||||
_appConfig: {
|
||||
measurementTrackingMode: 'standard',
|
||||
disableConfirmationPrompts: false,
|
||||
},
|
||||
};
|
||||
|
||||
const mockServicesManager = {
|
||||
services: {
|
||||
uiViewportDialogService: mockUIViewportDialogService,
|
||||
customizationService: mockCustomizationService,
|
||||
},
|
||||
_extensionManager: mockExtensionManager,
|
||||
};
|
||||
|
||||
const mockDisplaySet = {
|
||||
displaySetInstanceUID: 'test-display-set-uid',
|
||||
SeriesInstanceUID: 'test-series-uid',
|
||||
};
|
||||
|
||||
const mockHydrateCallback = jest.fn();
|
||||
const mockPreHydrateCallback = jest.fn();
|
||||
|
||||
const defaultParameters = {
|
||||
servicesManager: mockServicesManager as unknown as AppTypes.ServicesManager,
|
||||
viewportId: 'test-viewport-id',
|
||||
displaySet: mockDisplaySet as unknown as AppTypes.DisplaySet,
|
||||
preHydrateCallbacks: [mockPreHydrateCallback],
|
||||
hydrateCallback: mockHydrateCallback,
|
||||
type: HydrationType.SEG,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCustomizationService.getCustomization.mockReturnValue('Test message');
|
||||
mockHydrateCallback.mockResolvedValue(true);
|
||||
jest.spyOn(window, 'setTimeout').mockImplementation((callback: () => void) => {
|
||||
callback();
|
||||
return 0 as unknown as NodeJS.Timeout;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should return HYDRATE response when disableConfirmationPrompts is true for non-SR types', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
|
||||
const result = await promptHydrationDialog(defaultParameters);
|
||||
|
||||
expect(mockUIViewportDialogService.show).not.toHaveBeenCalled();
|
||||
expect(mockPreHydrateCallback).toHaveBeenCalled();
|
||||
expect(mockHydrateCallback).toHaveBeenCalledWith({
|
||||
segDisplaySet: mockDisplaySet,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should show dialog when disableConfirmationPrompts is false for non-SR types', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog(defaultParameters);
|
||||
|
||||
expect(mockUIViewportDialogService.show).toHaveBeenCalledWith({
|
||||
id: 'promptHydrateSEG',
|
||||
viewportId: defaultParameters.viewportId,
|
||||
type: 'info',
|
||||
message: 'Test message',
|
||||
actions: [
|
||||
{
|
||||
id: 'no-hydrate',
|
||||
type: 'secondary',
|
||||
text: 'No',
|
||||
value: 0,
|
||||
},
|
||||
{
|
||||
id: 'yes-hydrate',
|
||||
type: 'primary',
|
||||
text: 'Yes',
|
||||
value: 5,
|
||||
},
|
||||
],
|
||||
onSubmit: expect.any(Function),
|
||||
onOutsideClick: expect.any(Function),
|
||||
onKeyPress: expect.any(Function),
|
||||
});
|
||||
|
||||
const onSubmit = mockUIViewportDialogService.show.mock.calls[0][0].onSubmit;
|
||||
onSubmit(5);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle CANCEL response', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog(defaultParameters);
|
||||
|
||||
const onSubmit = mockUIViewportDialogService.show.mock.calls[0][0].onSubmit;
|
||||
onSubmit(0);
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
expect(mockHydrateCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onOutsideClick', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog(defaultParameters);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
expect(mockUIViewportDialogService.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onKeyPress Enter', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog(defaultParameters);
|
||||
|
||||
const onKeyPress = mockUIViewportDialogService.show.mock.calls[0][0].onKeyPress;
|
||||
onKeyPress({ key: 'Enter' });
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
expect(mockUIViewportDialogService.hide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onKeyPress non-Enter key', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog(defaultParameters);
|
||||
|
||||
const onKeyPress = mockUIViewportDialogService.show.mock.calls[0][0].onKeyPress;
|
||||
onKeyPress({ key: 'Escape' });
|
||||
|
||||
expect(mockUIViewportDialogService.hide).not.toHaveBeenCalled();
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle SEG type hydration with setTimeout', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
|
||||
await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SEG,
|
||||
});
|
||||
|
||||
expect(window.setTimeout).toHaveBeenCalledWith(expect.any(Function), 0);
|
||||
expect(mockHydrateCallback).toHaveBeenCalledWith({
|
||||
segDisplaySet: mockDisplaySet,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle RTSTRUCT type hydration', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
|
||||
const result = await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.RTSTRUCT,
|
||||
});
|
||||
|
||||
expect(mockHydrateCallback).toHaveBeenCalledWith({
|
||||
rtDisplaySet: mockDisplaySet,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
servicesManager: mockServicesManager,
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle SR type hydration when standardMode is true', async () => {
|
||||
mockExtensionManager._appConfig.measurementTrackingMode = 'standard';
|
||||
const mockHydrationResult = {
|
||||
StudyInstanceUID: 'test-study-uid',
|
||||
SeriesInstanceUIDs: ['series-1', 'series-2'],
|
||||
};
|
||||
mockHydrateCallback.mockResolvedValue(mockHydrationResult);
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SR,
|
||||
});
|
||||
|
||||
const onSubmit = mockUIViewportDialogService.show.mock.calls[0][0].onSubmit;
|
||||
onSubmit(5);
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toEqual({
|
||||
userResponse: 5,
|
||||
displaySetInstanceUID: mockDisplaySet.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: mockDisplaySet.SeriesInstanceUID,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
StudyInstanceUID: mockHydrationResult.StudyInstanceUID,
|
||||
SeriesInstanceUIDs: mockHydrationResult.SeriesInstanceUIDs,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle SR type hydration when standardMode is false', async () => {
|
||||
mockExtensionManager._appConfig.measurementTrackingMode = 'other';
|
||||
|
||||
const result = await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SR,
|
||||
});
|
||||
|
||||
expect(mockUIViewportDialogService.show).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
userResponse: 5,
|
||||
displaySetInstanceUID: mockDisplaySet.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: mockDisplaySet.SeriesInstanceUID,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
StudyInstanceUID: undefined,
|
||||
SeriesInstanceUIDs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle SR type with CANCEL response', async () => {
|
||||
mockExtensionManager._appConfig.measurementTrackingMode = 'standard';
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SR,
|
||||
});
|
||||
|
||||
const onSubmit = mockUIViewportDialogService.show.mock.calls[0][0].onSubmit;
|
||||
onSubmit(0);
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toEqual({
|
||||
userResponse: 0,
|
||||
displaySetInstanceUID: mockDisplaySet.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: mockDisplaySet.SeriesInstanceUID,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle undefined preHydrateCallbacks', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
|
||||
await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
preHydrateCallbacks: undefined,
|
||||
});
|
||||
|
||||
expect(mockHydrateCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty preHydrateCallbacks array', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
|
||||
await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
preHydrateCallbacks: [],
|
||||
});
|
||||
|
||||
expect(mockHydrateCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should execute multiple preHydrateCallbacks', async () => {
|
||||
const mockPreHydrateCallback2 = jest.fn();
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
|
||||
await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
preHydrateCallbacks: [mockPreHydrateCallback, mockPreHydrateCallback2],
|
||||
});
|
||||
|
||||
expect(mockPreHydrateCallback).toHaveBeenCalled();
|
||||
expect(mockPreHydrateCallback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should get correct customization message key for SEG type', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SEG,
|
||||
});
|
||||
|
||||
expect(mockCustomizationService.getCustomization).toHaveBeenCalledWith(
|
||||
'viewportNotification.hydrateSEGMessage'
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should get correct customization message key for RTSTRUCT type', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.RTSTRUCT,
|
||||
});
|
||||
|
||||
expect(mockCustomizationService.getCustomization).toHaveBeenCalledWith(
|
||||
'viewportNotification.hydrateRTMessage'
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should get correct customization message key for SR type', async () => {
|
||||
mockExtensionManager._appConfig.measurementTrackingMode = 'standard';
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SR,
|
||||
});
|
||||
|
||||
expect(mockCustomizationService.getCustomization).toHaveBeenCalledWith(
|
||||
'viewportNotification.hydrateSRMessage'
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should get default customization message key for unknown type', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: 'UNKNOWN_TYPE',
|
||||
});
|
||||
|
||||
expect(mockCustomizationService.getCustomization).toHaveBeenCalledWith(
|
||||
'viewportNotification.hydrateMessage'
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should get correct dialog id for SEG type', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SEG,
|
||||
});
|
||||
|
||||
expect(mockUIViewportDialogService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'promptHydrateSEG',
|
||||
})
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should get correct dialog id for SR type', async () => {
|
||||
mockExtensionManager._appConfig.measurementTrackingMode = 'standard';
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SR,
|
||||
});
|
||||
|
||||
expect(mockUIViewportDialogService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'promptHydrateSR',
|
||||
})
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should get default dialog id for unknown type', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: 'UNKNOWN_TYPE',
|
||||
});
|
||||
|
||||
expect(mockUIViewportDialogService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'promptHydrate',
|
||||
})
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
|
||||
it('should handle hydrateCallback returning false for SEG', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
mockHydrateCallback.mockResolvedValue(false);
|
||||
|
||||
const result = await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SEG,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle hydrateCallback returning false for RTSTRUCT', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = true;
|
||||
mockHydrateCallback.mockResolvedValue(false);
|
||||
|
||||
const result = await promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.RTSTRUCT,
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle SR hydration with null result', async () => {
|
||||
mockExtensionManager._appConfig.measurementTrackingMode = 'standard';
|
||||
mockHydrateCallback.mockResolvedValue(null);
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: HydrationType.SR,
|
||||
});
|
||||
|
||||
const onSubmit = mockUIViewportDialogService.show.mock.calls[0][0].onSubmit;
|
||||
onSubmit(5);
|
||||
|
||||
const result = await promise;
|
||||
|
||||
expect(result).toEqual({
|
||||
userResponse: 5,
|
||||
displaySetInstanceUID: mockDisplaySet.displaySetInstanceUID,
|
||||
srSeriesInstanceUID: mockDisplaySet.SeriesInstanceUID,
|
||||
viewportId: defaultParameters.viewportId,
|
||||
StudyInstanceUID: undefined,
|
||||
SeriesInstanceUIDs: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle RTSTRUCT type in getDialogId', async () => {
|
||||
mockExtensionManager._appConfig.disableConfirmationPrompts = false;
|
||||
|
||||
const promise = promptHydrationDialog({
|
||||
...defaultParameters,
|
||||
type: 'RTSTRUCT',
|
||||
});
|
||||
|
||||
expect(mockUIViewportDialogService.show).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'promptHydrateRT',
|
||||
})
|
||||
);
|
||||
|
||||
const onOutsideClick = mockUIViewportDialogService.show.mock.calls[0][0].onOutsideClick;
|
||||
onOutsideClick();
|
||||
await promise;
|
||||
});
|
||||
});
|
||||
@ -49,7 +49,7 @@ function getCustomizationMessageKey(type: string): string {
|
||||
|
||||
function getDialogId(type: string): string {
|
||||
switch (type) {
|
||||
case HydrationType.RTSS:
|
||||
case HydrationType.RTSTRUCT:
|
||||
return 'promptHydrateRT';
|
||||
case HydrationType.SEG:
|
||||
return 'promptHydrateSEG';
|
||||
|
||||
@ -0,0 +1,129 @@
|
||||
import { segmentation } from '@cornerstonejs/tools';
|
||||
import removeViewportSegmentationRepresentations from './removeViewportSegmentationRepresentations';
|
||||
|
||||
jest.mock('@cornerstonejs/tools', () => ({
|
||||
segmentation: {
|
||||
state: {
|
||||
getSegmentationRepresentations: jest.fn(),
|
||||
removeSegmentationRepresentation: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('removeViewportSegmentationRepresentations', () => {
|
||||
const viewportId = 'viewport-1';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return early when no representations are found', () => {
|
||||
(segmentation.state.getSegmentationRepresentations as jest.Mock).mockReturnValue(null);
|
||||
|
||||
removeViewportSegmentationRepresentations(viewportId);
|
||||
|
||||
expect(segmentation.state.getSegmentationRepresentations).toHaveBeenCalledWith(viewportId);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return early when representations array is empty', () => {
|
||||
(segmentation.state.getSegmentationRepresentations as jest.Mock).mockReturnValue([]);
|
||||
|
||||
removeViewportSegmentationRepresentations(viewportId);
|
||||
|
||||
expect(segmentation.state.removeSegmentationRepresentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove single segmentation representation', () => {
|
||||
const representations = [
|
||||
{
|
||||
segmentationRepresentationUID: 'representation-1',
|
||||
},
|
||||
];
|
||||
|
||||
(segmentation.state.getSegmentationRepresentations as jest.Mock).mockReturnValue(
|
||||
representations
|
||||
);
|
||||
|
||||
removeViewportSegmentationRepresentations(viewportId);
|
||||
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenCalledTimes(1);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenCalledWith(
|
||||
'representation-1'
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove multiple segmentation representations', () => {
|
||||
const representations = [
|
||||
{
|
||||
segmentationRepresentationUID: 'representation-1',
|
||||
},
|
||||
{
|
||||
segmentationRepresentationUID: 'representation-2',
|
||||
},
|
||||
{
|
||||
segmentationRepresentationUID: 'representation-3',
|
||||
},
|
||||
];
|
||||
|
||||
(segmentation.state.getSegmentationRepresentations as jest.Mock).mockReturnValue(
|
||||
representations
|
||||
);
|
||||
|
||||
removeViewportSegmentationRepresentations(viewportId);
|
||||
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenCalledTimes(3);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'representation-1'
|
||||
);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'representation-2'
|
||||
);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'representation-3'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle representations with additional properties', () => {
|
||||
const representations = [
|
||||
{
|
||||
segmentationRepresentationUID: 'representation-1',
|
||||
type: 'LABELMAP',
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
segmentationRepresentationUID: 'representation-2',
|
||||
type: 'CONTOUR',
|
||||
active: false,
|
||||
visible: true,
|
||||
},
|
||||
];
|
||||
|
||||
(segmentation.state.getSegmentationRepresentations as jest.Mock).mockReturnValue(
|
||||
representations
|
||||
);
|
||||
|
||||
removeViewportSegmentationRepresentations(viewportId);
|
||||
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenCalledTimes(2);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'representation-1'
|
||||
);
|
||||
expect(segmentation.state.removeSegmentationRepresentation).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'representation-2'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle undefined representations', () => {
|
||||
(segmentation.state.getSegmentationRepresentations as jest.Mock).mockReturnValue(undefined);
|
||||
|
||||
removeViewportSegmentationRepresentations(viewportId);
|
||||
|
||||
expect(segmentation.state.removeSegmentationRepresentation).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
479
extensions/cornerstone/src/utils/segmentUtils.test.ts
Normal file
479
extensions/cornerstone/src/utils/segmentUtils.test.ts
Normal file
@ -0,0 +1,479 @@
|
||||
import { handleSegmentChange } from './segmentUtils';
|
||||
|
||||
describe('handleSegmentChange', () => {
|
||||
const mockSegmentationService = {
|
||||
getSegmentation: jest.fn(),
|
||||
getActiveSegment: jest.fn(),
|
||||
setActiveSegment: jest.fn(),
|
||||
jumpToSegmentCenter: jest.fn(),
|
||||
};
|
||||
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
'2': {
|
||||
segmentIndex: 2,
|
||||
label: 'Segment 2',
|
||||
},
|
||||
'3': {
|
||||
segmentIndex: 3,
|
||||
label: 'Segment 3',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const defaultParameters = {
|
||||
direction: 1,
|
||||
segmentationId: 'test-segmentation-id',
|
||||
viewportId: 'test-viewport-id',
|
||||
selectedSegmentObjectIndex: 0,
|
||||
// @ts-expect-error - only part of the SegmentationService is needed
|
||||
segmentationService: mockSegmentationService as AppTypes.SegmentationService,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
});
|
||||
|
||||
it('should move to next segment when direction is positive and activeSegment is null', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.getSegmentation).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId
|
||||
);
|
||||
expect(mockSegmentationService.getActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
2
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
2,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should move to previous segment when direction is negative and activeSegment is null', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: -1,
|
||||
selectedSegmentObjectIndex: 1,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should update selectedSegmentObjectIndex when activeSegment is found', () => {
|
||||
const mockActiveSegment = {
|
||||
segmentIndex: 2,
|
||||
};
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(mockActiveSegment);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should loop to first segment when going beyond last segment', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 2,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should loop to last segment when going before first segment', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: -1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle direction with value greater than 1', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 2,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle direction with value less than -1', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: -2,
|
||||
selectedSegmentObjectIndex: 1,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle single segment scenario with positive direction', () => {
|
||||
const singleSegmentSegmentation = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Only Segment',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(singleSegmentSegmentation);
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle single segment scenario with negative direction', () => {
|
||||
const singleSegmentSegmentation = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Only Segment',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(singleSegmentSegmentation);
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: -1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle activeSegment not found in segments', () => {
|
||||
const mockActiveSegment = {
|
||||
segmentIndex: 99,
|
||||
};
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(mockActiveSegment);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle zero direction', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 0,
|
||||
selectedSegmentObjectIndex: 1,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
2
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
2,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle segments with non-sequential indices', () => {
|
||||
const nonSequentialSegmentation = {
|
||||
segments: {
|
||||
'5': {
|
||||
segmentIndex: 5,
|
||||
label: 'Segment 5',
|
||||
},
|
||||
'10': {
|
||||
segmentIndex: 10,
|
||||
label: 'Segment 10',
|
||||
},
|
||||
'15': {
|
||||
segmentIndex: 15,
|
||||
label: 'Segment 15',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(nonSequentialSegmentation);
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
10
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
10,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle wrap around with non-sequential indices', () => {
|
||||
const nonSequentialSegmentation = {
|
||||
segments: {
|
||||
'5': {
|
||||
segmentIndex: 5,
|
||||
label: 'Segment 5',
|
||||
},
|
||||
'10': {
|
||||
segmentIndex: 10,
|
||||
label: 'Segment 10',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(nonSequentialSegmentation);
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 1,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
5
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
5,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty segments object', () => {
|
||||
const emptySegmentation = {
|
||||
segments: {},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(emptySegmentation);
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
undefined
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
undefined,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle large positive direction that wraps multiple times', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 5,
|
||||
selectedSegmentObjectIndex: 1,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle large negative direction that wraps multiple times', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: -5,
|
||||
selectedSegmentObjectIndex: 1,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
3,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle activeSegment with segmentIndex 0', () => {
|
||||
const mockActiveSegment = {
|
||||
segmentIndex: 0,
|
||||
};
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(mockActiveSegment);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
defaultParameters.segmentationId,
|
||||
1,
|
||||
defaultParameters.viewportId
|
||||
);
|
||||
});
|
||||
|
||||
it('should use different viewport and segmentation IDs', () => {
|
||||
mockSegmentationService.getActiveSegment.mockReturnValue(null);
|
||||
|
||||
handleSegmentChange({
|
||||
...defaultParameters,
|
||||
segmentationId: 'different-segmentation-id',
|
||||
viewportId: 'different-viewport-id',
|
||||
direction: 1,
|
||||
selectedSegmentObjectIndex: 0,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.getSegmentation).toHaveBeenCalledWith(
|
||||
'different-segmentation-id'
|
||||
);
|
||||
expect(mockSegmentationService.getActiveSegment).toHaveBeenCalledWith('different-viewport-id');
|
||||
expect(mockSegmentationService.setActiveSegment).toHaveBeenCalledWith(
|
||||
'different-segmentation-id',
|
||||
2
|
||||
);
|
||||
expect(mockSegmentationService.jumpToSegmentCenter).toHaveBeenCalledWith(
|
||||
'different-segmentation-id',
|
||||
2,
|
||||
'different-viewport-id'
|
||||
);
|
||||
});
|
||||
});
|
||||
614
extensions/cornerstone/src/utils/segmentationHandlers.test.ts
Normal file
614
extensions/cornerstone/src/utils/segmentationHandlers.test.ts
Normal file
@ -0,0 +1,614 @@
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import {
|
||||
setupSegmentationDataModifiedHandler,
|
||||
setupSegmentationModifiedHandler,
|
||||
} from './segmentationHandlers';
|
||||
import { updateSegmentationStats } from './updateSegmentationStats';
|
||||
|
||||
jest.mock('@cornerstonejs/tools', () => ({
|
||||
annotation: {
|
||||
state: {
|
||||
getAllAnnotations: jest.fn(),
|
||||
removeAnnotation: jest.fn(),
|
||||
},
|
||||
},
|
||||
SegmentBidirectionalTool: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./updateSegmentationStats', () => ({
|
||||
updateSegmentationStats: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('setupSegmentationDataModifiedHandler', () => {
|
||||
const mockSegmentationService = {
|
||||
EVENTS: {
|
||||
SEGMENTATION_DATA_MODIFIED: 'SEGMENTATION_DATA_MODIFIED',
|
||||
},
|
||||
subscribeDebounced: jest.fn(),
|
||||
getSegmentation: jest.fn(),
|
||||
addOrUpdateSegmentation: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCustomizationService = {
|
||||
getCustomization: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCommandsManager = {
|
||||
runCommand: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUnsubscribeDebounced = jest.fn();
|
||||
|
||||
const defaultParameters = {
|
||||
segmentationService: mockSegmentationService,
|
||||
customizationService: mockCustomizationService,
|
||||
commandsManager: mockCommandsManager,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSegmentationService.subscribeDebounced.mockReturnValue({
|
||||
unsubscribe: mockUnsubscribeDebounced,
|
||||
});
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
it('should subscribe to SEGMENTATION_DATA_MODIFIED event with debounce', () => {
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
expect(mockSegmentationService.subscribeDebounced).toHaveBeenCalledWith(
|
||||
mockSegmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||
expect.any(Function),
|
||||
1000
|
||||
);
|
||||
});
|
||||
|
||||
it('should return unsubscribe function', () => {
|
||||
const result = setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
expect(result).toEqual({
|
||||
unsubscribe: expect.any(Function),
|
||||
});
|
||||
|
||||
result.unsubscribe();
|
||||
|
||||
expect(mockUnsubscribeDebounced).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return early when segmentation is null', async () => {
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(null);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(mockSegmentationService.getSegmentation).toHaveBeenCalledWith('test-id');
|
||||
expect(updateSegmentationStats).not.toHaveBeenCalled();
|
||||
expect(mockSegmentationService.addOrUpdateSegmentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return early when disableUpdateSegmentationStats is true', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization.mockReturnValue(true);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(mockCustomizationService.getCustomization).toHaveBeenCalledWith(
|
||||
'panelSegmentation.disableUpdateSegmentationStats'
|
||||
);
|
||||
expect(updateSegmentationStats).not.toHaveBeenCalled();
|
||||
expect(mockSegmentationService.addOrUpdateSegmentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should update segmentation stats when conditions are met', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
2: { segmentIndex: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockUpdatedSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1, stats: 'updated' },
|
||||
2: { segmentIndex: 2, stats: 'updated' },
|
||||
},
|
||||
};
|
||||
|
||||
const mockReadableText = { mean: 'Mean' };
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization
|
||||
.mockReturnValueOnce(false)
|
||||
.mockReturnValueOnce(mockReadableText);
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(mockUpdatedSegmentation);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(updateSegmentationStats).toHaveBeenCalledWith({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
expect(mockSegmentationService.addOrUpdateSegmentation).toHaveBeenCalledWith({
|
||||
segmentationId: 'test-id',
|
||||
segments: mockUpdatedSegmentation.segments,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not update segmentation when unsubscribed', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockUpdatedSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1, stats: 'updated' },
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization.mockReturnValue(false);
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(mockUpdatedSegmentation);
|
||||
|
||||
const result = setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
result.unsubscribe();
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(updateSegmentationStats).toHaveBeenCalled();
|
||||
expect(mockSegmentationService.addOrUpdateSegmentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not update segmentation when updateSegmentationStats returns null', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization.mockReturnValue(false);
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(updateSegmentationStats).toHaveBeenCalled();
|
||||
expect(mockSegmentationService.addOrUpdateSegmentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should run bidirectional command for segments with bidirectional stats', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
0: { segmentIndex: 0 },
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
bidirectional: { data: 'test' },
|
||||
},
|
||||
},
|
||||
},
|
||||
2: { segmentIndex: 2 },
|
||||
3: {
|
||||
segmentIndex: 3,
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
bidirectional: { data: 'test2' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization.mockReturnValue(false);
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(mockCommandsManager.runCommand).toHaveBeenCalledTimes(2);
|
||||
expect(mockCommandsManager.runCommand).toHaveBeenNthCalledWith(1, 'runSegmentBidirectional', {
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
});
|
||||
expect(mockCommandsManager.runCommand).toHaveBeenNthCalledWith(2, 'runSegmentBidirectional', {
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not run bidirectional command for segment index 0', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
0: {
|
||||
segmentIndex: 0,
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
bidirectional: { data: 'test' },
|
||||
},
|
||||
},
|
||||
},
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization.mockReturnValue(false);
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle segment without cachedStats', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
2: {
|
||||
segmentIndex: 2,
|
||||
cachedStats: undefined,
|
||||
},
|
||||
3: {
|
||||
segmentIndex: 3,
|
||||
cachedStats: {
|
||||
namedStats: undefined,
|
||||
},
|
||||
},
|
||||
4: {
|
||||
segmentIndex: 4,
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
other: { data: 'test' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
mockCustomizationService.getCustomization.mockReturnValue(false);
|
||||
(updateSegmentationStats as jest.Mock).mockResolvedValue(null);
|
||||
|
||||
setupSegmentationDataModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribeDebounced.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(mockCommandsManager.runCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupSegmentationModifiedHandler', () => {
|
||||
const mockSegmentationService = {
|
||||
EVENTS: {
|
||||
SEGMENTATION_MODIFIED: 'SEGMENTATION_MODIFIED',
|
||||
},
|
||||
subscribe: jest.fn(),
|
||||
getSegmentation: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUnsubscribe = jest.fn();
|
||||
|
||||
const defaultParameters = {
|
||||
segmentationService: mockSegmentationService,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSegmentationService.subscribe.mockReturnValue({
|
||||
unsubscribe: mockUnsubscribe,
|
||||
});
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue([]);
|
||||
});
|
||||
|
||||
it('should subscribe to SEGMENTATION_MODIFIED event', () => {
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
expect(mockSegmentationService.subscribe).toHaveBeenCalledWith(
|
||||
mockSegmentationService.EVENTS.SEGMENTATION_MODIFIED,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return unsubscribe function', () => {
|
||||
const result = setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
expect(result).toEqual({
|
||||
unsubscribe: mockUnsubscribe,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return early when segmentation is null', async () => {
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(null);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.getAllAnnotations).not.toHaveBeenCalled();
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove bidirectional annotations for non-existent segments', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
3: { segmentIndex: 3 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockAnnotations = [
|
||||
{
|
||||
annotationUID: 'annotation-1',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'annotation-2',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'annotation-3',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'other-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'annotation-4',
|
||||
metadata: {
|
||||
toolName: 'OtherTool',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue(
|
||||
mockAnnotations
|
||||
);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).toHaveBeenCalledTimes(1);
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).toHaveBeenCalledWith('annotation-2');
|
||||
});
|
||||
|
||||
it('should not remove annotations for existing segments', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
2: { segmentIndex: 2 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockAnnotations = [
|
||||
{
|
||||
annotationUID: 'annotation-1',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'annotation-2',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue(
|
||||
mockAnnotations
|
||||
);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty segments object', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {},
|
||||
};
|
||||
|
||||
const mockAnnotations = [
|
||||
{
|
||||
annotationUID: 'annotation-1',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue(
|
||||
mockAnnotations
|
||||
);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).toHaveBeenCalledWith('annotation-1');
|
||||
});
|
||||
|
||||
it('should filter out segment index 0', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
0: { segmentIndex: 0 },
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockAnnotations = [
|
||||
{
|
||||
annotationUID: 'annotation-0',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'annotation-1',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue(
|
||||
mockAnnotations
|
||||
);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).toHaveBeenCalledWith('annotation-0');
|
||||
});
|
||||
|
||||
it('should handle no bidirectional annotations', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockAnnotations = [
|
||||
{
|
||||
annotationUID: 'annotation-1',
|
||||
metadata: {
|
||||
toolName: 'OtherTool',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue(
|
||||
mockAnnotations
|
||||
);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle mixed annotation types and segmentation IDs', async () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: { segmentIndex: 1 },
|
||||
},
|
||||
};
|
||||
|
||||
const mockAnnotations = [
|
||||
{
|
||||
annotationUID: 'keep-1',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'remove-1',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'keep-2',
|
||||
metadata: {
|
||||
toolName: 'SegmentBidirectional',
|
||||
segmentationId: 'other-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
annotationUID: 'keep-3',
|
||||
metadata: {
|
||||
toolName: 'OtherTool',
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 2,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
(cornerstoneTools.annotation.state.getAllAnnotations as jest.Mock).mockReturnValue(
|
||||
mockAnnotations
|
||||
);
|
||||
|
||||
setupSegmentationModifiedHandler(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
await callback({ segmentationId: 'test-id' });
|
||||
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).toHaveBeenCalledTimes(1);
|
||||
expect(cornerstoneTools.annotation.state.removeAnnotation).toHaveBeenCalledWith('remove-1');
|
||||
});
|
||||
});
|
||||
@ -16,8 +16,9 @@ export function setupSegmentationDataModifiedHandler({
|
||||
const { unsubscribe: debouncedUnsubscribe } = segmentationService.subscribeDebounced(
|
||||
segmentationService.EVENTS.SEGMENTATION_DATA_MODIFIED,
|
||||
async ({ segmentationId }) => {
|
||||
|
||||
const disableUpdateSegmentationStats = customizationService.getCustomization('panelSegmentation.disableUpdateSegmentationStats');
|
||||
const disableUpdateSegmentationStats = customizationService.getCustomization(
|
||||
'panelSegmentation.disableUpdateSegmentationStats'
|
||||
);
|
||||
|
||||
const segmentation = segmentationService.getSegmentation(segmentationId);
|
||||
|
||||
@ -85,27 +86,21 @@ export function setupSegmentationModifiedHandler({ segmentationService }) {
|
||||
annotation.metadata.toolName === cornerstoneTools.SegmentBidirectionalTool.toolName
|
||||
);
|
||||
|
||||
let toRemoveUIDs = [];
|
||||
if (!segmentation) {
|
||||
toRemoveUIDs = bidirectionalAnnotations.map(
|
||||
annotation => annotation.metadata.segmentationId === segmentationId
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const segmentIndices = Object.keys(segmentation.segments)
|
||||
.map(index => parseInt(index))
|
||||
.filter(index => index > 0);
|
||||
const segmentIndices = Object.keys(segmentation.segments)
|
||||
.map(index => parseInt(index))
|
||||
.filter(index => index > 0);
|
||||
|
||||
// check if there is a bidirectional data that exists but the segment
|
||||
// does not exists anymore we need to remove the bidirectional data
|
||||
const bidirectionalAnnotationsToRemove = bidirectionalAnnotations.filter(
|
||||
annotation =>
|
||||
annotation.metadata.segmentationId === segmentationId &&
|
||||
!segmentIndices.includes(annotation.metadata.segmentIndex)
|
||||
);
|
||||
// check if there is a bidirectional data that exists but the segment
|
||||
// does not exists anymore we need to remove the bidirectional data
|
||||
const bidirectionalAnnotationsToRemove = bidirectionalAnnotations.filter(
|
||||
annotation =>
|
||||
annotation.metadata.segmentationId === segmentationId &&
|
||||
!segmentIndices.includes(annotation.metadata.segmentIndex)
|
||||
);
|
||||
|
||||
toRemoveUIDs = bidirectionalAnnotationsToRemove.map(annotation => annotation.annotationUID);
|
||||
}
|
||||
const toRemoveUIDs = bidirectionalAnnotationsToRemove.map(
|
||||
annotation => annotation.annotationUID
|
||||
);
|
||||
|
||||
toRemoveUIDs.forEach(uid => {
|
||||
cornerstoneTools.annotation.state.removeAnnotation(uid);
|
||||
|
||||
@ -0,0 +1,348 @@
|
||||
import { setUpSegmentationEventHandlers } from './setUpSegmentationEventHandlers';
|
||||
import {
|
||||
setupSegmentationDataModifiedHandler,
|
||||
setupSegmentationModifiedHandler,
|
||||
} from './segmentationHandlers';
|
||||
|
||||
jest.mock('./segmentationHandlers', () => ({
|
||||
setupSegmentationDataModifiedHandler: jest.fn(),
|
||||
setupSegmentationModifiedHandler: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('setUpSegmentationEventHandlers', () => {
|
||||
const mockSegmentationService = {
|
||||
EVENTS: {
|
||||
SEGMENTATION_ADDED: 'SEGMENTATION_ADDED',
|
||||
},
|
||||
subscribe: jest.fn(),
|
||||
getSegmentation: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCustomizationService = {};
|
||||
|
||||
const mockDisplaySetService = {
|
||||
getDisplaySetByUID: jest.fn(),
|
||||
addDisplaySets: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCommandsManager = {};
|
||||
|
||||
const mockServicesManager = {
|
||||
services: {
|
||||
segmentationService: mockSegmentationService,
|
||||
customizationService: mockCustomizationService,
|
||||
displaySetService: mockDisplaySetService,
|
||||
},
|
||||
};
|
||||
|
||||
const mockUnsubscribeDataModified = jest.fn();
|
||||
const mockUnsubscribeModified = jest.fn();
|
||||
const mockUnsubscribeCreated = jest.fn();
|
||||
|
||||
const defaultParameters = {
|
||||
servicesManager: mockServicesManager as unknown as AppTypes.ServicesManager,
|
||||
commandsManager: mockCommandsManager,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
(setupSegmentationDataModifiedHandler as jest.Mock).mockReturnValue({
|
||||
unsubscribe: mockUnsubscribeDataModified,
|
||||
});
|
||||
(setupSegmentationModifiedHandler as jest.Mock).mockReturnValue({
|
||||
unsubscribe: mockUnsubscribeModified,
|
||||
});
|
||||
mockSegmentationService.subscribe.mockReturnValue({
|
||||
unsubscribe: mockUnsubscribeCreated,
|
||||
});
|
||||
});
|
||||
|
||||
it('should setup segmentation data modified handler', () => {
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
expect(setupSegmentationDataModifiedHandler).toHaveBeenCalledWith({
|
||||
segmentationService: mockSegmentationService,
|
||||
customizationService: mockCustomizationService,
|
||||
commandsManager: mockCommandsManager,
|
||||
});
|
||||
});
|
||||
|
||||
it('should setup segmentation modified handler', () => {
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
expect(setupSegmentationModifiedHandler).toHaveBeenCalledWith({
|
||||
segmentationService: mockSegmentationService,
|
||||
});
|
||||
});
|
||||
|
||||
it('should subscribe to SEGMENTATION_ADDED event', () => {
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
expect(mockSegmentationService.subscribe).toHaveBeenCalledWith(
|
||||
mockSegmentationService.EVENTS.SEGMENTATION_ADDED,
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
|
||||
it('should return unsubscriptions object', () => {
|
||||
const result = setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
expect(result).toEqual({
|
||||
unsubscriptions: [
|
||||
mockUnsubscribeDataModified,
|
||||
mockUnsubscribeModified,
|
||||
mockUnsubscribeCreated,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return early when displaySet already exists for segmentationId', () => {
|
||||
const mockDisplaySet = {
|
||||
displaySetInstanceUID: 'test-segmentation-id',
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
const evt = { segmentationId: 'test-segmentation-id' };
|
||||
|
||||
callback(evt);
|
||||
|
||||
expect(mockDisplaySetService.getDisplaySetByUID).toHaveBeenCalledWith('test-segmentation-id');
|
||||
expect(mockSegmentationService.getSegmentation).not.toHaveBeenCalled();
|
||||
expect(mockDisplaySetService.addDisplaySets).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create and add display set when segmentation is added and no displaySet exists', () => {
|
||||
const mockSegmentation = {
|
||||
cachedStats: {
|
||||
info: 'Test Segmentation Label',
|
||||
},
|
||||
representationData: {
|
||||
Labelmap: {
|
||||
imageIds: ['image-1', 'image-2', 'image-3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(null);
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
const evt = { segmentationId: 'test-segmentation-id' };
|
||||
|
||||
callback(evt);
|
||||
|
||||
expect(mockDisplaySetService.getDisplaySetByUID).toHaveBeenCalledWith('test-segmentation-id');
|
||||
expect(mockSegmentationService.getSegmentation).toHaveBeenCalledWith('test-segmentation-id');
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenCalledWith({
|
||||
displaySetInstanceUID: 'test-segmentation-id',
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: mockSegmentation.cachedStats.info,
|
||||
Modality: 'SEG',
|
||||
numImageFrames: mockSegmentation.representationData.Labelmap.imageIds.length,
|
||||
imageIds: mockSegmentation.representationData.Labelmap.imageIds,
|
||||
isOverlayDisplaySet: true,
|
||||
label: mockSegmentation.cachedStats.info,
|
||||
madeInClient: true,
|
||||
segmentationId: 'test-segmentation-id',
|
||||
isDerived: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle displaySet undefined when segmentation is added', () => {
|
||||
const mockSegmentation = {
|
||||
cachedStats: {
|
||||
info: 'Test Segmentation Label',
|
||||
},
|
||||
representationData: {
|
||||
Labelmap: {
|
||||
imageIds: ['image-1', 'image-2'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(undefined);
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
const evt = { segmentationId: 'test-segmentation-id' };
|
||||
|
||||
callback(evt);
|
||||
|
||||
expect(mockSegmentationService.getSegmentation).toHaveBeenCalledWith('test-segmentation-id');
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenCalledWith({
|
||||
displaySetInstanceUID: 'test-segmentation-id',
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: mockSegmentation.cachedStats.info,
|
||||
Modality: 'SEG',
|
||||
numImageFrames: mockSegmentation.representationData.Labelmap.imageIds.length,
|
||||
imageIds: mockSegmentation.representationData.Labelmap.imageIds,
|
||||
isOverlayDisplaySet: true,
|
||||
label: mockSegmentation.cachedStats.info,
|
||||
madeInClient: true,
|
||||
segmentationId: 'test-segmentation-id',
|
||||
isDerived: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty imageIds array', () => {
|
||||
const mockSegmentation = {
|
||||
cachedStats: {
|
||||
info: 'Empty Segmentation',
|
||||
},
|
||||
representationData: {
|
||||
Labelmap: {
|
||||
imageIds: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(null);
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
const evt = { segmentationId: 'empty-segmentation-id' };
|
||||
|
||||
callback(evt);
|
||||
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenCalledWith({
|
||||
displaySetInstanceUID: 'empty-segmentation-id',
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: mockSegmentation.cachedStats.info,
|
||||
Modality: 'SEG',
|
||||
numImageFrames: 0,
|
||||
imageIds: [],
|
||||
isOverlayDisplaySet: true,
|
||||
label: mockSegmentation.cachedStats.info,
|
||||
madeInClient: true,
|
||||
segmentationId: 'empty-segmentation-id',
|
||||
isDerived: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle different segmentation label values', () => {
|
||||
const mockSegmentation = {
|
||||
cachedStats: {
|
||||
info: 'Custom Label Text',
|
||||
},
|
||||
representationData: {
|
||||
Labelmap: {
|
||||
imageIds: ['custom-image-1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(null);
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
const evt = { segmentationId: 'custom-segmentation-id' };
|
||||
|
||||
callback(evt);
|
||||
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenCalledWith({
|
||||
displaySetInstanceUID: 'custom-segmentation-id',
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: 'Custom Label Text',
|
||||
Modality: 'SEG',
|
||||
numImageFrames: 1,
|
||||
imageIds: ['custom-image-1'],
|
||||
isOverlayDisplaySet: true,
|
||||
label: 'Custom Label Text',
|
||||
madeInClient: true,
|
||||
segmentationId: 'custom-segmentation-id',
|
||||
isDerived: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle multiple segmentation events', () => {
|
||||
const mockSegmentation1 = {
|
||||
cachedStats: {
|
||||
info: 'Segmentation 1',
|
||||
},
|
||||
representationData: {
|
||||
Labelmap: {
|
||||
imageIds: ['image-1'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockSegmentation2 = {
|
||||
cachedStats: {
|
||||
info: 'Segmentation 2',
|
||||
},
|
||||
representationData: {
|
||||
Labelmap: {
|
||||
imageIds: ['image-2', 'image-3'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(null);
|
||||
mockSegmentationService.getSegmentation
|
||||
.mockReturnValueOnce(mockSegmentation1)
|
||||
.mockReturnValueOnce(mockSegmentation2);
|
||||
|
||||
setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
const callback = mockSegmentationService.subscribe.mock.calls[0][1];
|
||||
|
||||
callback({ segmentationId: 'segmentation-1' });
|
||||
callback({ segmentationId: 'segmentation-2' });
|
||||
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenCalledTimes(2);
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenNthCalledWith(1, {
|
||||
displaySetInstanceUID: 'segmentation-1',
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: mockSegmentation1.cachedStats.info,
|
||||
Modality: 'SEG',
|
||||
numImageFrames: 1,
|
||||
imageIds: mockSegmentation1.representationData.Labelmap.imageIds,
|
||||
isOverlayDisplaySet: true,
|
||||
label: mockSegmentation1.cachedStats.info,
|
||||
madeInClient: true,
|
||||
segmentationId: 'segmentation-1',
|
||||
isDerived: true,
|
||||
});
|
||||
expect(mockDisplaySetService.addDisplaySets).toHaveBeenNthCalledWith(2, {
|
||||
displaySetInstanceUID: 'segmentation-2',
|
||||
SOPClassUID: '1.2.840.10008.5.1.4.1.1.66.4',
|
||||
SOPClassHandlerId: '@ohif/extension-cornerstone-dicom-seg.sopClassHandlerModule.dicom-seg',
|
||||
SeriesDescription: mockSegmentation2.cachedStats.info,
|
||||
Modality: 'SEG',
|
||||
numImageFrames: 2,
|
||||
imageIds: mockSegmentation2.representationData.Labelmap.imageIds,
|
||||
isOverlayDisplaySet: true,
|
||||
label: mockSegmentation2.cachedStats.info,
|
||||
madeInClient: true,
|
||||
segmentationId: 'segmentation-2',
|
||||
isDerived: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should call all unsubscribe functions in returned unsubscriptions array', () => {
|
||||
const result = setUpSegmentationEventHandlers(defaultParameters);
|
||||
|
||||
result.unsubscriptions.forEach(unsubscribe => unsubscribe());
|
||||
|
||||
expect(mockUnsubscribeDataModified).toHaveBeenCalled();
|
||||
expect(mockUnsubscribeModified).toHaveBeenCalled();
|
||||
expect(mockUnsubscribeCreated).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
606
extensions/cornerstone/src/utils/toggleVOISliceSync.test.ts
Normal file
606
extensions/cornerstone/src/utils/toggleVOISliceSync.test.ts
Normal file
@ -0,0 +1,606 @@
|
||||
import toggleVOISliceSync from './toggleVOISliceSync';
|
||||
|
||||
describe('toggleVOISliceSync', () => {
|
||||
const mockSyncGroupService = {
|
||||
getSynchronizersForViewport: jest.fn(),
|
||||
addViewportToSyncGroup: jest.fn(),
|
||||
removeViewportFromSyncGroup: jest.fn(),
|
||||
};
|
||||
|
||||
const mockViewportGridService = {
|
||||
getState: jest.fn(),
|
||||
};
|
||||
|
||||
const mockDisplaySetService = {
|
||||
getDisplaySetByUID: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCornerstoneViewportService = {
|
||||
getCornerstoneViewport: jest.fn(),
|
||||
};
|
||||
|
||||
const mockServicesManager = {
|
||||
services: {
|
||||
syncGroupService: mockSyncGroupService,
|
||||
viewportGridService: mockViewportGridService,
|
||||
displaySetService: mockDisplaySetService,
|
||||
cornerstoneViewportService: mockCornerstoneViewportService,
|
||||
},
|
||||
};
|
||||
|
||||
const mockViewport = {
|
||||
id: 'viewport-1',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-1' })),
|
||||
};
|
||||
|
||||
const mockViewport2 = {
|
||||
id: 'viewport-2',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-2' })),
|
||||
};
|
||||
|
||||
const mockGridViewport = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-1',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-1'],
|
||||
};
|
||||
|
||||
const mockDisplaySet = {
|
||||
Modality: 'CT',
|
||||
};
|
||||
|
||||
const defaultParameters = {
|
||||
servicesManager: mockServicesManager as unknown as AppTypes.ServicesManager,
|
||||
viewports: null,
|
||||
syncId: null,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockCornerstoneViewportService.getCornerstoneViewport.mockReturnValue(mockViewport);
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
});
|
||||
|
||||
it('should enable sync when no sync exists for single modality', () => {
|
||||
const viewports = {
|
||||
CT: [mockGridViewport],
|
||||
};
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.getSynchronizersForViewport).toHaveBeenCalledWith('viewport-1');
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable sync when sync already exists', () => {
|
||||
const viewports = {
|
||||
CT: [mockGridViewport],
|
||||
};
|
||||
|
||||
const mockSyncState = {
|
||||
id: 'VOI_SYNC_CT',
|
||||
};
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([mockSyncState]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
'VOI_SYNC_CT'
|
||||
);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use custom syncId when provided', () => {
|
||||
const viewports = {
|
||||
CT: [mockGridViewport],
|
||||
};
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
syncId: 'CUSTOM_SYNC_ID',
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'CUSTOM_SYNC_ID',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple viewports in same modality', () => {
|
||||
const mockGridViewport2 = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-2',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-2'],
|
||||
};
|
||||
|
||||
const viewports = {
|
||||
CT: [mockGridViewport, mockGridViewport2],
|
||||
};
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport
|
||||
.mockReturnValueOnce(mockViewport)
|
||||
.mockReturnValueOnce(mockViewport2);
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledTimes(2);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'viewport-2',
|
||||
'rendering-engine-2',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle multiple modalities separately', () => {
|
||||
const mockGridViewportMR = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-mr',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-mr'],
|
||||
};
|
||||
|
||||
const mockViewportMR = {
|
||||
id: 'viewport-mr',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-mr' })),
|
||||
};
|
||||
|
||||
const viewports = {
|
||||
CT: [mockGridViewport],
|
||||
MR: [mockGridViewportMR],
|
||||
};
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport
|
||||
.mockReturnValueOnce(mockViewport)
|
||||
.mockReturnValueOnce(mockViewportMR);
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledTimes(2);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'viewport-mr',
|
||||
'rendering-engine-mr',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_MR',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip viewport when cornerstone viewport is not found', () => {
|
||||
const viewports = {
|
||||
CT: [mockGridViewport],
|
||||
};
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport.mockReturnValue(null);
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should generate viewports from grid service when not provided', () => {
|
||||
const mockViewportsMap = new Map([
|
||||
[
|
||||
'viewport-1',
|
||||
{
|
||||
...mockGridViewport,
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const mockState = {
|
||||
viewports: mockViewportsMap,
|
||||
};
|
||||
|
||||
mockViewportGridService.getState.mockReturnValue(mockState);
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports: null,
|
||||
});
|
||||
|
||||
expect(mockViewportGridService.getState).toHaveBeenCalled();
|
||||
expect(mockDisplaySetService.getDisplaySetByUID).toHaveBeenCalledWith('displaySet-1');
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle sync state with different id not matching', () => {
|
||||
const viewports = {
|
||||
CT: [mockGridViewport],
|
||||
};
|
||||
|
||||
const mockSyncState = {
|
||||
id: 'DIFFERENT_SYNC_ID',
|
||||
};
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([mockSyncState]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle mixed sync states where some match and some do not', () => {
|
||||
const mockGridViewport2 = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-2',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-2'],
|
||||
};
|
||||
|
||||
const viewports = {
|
||||
CT: [mockGridViewport, mockGridViewport2],
|
||||
};
|
||||
|
||||
const mockSyncState = {
|
||||
id: 'VOI_SYNC_CT',
|
||||
};
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport
|
||||
.mockReturnValueOnce([mockSyncState])
|
||||
.mockReturnValueOnce([]);
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport
|
||||
.mockReturnValueOnce(mockViewport)
|
||||
.mockReturnValueOnce(mockViewport2);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
'VOI_SYNC_CT'
|
||||
);
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-2',
|
||||
'rendering-engine-2',
|
||||
'VOI_SYNC_CT'
|
||||
);
|
||||
});
|
||||
|
||||
it('should disable sync for multiple viewports when sync exists', () => {
|
||||
const mockGridViewport2 = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-2',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-2'],
|
||||
};
|
||||
|
||||
const mockViewport2 = {
|
||||
id: 'viewport-2',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-2' })),
|
||||
};
|
||||
|
||||
const viewports = {
|
||||
CT: [mockGridViewport, mockGridViewport2],
|
||||
};
|
||||
|
||||
const mockSyncState = {
|
||||
id: 'VOI_SYNC_CT',
|
||||
};
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport
|
||||
.mockReturnValueOnce(mockViewport)
|
||||
.mockReturnValueOnce(mockViewport2);
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([mockSyncState]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenCalledTimes(2);
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
'VOI_SYNC_CT'
|
||||
);
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'viewport-2',
|
||||
'rendering-engine-2',
|
||||
'VOI_SYNC_CT'
|
||||
);
|
||||
});
|
||||
|
||||
it('should skip viewport during disable when cornerstone viewport is not found', () => {
|
||||
const mockGridViewport2 = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-2',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-2'],
|
||||
};
|
||||
|
||||
const viewports = {
|
||||
CT: [mockGridViewport, mockGridViewport2],
|
||||
};
|
||||
|
||||
const mockSyncState = {
|
||||
id: 'VOI_SYNC_CT',
|
||||
};
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport
|
||||
.mockReturnValueOnce(mockViewport)
|
||||
.mockReturnValueOnce(null);
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([mockSyncState]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
'VOI_SYNC_CT'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle empty viewports object', () => {
|
||||
const viewports = {};
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.getSynchronizersForViewport).not.toHaveBeenCalled();
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).not.toHaveBeenCalled();
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle modality with empty viewport array', () => {
|
||||
const viewports = {
|
||||
CT: [],
|
||||
};
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.getSynchronizersForViewport).not.toHaveBeenCalled();
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).not.toHaveBeenCalled();
|
||||
expect(mockSyncGroupService.removeViewportFromSyncGroup).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle multiple displaySetInstanceUIDs by using first one', () => {
|
||||
const mockGridViewportMultipleDisplaySets = {
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-1',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-1', 'displaySet-2', 'displaySet-3'],
|
||||
};
|
||||
|
||||
mockViewportGridService.getState.mockReturnValue({
|
||||
viewports: new Map([['viewport-1', mockGridViewportMultipleDisplaySets]]),
|
||||
});
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
mockDisplaySetService.getDisplaySetByUID.mockReturnValue(mockDisplaySet);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
});
|
||||
|
||||
expect(mockDisplaySetService.getDisplaySetByUID).toHaveBeenCalledWith('displaySet-1');
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledWith(
|
||||
'viewport-1',
|
||||
'rendering-engine-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should group viewports by modality correctly when generating from grid', () => {
|
||||
const mockViewportsMap = new Map([
|
||||
[
|
||||
'viewport-ct-1',
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-ct-1',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-ct-1'],
|
||||
},
|
||||
],
|
||||
[
|
||||
'viewport-ct-2',
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-ct-2',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-ct-2'],
|
||||
},
|
||||
],
|
||||
[
|
||||
'viewport-mr-1',
|
||||
{
|
||||
viewportOptions: {
|
||||
viewportId: 'viewport-mr-1',
|
||||
},
|
||||
displaySetInstanceUIDs: ['displaySet-mr-1'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const mockState = {
|
||||
viewports: mockViewportsMap,
|
||||
};
|
||||
|
||||
const mockDisplaySetCT = { Modality: 'CT' };
|
||||
const mockDisplaySetMR = { Modality: 'MR' };
|
||||
const mockViewportCT1 = {
|
||||
id: 'viewport-ct-1',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-ct-1' })),
|
||||
};
|
||||
const mockViewportCT2 = {
|
||||
id: 'viewport-ct-2',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-ct-2' })),
|
||||
};
|
||||
const mockViewportMR1 = {
|
||||
id: 'viewport-mr-1',
|
||||
getRenderingEngine: jest.fn(() => ({ id: 'rendering-engine-mr-1' })),
|
||||
};
|
||||
|
||||
mockViewportGridService.getState.mockReturnValue(mockState);
|
||||
mockDisplaySetService.getDisplaySetByUID
|
||||
.mockReturnValueOnce(mockDisplaySetCT)
|
||||
.mockReturnValueOnce(mockDisplaySetCT)
|
||||
.mockReturnValueOnce(mockDisplaySetMR);
|
||||
|
||||
mockCornerstoneViewportService.getCornerstoneViewport
|
||||
.mockReturnValueOnce(mockViewportCT1)
|
||||
.mockReturnValueOnce(mockViewportCT2)
|
||||
.mockReturnValueOnce(mockViewportMR1);
|
||||
|
||||
mockSyncGroupService.getSynchronizersForViewport.mockReturnValue([]);
|
||||
|
||||
toggleVOISliceSync({
|
||||
...defaultParameters,
|
||||
viewports: null,
|
||||
});
|
||||
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenCalledTimes(3);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'viewport-ct-1',
|
||||
'rendering-engine-ct-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'viewport-ct-2',
|
||||
'rendering-engine-ct-2',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_CT',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
expect(mockSyncGroupService.addViewportToSyncGroup).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'viewport-mr-1',
|
||||
'rendering-engine-mr-1',
|
||||
{
|
||||
type: 'voi',
|
||||
id: 'VOI_SYNC_MR',
|
||||
source: true,
|
||||
target: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -46,7 +46,7 @@ export default function toggleVOISliceSync({
|
||||
}
|
||||
syncGroupService.addViewportToSyncGroup(viewportId, viewport.getRenderingEngine().id, {
|
||||
type: 'voi',
|
||||
id: syncIdToUse,
|
||||
id: syncIdToUse as string,
|
||||
source: true,
|
||||
target: true,
|
||||
});
|
||||
@ -76,12 +76,12 @@ function groupViewportsByModality(
|
||||
viewportGridService: ViewportGridService,
|
||||
displaySetService: DisplaySetService
|
||||
) {
|
||||
let { viewports } = viewportGridService.getState();
|
||||
const { viewports } = viewportGridService.getState();
|
||||
|
||||
viewports = [...viewports.values()];
|
||||
const viewportsArray = [...viewports.values()];
|
||||
|
||||
// group the viewports by modality
|
||||
return viewports.reduce((acc, viewport) => {
|
||||
return viewportsArray.reduce((acc, viewport) => {
|
||||
const { displaySetInstanceUIDs } = viewport;
|
||||
// Todo: add proper fusion support
|
||||
const displaySetInstanceUID = displaySetInstanceUIDs[0];
|
||||
|
||||
942
extensions/cornerstone/src/utils/updateSegmentationStats.test.ts
Normal file
942
extensions/cornerstone/src/utils/updateSegmentationStats.test.ts
Normal file
@ -0,0 +1,942 @@
|
||||
import { Types as cstTypes, utilities as cstUtilities } from '@cornerstonejs/tools';
|
||||
import {
|
||||
updateSegmentationStats,
|
||||
updateSegmentBidirectionalStats,
|
||||
} from './updateSegmentationStats';
|
||||
|
||||
jest.mock('@cornerstonejs/tools', () => ({
|
||||
utilities: {
|
||||
segmentation: {
|
||||
getStatistics: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('updateSegmentationStats', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
'2': {
|
||||
segmentIndex: 2,
|
||||
label: 'Segment 2',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockReadableText = {
|
||||
mean: 'Mean',
|
||||
stdDev: 'Standard Deviation',
|
||||
volume: 'Volume',
|
||||
max: 'Maximum',
|
||||
};
|
||||
|
||||
const mockStats = {
|
||||
'1': {
|
||||
array: [
|
||||
{
|
||||
name: 'mean',
|
||||
value: 100.5,
|
||||
unit: 'HU',
|
||||
},
|
||||
],
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return null if no segmentation is provided', async () => {
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: null,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(cstUtilities.segmentation.getStatistics).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if segmentation is undefined', async () => {
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: undefined,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if no segments found (empty segments)', async () => {
|
||||
const segmentationWithEmptySegments = {
|
||||
segments: {},
|
||||
};
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: segmentationWithEmptySegments,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(cstUtilities.segmentation.getStatistics).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if no segments found (only segment 0)', async () => {
|
||||
const segmentationWithOnlyBackground = {
|
||||
segments: {
|
||||
'0': {
|
||||
segmentIndex: 0,
|
||||
label: 'Background',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: segmentationWithOnlyBackground,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(cstUtilities.segmentation.getStatistics).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null if getStatistics returns null', async () => {
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(null);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(cstUtilities.segmentation.getStatistics).toHaveBeenCalledWith({
|
||||
segmentationId: 'test-id',
|
||||
segmentIndices: [1, 2],
|
||||
mode: 'individual',
|
||||
});
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null if getStatistics returns undefined', async () => {
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(undefined);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should initialize cachedStats if not present', async () => {
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStats);
|
||||
|
||||
const segmentationWithoutCachedStats = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: segmentationWithoutCachedStats,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats).toBeDefined();
|
||||
expect(result.segments['1'].cachedStats.namedStats).toBeDefined();
|
||||
expect(result.segments['1'].cachedStats.namedStats.mean).toEqual({
|
||||
name: mockStats['1'].array[0].name,
|
||||
label: mockReadableText.mean,
|
||||
value: mockStats['1'].array[0].value,
|
||||
unit: mockStats['1'].array[0].unit,
|
||||
order: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing cachedStats and add namedStats', async () => {
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStats);
|
||||
|
||||
const segmentationWithExistingCachedStats = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
existingData: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: segmentationWithExistingCachedStats,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.existingData).toBe('test');
|
||||
expect(result.segments['1'].cachedStats.namedStats.mean).toEqual({
|
||||
name: mockStats['1'].array[0].name,
|
||||
label: mockReadableText.mean,
|
||||
value: mockStats['1'].array[0].value,
|
||||
unit: mockStats['1'].array[0].unit,
|
||||
order: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing namedStats and merge with new stats', async () => {
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStats);
|
||||
|
||||
const segmentationWithExistingNamedStats = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
existingStat: {
|
||||
name: 'existingStat',
|
||||
value: 'existing',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: segmentationWithExistingNamedStats,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.existingStat).toEqual({
|
||||
name: 'existingStat',
|
||||
value: 'existing',
|
||||
});
|
||||
expect(result.segments['1'].cachedStats.namedStats.mean).toEqual({
|
||||
name: mockStats['1'].array[0].name,
|
||||
label: mockReadableText.mean,
|
||||
value: mockStats['1'].array[0].value,
|
||||
unit: mockStats['1'].array[0].unit,
|
||||
order: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it('should skip stats not in readableText', async () => {
|
||||
const mockStatsWithUnknownStat = {
|
||||
'1': {
|
||||
array: [
|
||||
...mockStats['1'].array,
|
||||
{
|
||||
name: 'unknownStat',
|
||||
value: 50,
|
||||
unit: 'mm',
|
||||
},
|
||||
],
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(cstUtilities.segmentation, 'getStatistics')
|
||||
.mockResolvedValue(mockStatsWithUnknownStat);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.mean).toBeDefined();
|
||||
expect(result.segments['1'].cachedStats.namedStats.unknownStat).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should skip stats with invalid name', async () => {
|
||||
const mockStatsWithInvalidName = {
|
||||
'1': {
|
||||
array: [
|
||||
...mockStats['1'].array,
|
||||
{
|
||||
name: null,
|
||||
value: 50,
|
||||
unit: 'mm',
|
||||
},
|
||||
{
|
||||
value: 25,
|
||||
unit: 'cm',
|
||||
},
|
||||
],
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(cstUtilities.segmentation, 'getStatistics')
|
||||
.mockResolvedValue(mockStatsWithInvalidName);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.mean).toBeDefined();
|
||||
expect(Object.keys(result.segments['1'].cachedStats.namedStats)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should add volume stat when volume exists in segmentStats and readableText', async () => {
|
||||
console.log('mockSegmentation', mockSegmentation);
|
||||
|
||||
const mockStatsWithVolume = {
|
||||
'1': {
|
||||
array: [...mockStats['1'].array],
|
||||
volume: {
|
||||
value: 250.75,
|
||||
unit: 'mm³',
|
||||
},
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStatsWithVolume);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.volume).toEqual({
|
||||
name: 'volume',
|
||||
label: mockReadableText.volume,
|
||||
value: mockStatsWithVolume['1'].volume.value,
|
||||
unit: mockStatsWithVolume['1'].volume.unit,
|
||||
order: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add volume stat when volume not in readableText', async () => {
|
||||
console.log('mockSegmentation', mockSegmentation);
|
||||
|
||||
const mockStatsWithVolume = {
|
||||
'1': {
|
||||
array: [...mockStats['1'].array],
|
||||
volume: {
|
||||
value: 250.75,
|
||||
unit: 'mm³',
|
||||
},
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStatsWithVolume);
|
||||
|
||||
const readableTextWithoutVolume = {
|
||||
mean: 'Mean',
|
||||
stdDev: 'Standard Deviation',
|
||||
};
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: readableTextWithoutVolume,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.volume).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not add volume stat when volume already exists in namedStats', async () => {
|
||||
const mockStatsWithVolume = {
|
||||
'1': {
|
||||
array: [
|
||||
{
|
||||
name: 'volume',
|
||||
value: 200.5,
|
||||
unit: 'mm³',
|
||||
},
|
||||
],
|
||||
volume: {
|
||||
value: 250.75,
|
||||
unit: 'mm³',
|
||||
},
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStatsWithVolume);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.volume).toEqual({
|
||||
name: 'volume',
|
||||
label: mockReadableText.volume,
|
||||
value: mockStatsWithVolume['1'].array[0].value,
|
||||
unit: mockStatsWithVolume['1'].volume.unit,
|
||||
order: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not add volume stat when segmentStats.volume is missing', async () => {
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStats);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.volume).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should handle multiple segments with different stats', async () => {
|
||||
const mockStatsWithMax = {
|
||||
'1': {
|
||||
array: [
|
||||
...mockStats['1'].array,
|
||||
{
|
||||
name: 'max',
|
||||
value: 200,
|
||||
unit: 'HU',
|
||||
},
|
||||
],
|
||||
} as cstTypes.NamedStatistics,
|
||||
'2': {
|
||||
array: [
|
||||
{
|
||||
name: 'stdDev',
|
||||
value: 15.2,
|
||||
unit: 'HU',
|
||||
},
|
||||
],
|
||||
volume: {
|
||||
value: 150.25,
|
||||
unit: 'mm³',
|
||||
},
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStatsWithMax);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats.mean).toEqual({
|
||||
name: mockStatsWithMax['1'].array[0].name,
|
||||
label: mockReadableText.mean,
|
||||
value: mockStatsWithMax['1'].array[0].value,
|
||||
unit: mockStatsWithMax['1'].array[0].unit,
|
||||
order: 0,
|
||||
});
|
||||
expect(result.segments['1'].cachedStats.namedStats.max).toEqual({
|
||||
name: mockStatsWithMax['1'].array[1].name,
|
||||
label: mockReadableText.max,
|
||||
value: mockStatsWithMax['1'].array[1].value,
|
||||
unit: mockStatsWithMax['1'].array[1].unit,
|
||||
order: 3,
|
||||
});
|
||||
expect(result.segments['2'].cachedStats.namedStats.stdDev).toEqual({
|
||||
name: mockStatsWithMax['2'].array[0].name,
|
||||
label: mockReadableText.stdDev,
|
||||
value: mockStatsWithMax['2'].array[0].value,
|
||||
unit: mockStatsWithMax['2'].array[0].unit,
|
||||
order: 1,
|
||||
});
|
||||
expect(result.segments['2'].cachedStats.namedStats.volume).toEqual({
|
||||
name: 'volume',
|
||||
label: mockReadableText.volume,
|
||||
value: mockStatsWithMax['2'].volume.value,
|
||||
unit: mockStatsWithMax['2'].volume.unit,
|
||||
order: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should add empty namedStats when no stats are returned(empty array)', async () => {
|
||||
const mockStatsWithEmptyArray = {
|
||||
'1': {
|
||||
array: [],
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(cstUtilities.segmentation, 'getStatistics')
|
||||
.mockResolvedValue(mockStatsWithEmptyArray);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats).toEqual({});
|
||||
});
|
||||
|
||||
it('should only initialize namedStats when no matching stats are found', async () => {
|
||||
const mockStatsWithUnknownStat = {
|
||||
'1': {
|
||||
array: [
|
||||
{
|
||||
name: 'unknownStat',
|
||||
value: 100,
|
||||
unit: 'unit',
|
||||
},
|
||||
],
|
||||
} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(cstUtilities.segmentation, 'getStatistics')
|
||||
.mockResolvedValue(mockStatsWithUnknownStat);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats.namedStats).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle segments without array property by initializing cachedStats', async () => {
|
||||
const mockStatsWithNoArray = {
|
||||
'1': {} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStatsWithNoArray);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentation,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result.segments['1'].cachedStats).toEqual({});
|
||||
});
|
||||
|
||||
it('should not update segment if cachedStats is already initialized and no matching stats are found', async () => {
|
||||
const mockSegmentationWithCachedStats = {
|
||||
segments: {
|
||||
'1': {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
existingStat: {
|
||||
name: 'existing',
|
||||
value: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const mockStatsWithNoArray = {
|
||||
'1': {} as cstTypes.NamedStatistics,
|
||||
};
|
||||
|
||||
jest.spyOn(cstUtilities.segmentation, 'getStatistics').mockResolvedValue(mockStatsWithNoArray);
|
||||
|
||||
const result = await updateSegmentationStats({
|
||||
segmentation: mockSegmentationWithCachedStats,
|
||||
segmentationId: 'test-id',
|
||||
readableText: mockReadableText,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSegmentBidirectionalStats', () => {
|
||||
const mockSegmentationService = {
|
||||
getSegmentation: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAnnotation = {
|
||||
annotationUID: 'test-annotation-uid',
|
||||
};
|
||||
|
||||
const mockBidirectionalData = {
|
||||
majorAxis: { length: 20.5 },
|
||||
minorAxis: { length: 15.3 },
|
||||
maxMajor: 20.5,
|
||||
maxMinor: 15.3,
|
||||
};
|
||||
|
||||
const defaultParameters = {
|
||||
segmentationId: 'test-id',
|
||||
segmentIndex: 1,
|
||||
bidirectionalData: mockBidirectionalData,
|
||||
// @ts-expect-error - only part of the SegmentationService is needed
|
||||
segmentationService: mockSegmentationService as AppTypes.SegmentationService,
|
||||
annotation: mockAnnotation,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return null when segmentationId is missing', () => {
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
segmentationId: '',
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
expect(mockSegmentationService.getSegmentation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return null when segmentationId is null', () => {
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
segmentationId: null,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when segmentIndex is undefined', () => {
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
segmentIndex: undefined,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when bidirectionalData is missing', () => {
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: null,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when segmentation is not found', () => {
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(null);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
});
|
||||
|
||||
expect(mockSegmentationService.getSegmentation).toHaveBeenCalledWith('test-id');
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when segment is not found', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when majorAxis is missing', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bidirectionalDataWithoutMajorAxis = {
|
||||
majorAxis: null,
|
||||
minorAxis: { length: 15.3 },
|
||||
maxMajor: 20.5,
|
||||
maxMinor: 15.3,
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: bidirectionalDataWithoutMajorAxis,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when minorAxis is missing', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bidirectionalDataWithoutMinorAxis = {
|
||||
majorAxis: { length: 20.5 },
|
||||
minorAxis: null,
|
||||
maxMajor: 20.5,
|
||||
maxMinor: 15.3,
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: bidirectionalDataWithoutMinorAxis,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when maxMajor is zero', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bidirectionalDataWithZeroMajor = {
|
||||
majorAxis: { length: 20.5 },
|
||||
minorAxis: { length: 15.3 },
|
||||
maxMajor: 0,
|
||||
maxMinor: 15.3,
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: bidirectionalDataWithZeroMajor,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when maxMinor is zero', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bidirectionalDataWithZeroMinor = {
|
||||
majorAxis: { length: 20.5 },
|
||||
minorAxis: { length: 15.3 },
|
||||
maxMajor: 20.5,
|
||||
maxMinor: 0,
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: bidirectionalDataWithZeroMinor,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should return null when maxMajor is negative', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const bidirectionalDataWithNegativeMajor = {
|
||||
majorAxis: { length: 20.5 },
|
||||
minorAxis: { length: 15.3 },
|
||||
maxMajor: -5,
|
||||
maxMinor: 15.3,
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: bidirectionalDataWithNegativeMajor,
|
||||
});
|
||||
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
|
||||
it('should initialize cachedStats when segment has no cachedStats', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
});
|
||||
|
||||
expect(result.segments[1].cachedStats).toEqual({
|
||||
namedStats: {
|
||||
bidirectional: {
|
||||
name: 'bidirectional',
|
||||
label: 'Bidirectional',
|
||||
annotationUID: 'test-annotation-uid',
|
||||
value: {
|
||||
...mockBidirectionalData,
|
||||
},
|
||||
unit: 'mm',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should initialize namedStats when segment has cachedStats but no namedStats', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
existingData: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
});
|
||||
|
||||
expect(result.segments[1].cachedStats.existingData).toBe('test');
|
||||
expect(result.segments[1].cachedStats.namedStats).toEqual({
|
||||
bidirectional: {
|
||||
name: 'bidirectional',
|
||||
label: 'Bidirectional',
|
||||
annotationUID: 'test-annotation-uid',
|
||||
value: {
|
||||
...mockBidirectionalData,
|
||||
},
|
||||
unit: 'mm',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should add bidirectional stats to existing namedStats', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
namedStats: {
|
||||
existingStat: {
|
||||
name: 'existing',
|
||||
value: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
});
|
||||
|
||||
expect(
|
||||
(result.segments[1].cachedStats.namedStats as { existingStat: cstTypes.NamedStatistics })
|
||||
.existingStat
|
||||
).toEqual({
|
||||
name: 'existing',
|
||||
value: 'test',
|
||||
});
|
||||
expect(
|
||||
(result.segments[1].cachedStats.namedStats as { bidirectional: cstTypes.NamedStatistics })
|
||||
.bidirectional
|
||||
).toEqual({
|
||||
name: 'bidirectional',
|
||||
label: 'Bidirectional',
|
||||
annotationUID: 'test-annotation-uid',
|
||||
value: {
|
||||
...mockBidirectionalData,
|
||||
},
|
||||
unit: 'mm',
|
||||
});
|
||||
});
|
||||
|
||||
it('should update bidirectional stats with correct data structure', () => {
|
||||
const mockSegmentation = {
|
||||
segments: {
|
||||
1: {
|
||||
segmentIndex: 1,
|
||||
label: 'Segment 1',
|
||||
cachedStats: {
|
||||
namedStats: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const customBidirectionalData = {
|
||||
majorAxis: { length: 25.7 },
|
||||
minorAxis: { length: 18.9 },
|
||||
maxMajor: 25.7,
|
||||
maxMinor: 18.9,
|
||||
};
|
||||
|
||||
const customAnnotation = {
|
||||
annotationUID: 'custom-annotation-uid',
|
||||
};
|
||||
|
||||
mockSegmentationService.getSegmentation.mockReturnValue(mockSegmentation);
|
||||
|
||||
const result = updateSegmentBidirectionalStats({
|
||||
...defaultParameters,
|
||||
bidirectionalData: customBidirectionalData,
|
||||
annotation: customAnnotation,
|
||||
});
|
||||
|
||||
expect(
|
||||
(result.segments[1].cachedStats.namedStats as { bidirectional: cstTypes.NamedStatistics })
|
||||
.bidirectional
|
||||
).toEqual({
|
||||
name: 'bidirectional',
|
||||
label: 'Bidirectional',
|
||||
annotationUID: 'custom-annotation-uid',
|
||||
value: {
|
||||
...customBidirectionalData,
|
||||
},
|
||||
unit: 'mm',
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,5 @@
|
||||
import * as cornerstoneTools from '@cornerstonejs/tools';
|
||||
import cloneDeep from 'lodash.clonedeep';
|
||||
|
||||
interface BidirectionalAxis {
|
||||
length: number;
|
||||
@ -51,7 +52,7 @@ export async function updateSegmentationStats({
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatedSegmentation = { ...segmentation };
|
||||
const updatedSegmentation = cloneDeep(segmentation);
|
||||
let hasUpdates = false;
|
||||
|
||||
// Loop through each segment's stats
|
||||
|
||||
Loading…
Reference in New Issue
Block a user