test(CornerstoneUtils): Adding unit tests for most of Cornerstone Extension utilities (#5454)

This commit is contained in:
Vinícius Alves de Faria Resende 2025-10-07 12:42:47 -03:00 committed by GitHub
parent 44beac799f
commit 3c421f124e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 8743 additions and 66 deletions

View 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);
});
});

View File

@ -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/)
);
});
});

View File

@ -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);
});
});

View 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],
},
});
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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 },
]);
});
});

View File

@ -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 },
];

View 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);
});
});
});

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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();
});
});

View 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);
});
});

View 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();
});
});

View File

@ -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: '',
})
);
});
});

View 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]);
});
});

View File

@ -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;
}

View 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);
});
});

View 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);
});
});

View File

@ -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

View File

@ -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);
});
});

View 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);
});
});

View 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);
});
});

View 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;
});
});

View File

@ -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';

View File

@ -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();
});
});

View 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'
);
});
});

View 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');
});
});

View File

@ -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);

View File

@ -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();
});
});

View 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,
}
);
});
});

View File

@ -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];

View 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',
});
});
});

View File

@ -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