import { cache, Enums as csEnums, eventTarget, geometryLoader, getEnabledElementByViewportId, imageLoader, Types as csTypes, metaData, } from '@cornerstonejs/core'; import { ViewportType } from '@cornerstonejs/core/enums'; import { Enums as csToolsEnums, segmentation as cstSegmentation, Types as cstTypes, } from '@cornerstonejs/tools'; import { EasingFunctionEnum, EasingFunctionMap } from '../../utils/transitions'; import * as MapROIContoursToRTStructData from './RTSTRUCT/mapROIContoursToRTStructData'; import SegmentationServiceClass, { SegmentationRepresentation } from './SegmentationService'; import { setNextViewportsEnabled } from '../../utils/nextViewports'; jest.mock('@cornerstonejs/core', () => ({ ...jest.requireActual('@cornerstonejs/core'), getEnabledElementByViewportId: jest.fn(), eventTarget: { addEventListener: jest.fn(), removeEventListener: jest.fn(), }, })); jest.mock('@cornerstonejs/tools', () => ({ ...jest.requireActual('@cornerstonejs/tools'), segmentation: { ...jest.requireActual('@cornerstonejs/tools').segmentation, activeSegmentation: { getActiveSegmentation: jest.fn(), setActiveSegmentation: jest.fn(), }, addSegmentations: jest.fn(), config: { color: { getSegmentIndexColor: jest.fn(), setSegmentIndexColor: jest.fn(), }, visibility: { getHiddenSegmentIndices: jest.fn(), getSegmentIndexVisibility: jest.fn(), setSegmentIndexVisibility: jest.fn(), setSegmentationRepresentationVisibility: jest.fn(), }, style: { hasCustomStyle: jest.fn(), getRenderInactiveSegmentations: jest.fn(), getStyle: jest.fn(), resetToGlobalStyle: jest.fn(), setRenderInactiveSegmentations: jest.fn(), setStyle: jest.fn(), }, }, getLabelmapImageIds: jest.fn(), helpers: { convertStackToVolumeLabelmap: jest.fn() }, removeSegment: jest.fn(), removeSegmentationRepresentations: jest.fn(), segmentIndex: { setActiveSegmentIndex: jest.fn(), }, segmentLocking: { isSegmentIndexLocked: jest.fn(), setSegmentIndexLocked: jest.fn(), }, state: { addColorLUT: jest.fn(), getSegmentation: jest.fn(), getSegmentations: jest.fn(), getSegmentationRepresentationsBySegmentationId: jest.fn(), getSegmentationRepresentations: jest.fn(), getViewportIdsWithSegmentation: jest.fn(), removeAllSegmentations: jest.fn(), removeSegmentation: jest.fn(), updateLabelmapSegmentationImageReferences: jest.fn(), }, triggerSegmentationEvents: { triggerSegmentationRepresentationModified: jest.fn() }, }, })); const serviceManagerMock = { services: { cornerstoneViewportService: { getCornerstoneViewport: jest.fn(), }, displaySetService: { getDisplaySetByUID: jest.fn(), }, viewportGridService: { EVENTS: { GRID_STATE_CHANGED: 'event::gridStateChanged', }, getState: jest.fn(), setDisplaySetsForViewport: jest.fn(), subscribe: jest.fn(), }, }, }; describe('SegmentationService', () => { let service: SegmentationServiceClass; const viewportId = 'viewportId'; const mockCornerstoneRepresentations = [ { segmentationId: 'd7682642-c41d-abe5-3c78-716191336ab1', type: 'Labelmap', active: true, visible: true, colorLUTIndex: 0, segments: { '1': { visible: true }, }, config: { cfun: { nodes: [] }, ofun: { nodes: [] }, colorLUTIndex: 0, colorLUTOrIndex: 0, }, }, { segmentationId: 'd7682642-c41d-abe5-3c78-716191336ab2', type: 'Labelmap', active: true, visible: true, colorLUTIndex: 0, segments: { '1': { visible: true }, }, config: { cfun: { nodes: [] }, ofun: { nodes: [] }, colorLUTIndex: 0, colorLUTOrIndex: 0, }, }, ]; const mockCornerstoneSegmentation = { segmentationId: '1d6ce0c8-aeae-6890-1cc6-b39560866811', label: 'Segmentation', cachedStats: {}, segments: { '1': { active: true, cachedStats: {}, label: 'Segment 1', locked: false, segmentIndex: 1, }, }, representationData: { Labelmap: {} }, }; const mockVolumeCornerstoneSegmentation = { ...mockCornerstoneSegmentation, representationData: { Labelmap: { volumeId: 'volumeId' } }, }; const mockCornerstoneStackViewport = { element: { addEventListener: jest.fn(), removeEventListener: jest.fn(), }, id: viewportId, type: ViewportType.STACK, getFrameOfReferenceUID: jest.fn(), getViewPresentation: jest.fn(), getViewReference: jest.fn(), }; const mockCornerstoneVolumeViewport = { id: viewportId, type: ViewportType.VOLUME_3D, getFrameOfReferenceUID: jest.fn(), setViewPresentation: jest.fn(), setViewReference: jest.fn(), render: jest.fn(), }; const representations = [ { ...mockCornerstoneRepresentations[0], viewportId: 'viewportId', id: 'test-id', label: 'Test Segmentation', styles: {}, segments: { 1: { color: [255, 0, 0, 1], opacity: 1, segmentIndex: 1, visible: true, }, }, }, ] as SegmentationRepresentation[]; beforeEach(() => { service = new SegmentationServiceClass({ servicesManager: serviceManagerMock }); jest.clearAllMocks(); }); it('should instantiate the service properly', () => { expect(service).toBeDefined(); expect(service.servicesManager).toBe(serviceManagerMock); expect(service.EVENTS).toBeDefined(); }); it('should instantiate service through registration', () => { // @ts-expect-error - mock only has a subset of the properties const service = SegmentationServiceClass.REGISTRATION.create({ servicesManager: serviceManagerMock, }); expect(service).toBeDefined(); }); describe('onModeEnter', () => { it('should add event listeners', () => { service.onModeEnter(); expect(eventTarget.addEventListener).toHaveBeenCalledTimes(8); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_MODIFIED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REMOVED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_ADDED, expect.any(Function) ); expect(eventTarget.addEventListener).toHaveBeenCalledWith( csToolsEnums.Events.ANNOTATION_CUT_MERGE_PROCESS_COMPLETED, expect.any(Function) ); }); }); describe('onModeExit', () => { it('should remove event listeners', () => { jest.spyOn(service, 'reset'); service.onModeExit(); expect(eventTarget.removeEventListener).toHaveBeenCalledTimes(7); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_MODIFIED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REMOVED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_ADDED, expect.any(Function) ); expect(service.reset).toHaveBeenCalled(); }); }); describe('getSegmentation', () => { it('should call cornerstone to get specific segmentation', () => { service.getSegmentation('segmentationId'); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith('segmentationId'); }); }); describe('getSegmentations', () => { it('should call cornerstone to get all segmentations', () => { service.getSegmentations(); expect(cstSegmentation.state.getSegmentations).toHaveBeenCalled(); }); }); describe('getPresentation', () => { it('should properly retrieve the segmentation presentations', () => { jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValueOnce( mockCornerstoneRepresentations as cstTypes.SegmentationRepresentation[] ); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(cstSegmentation.config.color, 'getSegmentIndexColor') .mockReturnValue([0, 0, 0, 1]); jest .spyOn(cstSegmentation.config.visibility, 'getSegmentIndexVisibility') .mockReturnValue(true); jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({}); const returnedPresentations = service.getPresentation(viewportId); // config is empty due to _toOHIFSegmentationRepresentation returning empty config expect(returnedPresentations).toEqual([ { config: {}, hydrated: true, segmentationId: 'd7682642-c41d-abe5-3c78-716191336ab1', type: 'Labelmap', }, { config: {}, hydrated: true, segmentationId: 'd7682642-c41d-abe5-3c78-716191336ab2', type: 'Labelmap', }, ]); }); it('should ignore when representation is undefined', () => { jest.spyOn(service, 'getSegmentationRepresentations').mockReturnValueOnce([undefined]); const returnedPresentations = service.getPresentation(viewportId); expect(returnedPresentations).toEqual([]); }); }); describe('getRepresentationsForSegmentation', () => { it('should call cornerstone to get representations for a segmentation', () => { service.getRepresentationsForSegmentation('segmentationId'); expect( cstSegmentation.state.getSegmentationRepresentationsBySegmentationId ).toHaveBeenCalledWith('segmentationId'); }); }); describe('getSegmentationRepresentations', () => { it('should properly map cornerstone representations to OHIF representations', () => { jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValueOnce( mockCornerstoneRepresentations as cstTypes.SegmentationRepresentation[] ); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(cstSegmentation.config.color, 'getSegmentIndexColor') .mockReturnValue([0, 0, 0, 1]); jest .spyOn(cstSegmentation.config.visibility, 'getSegmentIndexVisibility') .mockReturnValue(true); jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({}); const returnedPresentations = service.getSegmentationRepresentations(viewportId); expect(returnedPresentations).toEqual([ { active: true, colorLUTIndex: 0, config: {}, id: 'd7682642-c41d-abe5-3c78-716191336ab1-Labelmap-viewportId', label: 'Segmentation', segmentationId: 'd7682642-c41d-abe5-3c78-716191336ab1', segments: { '1': { color: [0, 0, 0, 1], opacity: 1, segmentIndex: 1, visible: true, }, }, styles: {}, type: 'Labelmap', viewportId: 'viewportId', visible: true, }, { active: true, colorLUTIndex: 0, config: {}, id: 'd7682642-c41d-abe5-3c78-716191336ab2-Labelmap-viewportId', label: 'Segmentation', segmentationId: 'd7682642-c41d-abe5-3c78-716191336ab2', segments: { '1': { color: [0, 0, 0, 1], opacity: 1, segmentIndex: 1, visible: true, }, }, styles: {}, type: 'Labelmap', viewportId: 'viewportId', visible: true, }, ]); expect(cstSegmentation.state.getSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentationRepresentations).toHaveBeenCalledWith( viewportId, {} ); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(2); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith( mockCornerstoneRepresentations[0].segmentationId ); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith( mockCornerstoneRepresentations[1].segmentationId ); expect(cstSegmentation.config.color.getSegmentIndexColor).toHaveBeenCalledTimes(2); expect(cstSegmentation.config.color.getSegmentIndexColor).toHaveBeenCalledWith( viewportId, mockCornerstoneRepresentations[0].segmentationId, mockCornerstoneSegmentation.segments['1'].segmentIndex ); expect(cstSegmentation.config.color.getSegmentIndexColor).toHaveBeenCalledWith( viewportId, mockCornerstoneRepresentations[1].segmentationId, mockCornerstoneSegmentation.segments['1'].segmentIndex ); expect(cstSegmentation.config.visibility.getSegmentIndexVisibility).toHaveBeenCalledTimes(2); expect(cstSegmentation.config.visibility.getSegmentIndexVisibility).toHaveBeenCalledWith( viewportId, { segmentationId: mockCornerstoneRepresentations[0].segmentationId, type: mockCornerstoneRepresentations[0].type, }, mockCornerstoneSegmentation.segments['1'].segmentIndex ); expect(cstSegmentation.config.visibility.getSegmentIndexVisibility).toHaveBeenCalledWith( viewportId, { segmentationId: mockCornerstoneRepresentations[1].segmentationId, type: mockCornerstoneRepresentations[1].type, }, mockCornerstoneSegmentation.segments['1'].segmentIndex ); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledTimes(2); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledWith({ viewportId, segmentationId: mockCornerstoneRepresentations[0].segmentationId, type: 'Labelmap', }); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledWith({ viewportId, segmentationId: mockCornerstoneRepresentations[1].segmentationId, type: 'Labelmap', }); }); it('should throw an error if the segmentation is not found', () => { jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValueOnce( mockCornerstoneRepresentations as cstTypes.SegmentationRepresentation[] ); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(null as cstTypes.Segmentation); expect(() => service.getSegmentationRepresentations(viewportId)).toThrow( `Segmentation with ID ${mockCornerstoneRepresentations[0].segmentationId} not found.` ); }); it('should forward the specifier to the cornerstone getSegmentationRepresentations', () => { jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValueOnce( mockCornerstoneRepresentations as cstTypes.SegmentationRepresentation[] ); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(cstSegmentation.config.color, 'getSegmentIndexColor') .mockReturnValue([0, 0, 0, 1]); jest .spyOn(cstSegmentation.config.visibility, 'getSegmentIndexVisibility') .mockReturnValue(true); jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({}); service.getSegmentationRepresentations(viewportId, { segmentationId: mockCornerstoneRepresentations[0].segmentationId, type: mockCornerstoneRepresentations[0].type as csToolsEnums.SegmentationRepresentations, }); expect(cstSegmentation.state.getSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentationRepresentations).toHaveBeenCalledWith( viewportId, { segmentationId: mockCornerstoneRepresentations[0].segmentationId, type: mockCornerstoneRepresentations[0].type, } ); }); }); describe('destroy', () => { it('should remove event listeners and reset the service', () => { jest.spyOn(service, 'reset'); service.destroy(); expect(eventTarget.removeEventListener).toHaveBeenCalledTimes(7); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_MODIFIED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REMOVED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_DATA_MODIFIED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_MODIFIED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_ADDED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_REPRESENTATION_REMOVED, expect.any(Function) ); expect(eventTarget.removeEventListener).toHaveBeenCalledWith( csToolsEnums.Events.SEGMENTATION_ADDED, expect.any(Function) ); expect(service.reset).toHaveBeenCalled(); }); }); describe('addSegmentationRepresentation', () => { describe('stack viewport', () => { it('should add a non volume segmentation representation to stack viewport through Cornerstone updateLabelmapSegmentationImageReferences', async () => { jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneStackViewport as unknown as csTypes.IStackViewport); jest .spyOn(cstSegmentation.state, 'updateLabelmapSegmentationImageReferences') .mockReturnValue('labelmapImageId'); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, }); expect( cstSegmentation.state.updateLabelmapSegmentationImageReferences ).toHaveBeenCalledTimes(1); expect( cstSegmentation.state.updateLabelmapSegmentationImageReferences ).toHaveBeenCalledWith(viewportId, mockCornerstoneSegmentation.segmentationId); // this will be called directly because there's no OHIF conversion needed expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, // expected since _segmentationIdToColorLUTIndexMap wasn't previous set }, ]); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: mockCornerstoneSegmentation.segmentationId, }); }); it('should add a non volume segmentation representation to stack viewport through SegmentationService conversion attempt', async () => { const frameOfReferenceUID = 'frameOfReferenceUID'; const imageId = 'imageId'; const viewportGridServiceUnsubscribe = jest.fn(); const prevViewPresentation = {}; const prevViewReference = {}; mockCornerstoneStackViewport.getFrameOfReferenceUID.mockReturnValueOnce( frameOfReferenceUID ); mockCornerstoneStackViewport.getViewPresentation.mockReturnValueOnce(prevViewPresentation); mockCornerstoneStackViewport.getViewReference.mockReturnValueOnce(prevViewReference); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneStackViewport as unknown as csTypes.IStackViewport); jest .spyOn(cstSegmentation.state, 'updateLabelmapSegmentationImageReferences') .mockReturnValue(undefined); jest.spyOn(cstSegmentation, 'getLabelmapImageIds').mockReturnValue([imageId]); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); jest .spyOn(cache, 'getImage') .mockReturnValueOnce({ FrameOfReferenceUID: frameOfReferenceUID } as csTypes.IImage); jest.spyOn(serviceManagerMock.services.viewportGridService, 'getState').mockReturnValue({ viewports: new Map([ [ viewportId, { displaySetInstanceUIDs: ['displaySetInstanceUID'], viewportOptions: {} }, ], ]), }); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); jest.spyOn(serviceManagerMock.services.viewportGridService, 'subscribe').mockReturnValue({ unsubscribe: viewportGridServiceUnsubscribe, }); // awaiting here will wait for publishing of viewportGridService.EVENTS.GRID_STATE_CHANGED (deadlock avoidance) const serviceAddSegmentationRepresentationPromise = service.addSegmentationRepresentation( viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, } ); let _addSegmentationRepresentationCallback; const waitForSubscription = async () => { let remainingAttempts = 100; while (_addSegmentationRepresentationCallback === undefined && remainingAttempts > 0) { _addSegmentationRepresentationCallback = jest.mocked( serviceManagerMock.services.viewportGridService.subscribe ).mock.calls[0]?.[1]; await new Promise(resolve => resolve(void 0)); remainingAttempts--; } }; await waitForSubscription(); // trigger callback being awaited _addSegmentationRepresentationCallback(); await serviceAddSegmentationRepresentationPromise; expect(cstSegmentation.getLabelmapImageIds).toHaveBeenCalledTimes(1); expect(cstSegmentation.getLabelmapImageIds).toHaveBeenCalledWith( mockCornerstoneSegmentation.segmentationId ); expect(mockCornerstoneStackViewport.getFrameOfReferenceUID).toHaveBeenCalledTimes(1); expect(mockCornerstoneStackViewport.getFrameOfReferenceUID).toHaveBeenCalledWith(); expect(cache.getImage).toHaveBeenCalledTimes(1); expect(cache.getImage).toHaveBeenCalledWith(imageId); expect(serviceManagerMock.services.viewportGridService.getState).toHaveBeenCalledTimes(1); expect(serviceManagerMock.services.viewportGridService.getState).toHaveBeenCalledWith(); expect(mockCornerstoneStackViewport.getViewPresentation).toHaveBeenCalledTimes(1); expect(mockCornerstoneStackViewport.getViewPresentation).toHaveBeenCalledWith(); expect(mockCornerstoneStackViewport.getViewReference).toHaveBeenCalledTimes(1); expect(mockCornerstoneStackViewport.getViewReference).toHaveBeenCalledWith(); expect( serviceManagerMock.services.cornerstoneViewportService.getCornerstoneViewport ).toHaveBeenCalledTimes(2); expect( serviceManagerMock.services.cornerstoneViewportService.getCornerstoneViewport ).toHaveBeenCalledWith(viewportId); expect(mockCornerstoneStackViewport.element.addEventListener).toHaveBeenCalledTimes(1); expect(mockCornerstoneStackViewport.element.addEventListener).toHaveBeenCalledWith( csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, expect.any(Function) ); const volumeViewportNewVolumeHandlerCallback = jest.mocked( mockCornerstoneStackViewport.element.addEventListener ).mock.calls[0][1]; expect( serviceManagerMock.services.viewportGridService.setDisplaySetsForViewport ).toHaveBeenCalledTimes(1); expect( serviceManagerMock.services.viewportGridService.setDisplaySetsForViewport ).toHaveBeenCalledWith({ viewportId, displaySetInstanceUIDs: ['displaySetInstanceUID'], viewportOptions: { viewportType: ViewportType.ORTHOGRAPHIC }, }); expect( cstSegmentation.triggerSegmentationEvents.triggerSegmentationRepresentationModified ).toHaveBeenCalledTimes(1); expect( cstSegmentation.triggerSegmentationEvents.triggerSegmentationRepresentationModified ).toHaveBeenCalledWith( viewportId, mockCornerstoneSegmentation.segmentationId, csToolsEnums.SegmentationRepresentations.Labelmap ); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneVolumeViewport as unknown as csTypes.IVolumeViewport); volumeViewportNewVolumeHandlerCallback(); expect( serviceManagerMock.services.cornerstoneViewportService.getCornerstoneViewport ).toHaveBeenCalledTimes(3); expect( serviceManagerMock.services.cornerstoneViewportService.getCornerstoneViewport ).toHaveBeenCalledWith(viewportId); expect(mockCornerstoneVolumeViewport.setViewPresentation).toHaveBeenCalledTimes(1); expect(mockCornerstoneVolumeViewport.setViewPresentation).toHaveBeenCalledWith( prevViewPresentation ); expect(mockCornerstoneVolumeViewport.setViewReference).toHaveBeenCalledTimes(1); expect(mockCornerstoneVolumeViewport.setViewReference).toHaveBeenCalledWith( prevViewReference ); expect(mockCornerstoneVolumeViewport.render).toHaveBeenCalledTimes(1); expect(mockCornerstoneVolumeViewport.render).toHaveBeenCalledWith(); expect(mockCornerstoneStackViewport.element.removeEventListener).toHaveBeenCalledTimes(1); expect(mockCornerstoneStackViewport.element.removeEventListener).toHaveBeenCalledWith( csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, expect.any(Function) ); expect(serviceManagerMock.services.viewportGridService.subscribe).toHaveBeenCalledTimes(1); expect(serviceManagerMock.services.viewportGridService.subscribe).toHaveBeenCalledWith( serviceManagerMock.services.viewportGridService.EVENTS.GRID_STATE_CHANGED, expect.any(Function) ); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); expect(viewportGridServiceUnsubscribe).toHaveBeenCalledTimes(1); expect(viewportGridServiceUnsubscribe).toHaveBeenCalledWith(); }); it('should add a volume segmentation representation to stack viewport through SegmentationService conversion', async () => { mockCornerstoneStackViewport.getViewPresentation.mockReturnValueOnce({}); mockCornerstoneStackViewport.getViewReference.mockReturnValueOnce({}); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockVolumeCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneStackViewport as unknown as csTypes.IStackViewport); jest.spyOn(serviceManagerMock.services.viewportGridService, 'getState').mockReturnValue({ viewports: new Map([ [ viewportId, { displaySetInstanceUIDs: ['displaySetInstanceUID'], viewportOptions: {} }, ], ]), }); // awaiting here will wait for publishing of viewportGridService.EVENTS.GRID_STATE_CHANGED (deadlock avoidance) const serviceAddSegmentationRepresentationPromise = service.addSegmentationRepresentation( viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, } ); let _addSegmentationRepresentationCallback; const waitForSubscription = async () => { let remainingAttempts = 100; while (_addSegmentationRepresentationCallback === undefined && remainingAttempts > 0) { _addSegmentationRepresentationCallback = jest.mocked( serviceManagerMock.services.viewportGridService.subscribe ).mock.calls[0]?.[1]; await new Promise(resolve => resolve(void 0)); remainingAttempts--; } }; await waitForSubscription(); // trigger callback being awaited _addSegmentationRepresentationCallback(); await serviceAddSegmentationRepresentationPromise; // guarantee the early exit at handleStackViewportCase (isVolumeSegmentation) expect( cstSegmentation.state.updateLabelmapSegmentationImageReferences ).not.toHaveBeenCalled(); expect( serviceManagerMock.services.viewportGridService.setDisplaySetsForViewport ).toHaveBeenCalledTimes(1); expect( serviceManagerMock.services.viewportGridService.setDisplaySetsForViewport ).toHaveBeenCalledWith({ viewportId, displaySetInstanceUIDs: ['displaySetInstanceUID'], viewportOptions: { viewportType: ViewportType.ORTHOGRAPHIC }, }); const volumeViewportNewVolumeHandlerCallback = jest.mocked( mockCornerstoneStackViewport.element.addEventListener ).mock.calls[0][1]; jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneVolumeViewport as unknown as csTypes.IVolumeViewport); volumeViewportNewVolumeHandlerCallback(); expect( serviceManagerMock.services.cornerstoneViewportService.getCornerstoneViewport ).toHaveBeenCalledTimes(3); expect( serviceManagerMock.services.cornerstoneViewportService.getCornerstoneViewport ).toHaveBeenCalledWith(viewportId); expect(mockCornerstoneStackViewport.element.removeEventListener).toHaveBeenCalledTimes(1); expect(mockCornerstoneStackViewport.element.removeEventListener).toHaveBeenCalledWith( csEnums.Events.VOLUME_VIEWPORT_NEW_VOLUME, expect.any(Function) ); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); }); }); describe('next (generic) viewport', () => { // A native GenericViewport (raw PlanarViewport) exposes setDisplaySets / // setDisplaySetPresentation / setViewState, so csUtils.isGenericViewport is // true and addSegmentationRepresentation routes to NextSegmentationBackend. // It deliberately has NO getViewPresentation: the native path must never reach // convertStackToVolumeViewport (the source of the observed // "getViewPresentation is not a function" / silent ORTHOGRAPHIC flip). const makeNextViewport = () => ({ element: { addEventListener: jest.fn(), removeEventListener: jest.fn() }, id: viewportId, type: ViewportType.PLANAR_NEXT, setDisplaySets: jest.fn(), setDisplaySetPresentation: jest.fn(), setViewState: jest.fn(), }); it('renders the labelmap in place and never promotes the viewport (resolver maps in place)', async () => { jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') .mockReturnValue(makeNextViewport() as unknown as csTypes.IStackViewport); jest .spyOn(cstSegmentation.state, 'updateLabelmapSegmentationImageReferences') .mockReturnValue('labelmapImageId'); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); const convertSpy = jest.spyOn(service, 'convertStackToVolumeViewport'); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, }); // the native in-place resolver is consulted... expect( cstSegmentation.state.updateLabelmapSegmentationImageReferences ).toHaveBeenCalledWith(viewportId, mockCornerstoneSegmentation.segmentationId); // ...but the viewport is NEVER promoted to an ORTHOGRAPHIC volume viewport expect(convertSpy).not.toHaveBeenCalled(); expect( serviceManagerMock.services.viewportGridService.setDisplaySetsForViewport ).not.toHaveBeenCalled(); // the representation is added in place, synchronously (isConverted === false) expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); expect(callback).toHaveBeenCalledWith({ segmentationId: mockCornerstoneSegmentation.segmentationId, }); }); it('never promotes on native even when the in-place resolver cannot map (returns isConverted:false unconditionally)', async () => { jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') .mockReturnValue(makeNextViewport() as unknown as csTypes.IStackViewport); // resolver fails (FrameOfReference mismatch / mount-timing race): on legacy this // would fall through to convertStackToVolumeViewport; on native it must not. jest .spyOn(cstSegmentation.state, 'updateLabelmapSegmentationImageReferences') .mockReturnValue(undefined); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); const convertSpy = jest.spyOn(service, 'convertStackToVolumeViewport'); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, }); expect(convertSpy).not.toHaveBeenCalled(); expect( serviceManagerMock.services.viewportGridService.setDisplaySetsForViewport ).not.toHaveBeenCalled(); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); }); }); describe('volume viewport', () => { it('should add a segmentation representation to volume viewport without need for handling', async () => { jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneVolumeViewport as unknown as csTypes.IVolumeViewport); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, }); expect(serviceManagerMock.services.viewportGridService.getState).not.toHaveBeenCalled(); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Surface, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: mockCornerstoneSegmentation.segmentationId, }); }); it('should add a surface segmentation representation to volume viewport without need for handling', async () => { jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue({ ...mockCornerstoneVolumeViewport, type: ViewportType.VOLUME_3D, } as unknown as csTypes.IVolumeViewport); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, }); expect(serviceManagerMock.services.viewportGridService.getState).not.toHaveBeenCalled(); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Surface, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); }); it('should add a volume segmentation representation to volume viewport through SegmentationService handling', async () => { mockCornerstoneVolumeViewport.type = ViewportType.ORTHOGRAPHIC; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockVolumeCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') .mockReturnValue(mockCornerstoneVolumeViewport as unknown as csTypes.IVolumeViewport); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, }); expect(serviceManagerMock.services.viewportGridService.getState).not.toHaveBeenCalled(); expect(mockCornerstoneVolumeViewport.getFrameOfReferenceUID).not.toHaveBeenCalled(); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: mockCornerstoneSegmentation.segmentationId, }); mockCornerstoneVolumeViewport.type = ViewportType.VOLUME_3D; }); it('should add a volume segmentation representation to volume viewport through SegmentationService handling', async () => { const frameOfReferenceUID = 'frameOfReferenceUID'; const imageId = 'imageId'; mockCornerstoneVolumeViewport.type = ViewportType.ORTHOGRAPHIC; mockCornerstoneVolumeViewport.getFrameOfReferenceUID.mockReturnValueOnce( frameOfReferenceUID ); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') .mockReturnValue(mockCornerstoneVolumeViewport as unknown as csTypes.IVolumeViewport); jest .spyOn(cstSegmentation, 'addSegmentationRepresentations') .mockReturnValueOnce(undefined); jest.spyOn(cstSegmentation, 'getLabelmapImageIds').mockReturnValue([imageId]); jest .spyOn(cache, 'getImage') .mockReturnValueOnce({ FrameOfReferenceUID: frameOfReferenceUID } as csTypes.IImage); jest .spyOn(cstSegmentation.helpers, 'convertStackToVolumeLabelmap') .mockReturnValueOnce(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); await service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, }); expect(mockCornerstoneVolumeViewport.getFrameOfReferenceUID).toHaveBeenCalledTimes(1); expect(mockCornerstoneVolumeViewport.getFrameOfReferenceUID).toHaveBeenCalledWith(); expect(cstSegmentation.getLabelmapImageIds).toHaveBeenCalledTimes(1); expect(cstSegmentation.getLabelmapImageIds).toHaveBeenCalledWith( mockCornerstoneSegmentation.segmentationId ); expect(cache.getImage).toHaveBeenCalledTimes(1); expect(cache.getImage).toHaveBeenCalledWith(imageId); expect(cstSegmentation.helpers.convertStackToVolumeLabelmap).toHaveBeenCalledTimes(1); expect(cstSegmentation.helpers.convertStackToVolumeLabelmap).toHaveBeenCalledWith( mockCornerstoneSegmentation ); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: mockCornerstoneSegmentation.segmentationId, config: { colorLUTOrIndex: undefined }, }, ]); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: mockCornerstoneSegmentation.segmentationId, }); mockCornerstoneVolumeViewport.type = ViewportType.VOLUME_3D; }); }); it('should early return if the viewport is not found', () => { jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') .mockReturnValue(null); jest.spyOn(console, 'warn').mockReturnValue(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); service.addSegmentationRepresentation(viewportId, { segmentationId: mockCornerstoneSegmentation.segmentationId, }); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith(`Viewport with id ${viewportId} not found.`); expect(callback).not.toHaveBeenCalled(); }); }); describe('createLabelmapForDisplaySet', () => { it('should create a labelmap for a non dynamic volume display set', async () => { const displaySet = { imageIds: ['imageId'], isDynamicVolume: false, SeriesNumber: 1, SeriesDescription: 'Series Description', Modality: 'SEG', } as unknown as AppTypes.DisplaySet; jest .spyOn(imageLoader, 'createAndCacheDerivedLabelmapImages') .mockReturnValue([{ imageId: 'imageId' }] as csTypes.IImage[]); jest .spyOn(cstSegmentation.state, 'getSegmentations') .mockReturnValue([{ segmentationId: 'segmentationId' }] as cstTypes.Segmentation[]); jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); const retrievedSegmentationId = await service.createLabelmapForDisplaySet(displaySet); expect(imageLoader.createAndCacheDerivedLabelmapImages).toHaveBeenCalledTimes(1); expect(imageLoader.createAndCacheDerivedLabelmapImages).toHaveBeenCalledWith(['imageId']); expect(cstSegmentation.state.getSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentations).toHaveBeenCalledWith(); expect(service.addOrUpdateSegmentation).toHaveBeenCalledTimes(1); expect(service.addOrUpdateSegmentation).toHaveBeenCalledWith({ config: { cachedStats: { info: 'S1: Series Description', }, label: 'Segmentation 2', fallbackLabel: 'S:1 SEG', segments: { '1': { active: true, label: 'Segment 1', }, }, }, representation: { data: { imageIds: ['imageId'], referencedImageIds: ['imageId'], }, type: 'Labelmap', }, segmentationId: expect.any(String), }); expect(retrievedSegmentationId).toEqual(expect.any(String)); }); it('should create a labelmap for a dynamic volume display set', async () => { const segmentationId = 'segmentationId'; const displaySet = { imageIds: ['imageId'], isDynamicVolume: true, SeriesNumber: 1, SeriesDescription: 'Series Description', dynamicVolumeInfo: { timePoints: ['timePoint1', 'timePoint2', 'timePoint3'], }, Modality: 'SEG', } as unknown as AppTypes.DisplaySet; jest .spyOn(imageLoader, 'createAndCacheDerivedLabelmapImages') .mockReturnValue([{ imageId: 'imageId' }] as csTypes.IImage[]); jest .spyOn(cstSegmentation.state, 'getSegmentations') .mockReturnValue([{ segmentationId }] as cstTypes.Segmentation[]); jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); const options = { segmentationId, segments: { 1: { label: 'Custom Segment 1', active: true, }, }, FrameOfReferenceUID: 'frameOfReferenceUID', label: 'Segmentation 2', }; const retrievedSegmentationId = await service.createLabelmapForDisplaySet( displaySet, options ); expect(imageLoader.createAndCacheDerivedLabelmapImages).toHaveBeenCalledTimes(1); expect(imageLoader.createAndCacheDerivedLabelmapImages).toHaveBeenCalledWith('timePoint2'); expect(service.addOrUpdateSegmentation).toHaveBeenCalledTimes(1); expect(service.addOrUpdateSegmentation).toHaveBeenCalledWith({ config: { cachedStats: { info: 'S1: Series Description', }, label: 'Segmentation 2', fallbackLabel: 'S:1 SEG', segments: { '1': { active: true, label: 'Custom Segment 1', }, }, }, representation: { data: { imageIds: ['imageId'], referencedImageIds: 'timePoint2', }, type: 'Labelmap', }, segmentationId: segmentationId, }); expect(retrievedSegmentationId).toEqual(segmentationId); }); }); describe('createSegmentationForSEGDisplaySet', () => { it('should throw an error if the type is not labelmap', async () => { await expect( service.createSegmentationForSEGDisplaySet( {}, { type: csToolsEnums.SegmentationRepresentations.Contour, } ) ).rejects.toThrow('Only labelmap type is supported for SEG display sets right now'); }); it('should throw and error if the labelmap images are not found', async () => { await expect( service.createSegmentationForSEGDisplaySet({ labelMapImages: [], }) ).rejects.toThrow('SEG reading failed'); }); it('should throw an error if the referenced display set is not found', async () => { jest .spyOn(serviceManagerMock.services.displaySetService, 'getDisplaySetByUID') .mockReturnValue({ instances: [], }); await expect( service.createSegmentationForSEGDisplaySet({ displaySetInstanceUID: 'display-set-uid', referencedDisplaySetInstanceUID: 'non-existent-display-set-uid', labelMapImages: [{}, {}], }) ).rejects.toThrow('No instances were provided for the referenced display set of the SEG'); }); it('it should create a segmentation for a SEG display set', async () => { const segmentationId = 'segmentationId'; const voxelManager = { getScalarData: jest.fn().mockReturnValue([1, 0, 0]), setScalarData: jest.fn(), }; const segDisplaySet = { centroids: new Map([ [0, { image: { x: 0, y: 0, z: 0 }, world: { x: 0, y: 0, z: 0 } }], [2, { image: { x: 200, y: 200, z: 200 }, world: { x: 200, y: 200, z: 200 } }], ]), displaySetInstanceUID: 'display-set-uid', referencedDisplaySetInstanceUID: 'existent-display-set-uid', labelMapImages: [ { imageId: 'imageId1', referencedImageId: 'referencedImageId1', voxelManager }, { imageId: 'imageId2', referencedImageId: 'referencedImageId2', voxelManager }, ], segMetadata: { data: [ {}, { SegmentedPropertyCategoryCodeSequence: { CodeMeaning: 'Segmented Property Category Code Sequence', }, SegmentNumber: '1', SegmentLabel: 'Segment 1', SegmentAlgorithmType: 'MANUAL', SegmentAlgorithmName: 'OHIF Brush', SegmentedPropertyTypeCodeSequence: { CodeMeaning: 'Segmented Property Category Code Sequence', }, rgba: [255, 0, 0, 255], }, { SegmentedPropertyCategoryCodeSequence: { CodeMeaning: 'Segmented Property Category Code Sequence', }, SegmentNumber: '2', SegmentAlgorithmType: 'MANUAL', SegmentAlgorithmName: 'OHIF Brush', SegmentedPropertyTypeCodeSequence: { CodeMeaning: 'Segmented Property Type Code Sequence', }, rgba: [0, 255, 0, 255], }, ], }, SeriesDate: '2025-01-01', SeriesDescription: 'Series Description', Modality: 'SEG', SeriesNumber: 1, }; const referencedDisplaySet = { instances: [{ imageId: 'referencedImageId1' }, { imageId: 'referencedImageId2' }], }; jest .spyOn(serviceManagerMock.services.displaySetService, 'getDisplaySetByUID') .mockReturnValue(referencedDisplaySet); // @ts-expect-error - jest can't handle Array.prototype.flat typing jest.spyOn(Array.prototype, 'flat'); jest.spyOn(metaData, 'get').mockReturnValue({}); jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_LOADING_COMPLETE, callback); const retrievedSegmentationId = await service.createSegmentationForSEGDisplaySet( segDisplaySet, { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId, } ); expect( serviceManagerMock.services.displaySetService.getDisplaySetByUID ).toHaveBeenCalledTimes(1); expect(serviceManagerMock.services.displaySetService.getDisplaySetByUID).toHaveBeenCalledWith( 'existent-display-set-uid' ); expect(Array.prototype.flat).toHaveBeenCalledTimes(1); expect(metaData.get).toHaveBeenCalledTimes(2); expect(metaData.get).toHaveBeenCalledWith('instance', 'referencedImageId1'); expect(metaData.get).toHaveBeenCalledWith('instance', 'referencedImageId2'); expect(voxelManager.getScalarData).toHaveBeenCalledTimes(2); expect(voxelManager.getScalarData).toHaveBeenCalledWith(); expect(voxelManager.setScalarData).toHaveBeenCalledTimes(2); expect(voxelManager.setScalarData).toHaveBeenCalledWith([1, 0, 0]); expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledWith([ [0, 0, 0, 0], [255, 0, 0, 255], [0, 255, 0, 255], ]); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId, segDisplaySet, }); const expectedSegmentation = { config: { label: segDisplaySet.SeriesDescription, fallbackLabel: 'S:1 SEG', segments: { '1': { active: false, cachedStats: { algorithmName: 'OHIF Brush', algorithmType: 'MANUAL', category: 'Segmented Property Category Code Sequence', center: { image: [0, 0, 0], world: [0, 0, 0], }, modifiedTime: '2025-01-01', type: 'Segmented Property Category Code Sequence', }, label: 'Segment 1', locked: false, segmentIndex: 1, }, '2': { active: false, cachedStats: { algorithmName: 'OHIF Brush', algorithmType: 'MANUAL', category: 'Segmented Property Category Code Sequence', center: { image: [200, 200, 200], world: [200, 200, 200], }, modifiedTime: '2025-01-01', type: 'Segmented Property Type Code Sequence', }, label: 'Segment 2', locked: false, segmentIndex: 2, }, }, }, representation: { data: { imageIds: ['imageId1', 'imageId2'], referencedImageIds: ['referencedImageId1', 'referencedImageId2'], }, type: csToolsEnums.SegmentationRepresentations.Labelmap, }, segmentationId, }; expect(service.addOrUpdateSegmentation).toHaveBeenCalledTimes(1); expect(service.addOrUpdateSegmentation).toHaveBeenCalledWith(expectedSegmentation); const expectedSegDisplaySet = { ...segDisplaySet, firstSegmentedSliceImageId: 'referencedImageId1', imageIds: ['imageId1', 'imageId2'], images: [ { imageId: 'imageId1', referencedImageId: 'referencedImageId1', voxelManager, }, { imageId: 'imageId2', referencedImageId: 'referencedImageId2', voxelManager, }, ], isLoaded: true, labelMapImages: [ { imageId: 'imageId1', referencedImageId: 'referencedImageId1', voxelManager, }, { imageId: 'imageId2', referencedImageId: 'referencedImageId2', voxelManager, }, ], }; expect(segDisplaySet).toEqual(expectedSegDisplaySet); expect(retrievedSegmentationId).toEqual(segmentationId); }); describe('overlap / useSliceRendering (next backend)', () => { const segmentationId = 'segmentationId'; // The session flag selects the seg-backend lane at SEG-load; reset it so it // never leaks into the legacy-path tests that follow. afterEach(() => { setNextViewportsEnabled(false); }); const segMetadata = { data: [ {}, { SegmentNumber: '1', SegmentLabel: 'Segment 1', rgba: [255, 0, 0, 255] }, { SegmentNumber: '2', SegmentLabel: 'Segment 2', rgba: [0, 255, 0, 255] }, ], }; const centroids = new Map([ [1, { image: { x: 0, y: 0, z: 0 }, world: { x: 0, y: 0, z: 0 } }], [2, { image: { x: 1, y: 1, z: 1 }, world: { x: 1, y: 1, z: 1 } }], ]); // labelMapImages is the adapter's array-of-GROUPS (one conflict-free group per // overlap layer). Two groups whose voxels carry values {1} and {2} respectively. const makeOverlapGroups = () => { const vm0 = { getScalarData: jest.fn().mockReturnValue([1, 0]), setScalarData: jest.fn() }; const vm1 = { getScalarData: jest.fn().mockReturnValue([0, 2]), setScalarData: jest.fn() }; return [ [ { imageId: 'g0i1', referencedImageId: 'r1', voxelManager: vm0 }, { imageId: 'g0i2', referencedImageId: 'r2', voxelManager: vm0 }, ], [ { imageId: 'g1i1', referencedImageId: 'r1', voxelManager: vm1 }, { imageId: 'g1i2', referencedImageId: 'r2', voxelManager: vm1 }, ], ]; }; const makeSegDisplaySet = (labelMapImages, overlappingSegments) => ({ centroids, displaySetInstanceUID: 'display-set-uid', referencedDisplaySetInstanceUID: 'existent-display-set-uid', labelMapImages, overlappingSegments, segMetadata, SeriesDate: '2025-01-01', SeriesDescription: 'Series Description', Modality: 'SEG', SeriesNumber: 1, }); const primeMocks = () => { jest .spyOn(serviceManagerMock.services.displaySetService, 'getDisplaySetByUID') .mockReturnValue({ instances: [{ imageId: 'r1' }, { imageId: 'r2' }] }); jest.spyOn(metaData, 'get').mockReturnValue({}); jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); }; it('flag ON + overlapping SEG builds one labelmap layer per group + segmentBindings', async () => { setNextViewportsEnabled(true); primeMocks(); await service.createSegmentationForSEGDisplaySet( makeSegDisplaySet(makeOverlapGroups(), true), { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId } ); const seg = jest.mocked(service.addOrUpdateSegmentation).mock.calls[0][0]; const data = seg.representation.data as Record; // one labelmap layer per conflict-free group, under ONE segmentationId expect(Object.keys(data.labelmaps)).toEqual([ 'segmentationId-storage-0', 'segmentationId-storage-1', ]); expect(data.labelmaps['segmentationId-storage-0'].imageIds).toEqual(['g0i1', 'g0i2']); expect(data.labelmaps['segmentationId-storage-1'].imageIds).toEqual(['g1i1', 'g1i2']); expect(data.labelmaps['segmentationId-storage-0'].storageKind).toBe('stack'); // segment->layer bindings recovered from the distinct non-zero voxel values expect(data.segmentBindings).toEqual({ 1: { labelmapId: 'segmentationId-storage-0', labelValue: 1 }, 2: { labelmapId: 'segmentationId-storage-1', labelValue: 2 }, }); expect(data.primaryLabelmapId).toBe('segmentationId-storage-0'); // flattened list retained for legacy singular readers expect(data.imageIds).toEqual(['g0i1', 'g0i2', 'g1i1', 'g1i2']); }); it('flag ON + non-overlapping SEG stays a single layer (no multi-layer map)', async () => { setNextViewportsEnabled(true); primeMocks(); const vm = { getScalarData: jest.fn().mockReturnValue([1, 0]), setScalarData: jest.fn() }; const singleGroup = [ [ { imageId: 'i1', referencedImageId: 'r1', voxelManager: vm }, { imageId: 'i2', referencedImageId: 'r2', voxelManager: vm }, ], ]; await service.createSegmentationForSEGDisplaySet(makeSegDisplaySet(singleGroup, false), { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId, }); const seg = jest.mocked(service.addOrUpdateSegmentation).mock.calls[0][0]; const data = seg.representation.data as Record; expect(data.labelmaps).toBeUndefined(); expect(data.segmentBindings).toBeUndefined(); expect(data.imageIds).toEqual(['i1', 'i2']); }); it('flag OFF + overlapping SEG collapses to a single flattened layer (legacy byte-identical)', async () => { // flag stays off (default) primeMocks(); await service.createSegmentationForSEGDisplaySet( makeSegDisplaySet(makeOverlapGroups(), true), { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId } ); const seg = jest.mocked(service.addOrUpdateSegmentation).mock.calls[0][0]; const data = seg.representation.data as Record; expect(data.labelmaps).toBeUndefined(); expect(data.imageIds).toEqual(['g0i1', 'g0i2', 'g1i1', 'g1i2']); }); }); }); describe('createSegmentationForRTDisplaySet', () => { it('should throw an error if the type is not contour', async () => { await expect( service.createSegmentationForRTDisplaySet( { modality: 'RTSTRUCT', }, { type: csToolsEnums.SegmentationRepresentations.Labelmap, } ) ).rejects.toThrow('Only contour type is supported for RT display sets right now'); }); it('should throw an error if the structureSet is not loaded', async () => { await expect( service.createSegmentationForRTDisplaySet( { modality: 'RTSTRUCT', displaySetInstanceUID: 'display-set-uid', }, { type: csToolsEnums.SegmentationRepresentations.Contour, } ) ).rejects.toThrow( 'To create the contours from RT displaySet, the displaySet should be loaded first. You can perform rtDisplaySet.load() before calling this method.' ); }); it('should throw and error if structureSet does not contain any ROIContours', async () => { const rtStructDisplaySet = { modality: 'RTSTRUCT', displaySetInstanceUID: 'display-set-uid', referencedDisplaySetInstanceUID: 'existent-display-set-uid', structureSet: { ReferencedSOPInstanceUIDsSet: new Set(['referencedImageId1', 'referencedImageId2']), ROIContours: [], }, }; const referencedDisplaySet = { instances: [{ imageId: 'referencedImageId1' }, { imageId: 'referencedImageId2' }], }; jest .spyOn(serviceManagerMock.services.displaySetService, 'getDisplaySetByUID') .mockReturnValue(referencedDisplaySet); await expect( service.createSegmentationForRTDisplaySet(rtStructDisplaySet, { type: csToolsEnums.SegmentationRepresentations.Contour, }) ).rejects.toThrow( 'The structureSet does not contain any ROIContours. Please ensure the structureSet is loaded first.' ); expect( serviceManagerMock.services.displaySetService.getDisplaySetByUID ).toHaveBeenCalledTimes(1); expect(serviceManagerMock.services.displaySetService.getDisplaySetByUID).toHaveBeenCalledWith( 'existent-display-set-uid' ); }); it('should create a segmentation for a RTSTRUCT display set', async () => { const segmentationId = 'segmentationId'; const rtStructDisplaySet = { Modality: 'RTSTRUCT', displaySetInstanceUID: 'display-set-uid', referencedDisplaySetInstanceUID: 'existent-display-set-uid', SeriesDate: '2025-01-01', SeriesDescription: 'Series Description', structureSet: { ReferencedSOPInstanceUIDsSet: new Set(['referencedIsmageId1', 'referencedImageId2']), ROIContours: [{}, {}, {}], frameOfReferenceUID: 'frameOfReferenceUID', }, SeriesNumber: 1, }; const referencedDisplaySet = { instances: [{ imageId: 'referencedImageId1' }, { imageId: 'referencedImageId2' }], imageIds: ['referencedImageId1', 'referencedImageId2'], }; const allRTStructData = [ { data: {}, id: 'id3', color: [255, 0, 0, 255], group: 'group3', segmentIndex: 3, geometryId: 'geometryId3', }, { data: {}, id: 'id2', color: [0, 255, 0, 255], group: 'group2', segmentIndex: 2, geometryId: 'geometryId2', }, { data: {}, id: 'id1', color: [0, 0, 255, 255], group: 'group1', segmentIndex: 1, geometryId: 'geometryId1', }, ]; jest .spyOn(serviceManagerMock.services.displaySetService, 'getDisplaySetByUID') .mockReturnValue(referencedDisplaySet); jest .spyOn(MapROIContoursToRTStructData, 'mapROIContoursToRTStructData') .mockReturnValue(allRTStructData); jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); jest.spyOn(geometryLoader, 'createAndCacheGeometry').mockReturnValue({ // @ts-expect-error - only mocking needed properties data: { centroid: [0, 0, 0] }, }); const segmentLoadingCompleteCallback = jest.fn(); service.subscribe(service.EVENTS.SEGMENT_LOADING_COMPLETE, segmentLoadingCompleteCallback); const segmentationLoadingCompleteCallback = jest.fn(); service.subscribe( service.EVENTS.SEGMENTATION_LOADING_COMPLETE, segmentationLoadingCompleteCallback ); const retrievedSegmentationId = await service.createSegmentationForRTDisplaySet( rtStructDisplaySet, { type: csToolsEnums.SegmentationRepresentations.Contour, segmentationId, } ); expect(MapROIContoursToRTStructData.mapROIContoursToRTStructData).toHaveBeenCalledTimes(1); expect(MapROIContoursToRTStructData.mapROIContoursToRTStructData).toHaveBeenCalledWith( rtStructDisplaySet.structureSet, rtStructDisplaySet.displaySetInstanceUID ); expect(geometryLoader.createAndCacheGeometry).toHaveBeenCalledTimes(3); allRTStructData.forEach(data => { expect(geometryLoader.createAndCacheGeometry).toHaveBeenCalledWith(data.geometryId, { geometryData: { data: data.data, id: data.id, color: data.color, frameOfReferenceUID: rtStructDisplaySet.structureSet.frameOfReferenceUID, segmentIndex: data.segmentIndex, }, type: csEnums.GeometryType.CONTOUR, }); }); expect(segmentLoadingCompleteCallback).toHaveBeenCalledTimes(3); expect(segmentLoadingCompleteCallback).toHaveBeenCalledWith({ percentComplete: expect.any(Number), numSegments: 3, }); expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.addColorLUT).toHaveBeenCalledWith([ [0, 0, 0, 0], [0, 0, 255, 255], [0, 255, 0, 255], [255, 0, 0, 255], ]); expect(segmentationLoadingCompleteCallback).toHaveBeenCalledTimes(1); expect(segmentationLoadingCompleteCallback).toHaveBeenCalledWith({ segmentationId, rtDisplaySet: rtStructDisplaySet, }); const expectedSegmentation = { config: { label: rtStructDisplaySet.SeriesDescription, fallbackLabel: 'S:1 RTSTRUCT', segments: { '1': { active: false, cachedStats: { center: { world: [0, 0, 0], }, modifiedTime: rtStructDisplaySet.SeriesDate, }, group: 'group1', label: 'id1', locked: false, segmentIndex: 1, }, '2': { active: false, cachedStats: { center: { world: [0, 0, 0], }, modifiedTime: rtStructDisplaySet.SeriesDate, }, group: 'group2', label: 'id2', locked: false, segmentIndex: 2, }, '3': { active: false, cachedStats: { center: { world: [0, 0, 0], }, modifiedTime: rtStructDisplaySet.SeriesDate, }, group: 'group3', label: 'id3', locked: false, segmentIndex: 3, }, }, }, representation: { data: { geometryIds: ['geometryId1', 'geometryId2', 'geometryId3'], }, type: csToolsEnums.SegmentationRepresentations.Contour, }, segmentationId, }; expect(service.addOrUpdateSegmentation).toHaveBeenCalledTimes(1); expect(service.addOrUpdateSegmentation).toHaveBeenCalledWith(expectedSegmentation); expect(retrievedSegmentationId).toEqual(segmentationId); const expectedRtStructDisplaySet = { ...rtStructDisplaySet, isLoaded: true, }; expect(rtStructDisplaySet).toEqual(expectedRtStructDisplaySet); }); it('should ignore when a segment fails to initialize', async () => { const segmentationId = 'segmentationId'; const rtStructDisplaySet = { modality: 'RTSTRUCT', displaySetInstanceUID: 'display-set-uid', referencedDisplaySetInstanceUID: 'existent-display-set-uid', SeriesDate: '2025-01-01', SeriesDescription: 'Series Description', structureSet: { ReferencedSOPInstanceUIDsSet: new Set(['referencedIsmageId1', 'referencedImageId2']), ROIContours: [{}, {}, {}], frameOfReferenceUID: 'frameOfReferenceUID', }, }; const referencedDisplaySet = { instances: [{ imageId: 'referencedImageId1' }, { imageId: 'referencedImageId2' }], }; const allRTStructData = [ { data: {}, id: 'id3', color: [255, 0, 0, 255], group: 'group3', segmentIndex: 3, geometryId: 'geometryId3', }, ]; jest .spyOn(serviceManagerMock.services.displaySetService, 'getDisplaySetByUID') .mockReturnValue(referencedDisplaySet); jest .spyOn(MapROIContoursToRTStructData, 'mapROIContoursToRTStructData') .mockReturnValue(allRTStructData); jest .spyOn(geometryLoader, 'createAndCacheGeometry') .mockRejectedValue(new Error('Segment Initialization Error') as never); jest.spyOn(console, 'warn').mockImplementation(() => {}); jest.spyOn(service, 'addOrUpdateSegmentation').mockReturnValue(undefined); const segmentLoadingCompleteCallback = jest.fn(); service.subscribe(service.EVENTS.SEGMENT_LOADING_COMPLETE, segmentLoadingCompleteCallback); const segmentationLoadingCompleteCallback = jest.fn(); service.subscribe( service.EVENTS.SEGMENTATION_LOADING_COMPLETE, segmentationLoadingCompleteCallback ); await expect( service.createSegmentationForRTDisplaySet(rtStructDisplaySet, { type: csToolsEnums.SegmentationRepresentations.Contour, segmentationId, }) ).resolves.not.toThrow(); expect(segmentLoadingCompleteCallback).not.toHaveBeenCalled(); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( `Error initializing contour for segment ${allRTStructData[0].segmentIndex}:`, expect.any(Error) ); expect(segmentationLoadingCompleteCallback).toHaveBeenCalledTimes(1); expect(service.addOrUpdateSegmentation).toHaveBeenCalledTimes(1); }); }); describe('addOrUpdateSegmentation', () => { it('should add new segmentation if it does not exist', () => { const segmentationId = 'segmentationId'; const segmentationData = { segmentationId, representation: { type: csToolsEnums.SegmentationRepresentations.Labelmap, data: {}, }, config: { label: 'Segmentation 1', }, }; jest.spyOn(cstSegmentation.state, 'getSegmentation').mockReturnValue(undefined); jest.spyOn(cstSegmentation, 'addSegmentations').mockReturnValue(undefined); service.addOrUpdateSegmentation(segmentationData); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith(segmentationId); expect(cstSegmentation.addSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentations).toHaveBeenCalledWith([segmentationData]); }); it('should update existing segmentation if it exists', () => { const segmentationId = 'segmentationId'; const segmentationData = { segmentationId, config: { label: 'Segmentation 1', }, }; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(cstSegmentation, 'updateSegmentations').mockReturnValue(undefined); service.addOrUpdateSegmentation(segmentationData); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledWith([ { segmentationId, payload: segmentationData }, ]); }); }); describe('setActiveSegmentation', () => { it('should set the active segmentation for a viewport', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; service.setActiveSegmentation(viewportId, segmentationId); expect(cstSegmentation.activeSegmentation.setActiveSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.activeSegmentation.setActiveSegmentation).toHaveBeenCalledWith( viewportId, segmentationId ); }); }); describe('getActiveSegmentation', () => { it('should get the active segmentation for a viewport', () => { const viewportId = 'viewportId'; service.getActiveSegmentation(viewportId); expect(cstSegmentation.activeSegmentation.getActiveSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.activeSegmentation.getActiveSegmentation).toHaveBeenCalledWith( viewportId ); }); }); describe('getActiveSegment', () => { it('should return undefined if no active segmentation', () => { const viewportId = 'viewportId'; jest.spyOn(cstSegmentation.activeSegmentation, 'getActiveSegmentation').mockReturnValue(null); const activeSegment = service.getActiveSegment(viewportId); expect(cstSegmentation.activeSegmentation.getActiveSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.activeSegmentation.getActiveSegmentation).toHaveBeenCalledWith( viewportId ); expect(activeSegment).toBeUndefined(); }); it('should find and return the active segment', () => { const viewportId = 'viewportId'; jest .spyOn(cstSegmentation.activeSegmentation, 'getActiveSegmentation') .mockReturnValue(mockCornerstoneSegmentation); const activeSegment = service.getActiveSegment(viewportId); expect(activeSegment).toEqual(mockCornerstoneSegmentation.segments['1']); }); }); describe('hasCustomStyles', () => { it('should return true if the segmentation has custom styles', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const type = csToolsEnums.SegmentationRepresentations.Contour; jest.spyOn(cstSegmentation.config.style, 'hasCustomStyle').mockReturnValue(true); const hasCustomStyles = service.hasCustomStyles({ viewportId, segmentationId, type }); expect(cstSegmentation.config.style.hasCustomStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.hasCustomStyle).toHaveBeenCalledWith({ viewportId, segmentationId, type, }); expect(hasCustomStyles).toBe(true); }); }); describe('getStyle', () => { it('should return the style for the segmentation', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const type = csToolsEnums.SegmentationRepresentations.Contour; const segmentIndex = 1; jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({}); const style = service.getStyle({ viewportId, segmentationId, type, segmentIndex }); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledWith({ viewportId, segmentationId, type, segmentIndex, }); expect(style).toEqual({}); }); }); describe('setStyle', () => { it('should set the style for the segmentation', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const type = csToolsEnums.SegmentationRepresentations.Contour; const segmentIndex = 1; const style = { fillAlpha: 0.5, outlineWidth: 2, renderOutline: true, renderFill: true, }; jest.spyOn(cstSegmentation.config.style, 'setStyle').mockReturnValue(undefined); service.setStyle({ viewportId, segmentationId, type, segmentIndex }, style); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledWith( { viewportId, segmentationId, type, segmentIndex, }, style, true ); }); }); describe('resetToGlobalStyle', () => { it('should reset the style for the segmentation', () => { jest.spyOn(cstSegmentation.config.style, 'resetToGlobalStyle').mockReturnValue(undefined); service.resetToGlobalStyle(); expect(cstSegmentation.config.style.resetToGlobalStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.resetToGlobalStyle).toHaveBeenCalledWith(); }); }); describe('addSegment', () => { it('should throw an error if the segment index is 0', () => { const segmentationId = 'segmentationId'; const config = { segmentIndex: 0, }; expect(() => service.addSegment(segmentationId, config)).toThrow( 'Segment index 0 is reserved for "no label"' ); }); it('should add a new segment with next available index if not provided', () => { const segmentationId = 'segmentationId'; const config = { label: 'New Segment 2', visibility: true, }; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(cstSegmentation, 'updateSegmentations').mockReturnValue(undefined); jest.spyOn(cstSegmentation.segmentIndex, 'setActiveSegmentIndex').mockReturnValue(undefined); jest .spyOn(cstSegmentation.state, 'getViewportIdsWithSegmentation') .mockReturnValue(['viewportId']); jest .spyOn(cstSegmentation.config.visibility, 'setSegmentIndexVisibility') .mockReturnValue(undefined); service.addSegment(segmentationId, config); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(2); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith(segmentationId); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledWith([ { segmentationId, payload: { segments: { ...mockCornerstoneSegmentation.segments, '2': { label: 'New Segment 2', segmentIndex: 2, cachedStats: {}, locked: false, ...config, }, }, }, }, ]); expect(cstSegmentation.segmentIndex.setActiveSegmentIndex).toHaveBeenCalledTimes(1); expect(cstSegmentation.segmentIndex.setActiveSegmentIndex).toHaveBeenCalledWith( segmentationId, 2 ); expect(cstSegmentation.state.getViewportIdsWithSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getViewportIdsWithSegmentation).toHaveBeenCalledWith( segmentationId ); expect(cstSegmentation.config.visibility.setSegmentIndexVisibility).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.visibility.setSegmentIndexVisibility).toHaveBeenCalledWith( 'viewportId', { segmentationId, type: undefined }, 2, config.visibility ); }); it('should set properties if segment index already exists', () => { const segmentationId = 'segmentationId'; const config = { segmentIndex: 1, isLocked: false, active: true, color: [255, 0, 0, 255] as csTypes.Color, }; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(cstSegmentation, 'updateSegmentations').mockReturnValue(undefined); jest.spyOn(cstSegmentation.segmentIndex, 'setActiveSegmentIndex').mockReturnValue(undefined); jest.spyOn(service, 'getViewportIdsWithSegmentation').mockReturnValue(['viewportId']); jest .spyOn(cstSegmentation.segmentLocking, 'setSegmentIndexLocked') .mockReturnValue(undefined); jest.spyOn(cstSegmentation.config.color, 'setSegmentIndexColor').mockReturnValue(undefined); jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValue([{ colorLUTIndex: 1 }] as cstTypes.SegmentationRepresentation[]); service.addSegment(segmentationId, config); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledWith([ { segmentationId, payload: { segments: { ...mockCornerstoneSegmentation.segments, '1': { label: 'Segment 1', segmentIndex: 1, cachedStats: {}, locked: false, active: true, ...config, }, }, }, }, ]); expect(cstSegmentation.segmentLocking.setSegmentIndexLocked).toHaveBeenCalledTimes(1); expect(cstSegmentation.segmentLocking.setSegmentIndexLocked).toHaveBeenCalledWith( segmentationId, 1, config.isLocked ); expect(cstSegmentation.config.color.setSegmentIndexColor).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.color.setSegmentIndexColor).toHaveBeenCalledWith( 'viewportId', segmentationId, 1, config.color ); }); }); describe('removeSegment', () => { it('should remove the segment', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; jest.spyOn(cstSegmentation, 'removeSegment').mockReturnValue(undefined); service.removeSegment(segmentationId, segmentIndex); expect(cstSegmentation.removeSegment).toHaveBeenCalledTimes(1); expect(cstSegmentation.removeSegment).toHaveBeenCalledWith(segmentationId, segmentIndex, { recordHistory: true, }); }); }); describe('setSegmentVisibility', () => { it('should set the visibility of the segment', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const segmentIndex = 1; const isVisible = true; const type = csToolsEnums.SegmentationRepresentations.Contour; jest .spyOn(cstSegmentation.config.visibility, 'setSegmentIndexVisibility') .mockReturnValue(undefined); service.setSegmentVisibility(viewportId, segmentationId, segmentIndex, isVisible, type); expect(cstSegmentation.config.visibility.setSegmentIndexVisibility).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.visibility.setSegmentIndexVisibility).toHaveBeenCalledWith( viewportId, { segmentationId, type }, segmentIndex, isVisible ); }); }); describe('setSegmentLocked', () => { it('should set the locked status of the segment', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; const isLocked = true; jest .spyOn(cstSegmentation.segmentLocking, 'setSegmentIndexLocked') .mockReturnValue(undefined); service.setSegmentLocked(segmentationId, segmentIndex, isLocked); expect(cstSegmentation.segmentLocking.setSegmentIndexLocked).toHaveBeenCalledTimes(1); expect(cstSegmentation.segmentLocking.setSegmentIndexLocked).toHaveBeenCalledWith( segmentationId, segmentIndex, isLocked ); }); }); describe('toggleSegmentLocked', () => { it('should toggle the locked status of the segment', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; const isLocked = true; jest.spyOn(cstSegmentation.segmentLocking, 'isSegmentIndexLocked').mockReturnValue(isLocked); jest .spyOn(cstSegmentation.segmentLocking, 'setSegmentIndexLocked') .mockReturnValue(undefined); service.toggleSegmentLocked(segmentationId, segmentIndex); expect(cstSegmentation.segmentLocking.isSegmentIndexLocked).toHaveBeenCalledTimes(1); expect(cstSegmentation.segmentLocking.isSegmentIndexLocked).toHaveBeenCalledWith( segmentationId, segmentIndex ); expect(cstSegmentation.segmentLocking.setSegmentIndexLocked).toHaveBeenCalledTimes(1); expect(cstSegmentation.segmentLocking.setSegmentIndexLocked).toHaveBeenCalledWith( segmentationId, segmentIndex, !isLocked ); }); }); describe('toggleSegmentVisibility', () => { it('should toggle the visibility of the segment', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const segmentIndex = 1; const isVisible = true; const type = csToolsEnums.SegmentationRepresentations.Contour; jest .spyOn(cstSegmentation.config.visibility, 'getSegmentIndexVisibility') .mockReturnValue(isVisible); jest .spyOn(cstSegmentation.config.visibility, 'setSegmentIndexVisibility') .mockReturnValue(undefined); service.toggleSegmentVisibility(viewportId, segmentationId, segmentIndex, type); expect(cstSegmentation.config.visibility.getSegmentIndexVisibility).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.visibility.getSegmentIndexVisibility).toHaveBeenCalledWith( viewportId, { segmentationId, type }, segmentIndex ); expect(cstSegmentation.config.visibility.setSegmentIndexVisibility).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.visibility.setSegmentIndexVisibility).toHaveBeenCalledWith( viewportId, { segmentationId, type }, segmentIndex, !isVisible ); }); }); describe('setSegmentColor', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const segmentIndex = 1; const color = [255, 0, 0, 255] as csTypes.Color; it('should set the color of the segment', () => { jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValue([{ colorLUTIndex: 1 }] as cstTypes.SegmentationRepresentation[]); jest.spyOn(cstSegmentation.config.color, 'setSegmentIndexColor').mockReturnValue(undefined); service.setSegmentColor(viewportId, segmentationId, segmentIndex, color); expect(cstSegmentation.state.getSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentationRepresentations).toHaveBeenCalledWith( viewportId, { segmentationId } ); expect(cstSegmentation.config.color.setSegmentIndexColor).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.color.setSegmentIndexColor).toHaveBeenCalledWith( viewportId, segmentationId, segmentIndex, color ); }); it('should set the color of the segment with the colorLUTIndex', async () => { jest .spyOn(cstSegmentation.state, 'getSegmentationRepresentations') .mockReturnValue([{ colorLUTIndex: 1 }] as cstTypes.SegmentationRepresentation[]); jest.spyOn(cstSegmentation.config.color, 'setSegmentIndexColor').mockReturnValue(undefined); service.setSegmentColor(viewportId, segmentationId, segmentIndex, color); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation as cstTypes.Segmentation); jest .spyOn(serviceManagerMock.services.cornerstoneViewportService, 'getCornerstoneViewport') // only needed interfaces for the addSegmentationRepresentation call .mockReturnValue(mockCornerstoneStackViewport as unknown as csTypes.IStackViewport); jest .spyOn(cstSegmentation.state, 'updateLabelmapSegmentationImageReferences') .mockReturnValue('labelmapImageId'); jest.spyOn(cstSegmentation, 'addSegmentationRepresentations').mockReturnValueOnce(undefined); await service.addSegmentationRepresentation(viewportId, { segmentationId: segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, config: { blendMode: csEnums.BlendModes.COMPOSITE }, suppressEvents: true, }); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.addSegmentationRepresentations).toHaveBeenCalledWith(viewportId, [ { type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentationId: segmentationId, config: { colorLUTOrIndex: 1, blendMode: csEnums.BlendModes.COMPOSITE }, }, ]); }); }); describe('getSegmentColor', () => { it('should get the color of the segment', () => { const viewportId = 'viewportId'; const segmentationId = 'segmentationId'; const segmentIndex = 1; const color = [255, 0, 0, 255] as csTypes.Color; jest.spyOn(cstSegmentation.config.color, 'getSegmentIndexColor').mockReturnValue(color); const returnedColor = service.getSegmentColor(viewportId, segmentationId, segmentIndex); expect(cstSegmentation.config.color.getSegmentIndexColor).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.color.getSegmentIndexColor).toHaveBeenCalledWith( viewportId, segmentationId, segmentIndex ); expect(returnedColor).toEqual(color); }); }); describe('getLabelmapVolume', () => { it('should get the labelmap volume for the segmentation', () => { const segmentationId = 'segmentationId'; const labelmapVolume = { id: 'volumeId' }; mockCornerstoneSegmentation.representationData.Labelmap = { volumeId: 'volumeId' }; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); // @ts-expect-error - no need to mock every property for this test jest.spyOn(cache, 'getVolume').mockReturnValue(labelmapVolume); const returnedLabelmapVolume = service.getLabelmapVolume(segmentationId); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith(segmentationId); expect(cache.getVolume).toHaveBeenCalledTimes(1); expect(cache.getVolume).toHaveBeenCalledWith(labelmapVolume.id); expect(returnedLabelmapVolume).toEqual(labelmapVolume); mockCornerstoneSegmentation.representationData.Labelmap = {}; }); it('should return null if the segmentation does not have a labelmap volume', () => { const segmentationId = 'segmentationId'; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); const returnedLabelmapVolume = service.getLabelmapVolume(segmentationId); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1); expect(returnedLabelmapVolume).toEqual(null); }); }); describe('setSegmentLabel', () => { it('should set the label of the segment', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; const label = 'New Segment 1'; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(cstSegmentation, 'updateSegmentations').mockReturnValue(undefined); service.setSegmentLabel(segmentationId, segmentIndex, label); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith(segmentationId); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.updateSegmentations).toHaveBeenCalledWith([ { segmentationId, payload: { segments: { ...mockCornerstoneSegmentation.segments, [segmentIndex]: { ...mockCornerstoneSegmentation.segments[segmentIndex], label }, }, }, }, ]); }); }); describe('setActiveSegment', () => { it('should set the active segment for the segmentation', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; jest.spyOn(cstSegmentation.segmentIndex, 'setActiveSegmentIndex').mockReturnValue(undefined); service.setActiveSegment(segmentationId, segmentIndex); expect(cstSegmentation.segmentIndex.setActiveSegmentIndex).toHaveBeenCalledTimes(1); expect(cstSegmentation.segmentIndex.setActiveSegmentIndex).toHaveBeenCalledWith( segmentationId, segmentIndex ); }); }); describe('setRenderInactiveSegmentations', () => { it('should set the render inactive segmentations for the viewport', () => { const viewportId = 'viewportId'; const renderInactive = true; jest .spyOn(cstSegmentation.config.style, 'setRenderInactiveSegmentations') .mockReturnValue(undefined); service.setRenderInactiveSegmentations(viewportId, renderInactive); expect(cstSegmentation.config.style.setRenderInactiveSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.setRenderInactiveSegmentations).toHaveBeenCalledWith( viewportId, renderInactive ); }); }); describe('getRenderInactiveSegmentations', () => { it('should get the render inactive segmentations for the viewport', () => { const viewportId = 'viewportId'; const renderInactive = true; jest .spyOn(cstSegmentation.config.style, 'getRenderInactiveSegmentations') .mockReturnValue(renderInactive); const returnedRenderInactive = service.getRenderInactiveSegmentations(viewportId); expect(cstSegmentation.config.style.getRenderInactiveSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.getRenderInactiveSegmentations).toHaveBeenCalledWith( viewportId ); expect(returnedRenderInactive).toEqual(renderInactive); }); }); describe('setSegmentationGroupStats', () => { it('should set the segmentation group stats', () => { const segmentationIds = ['segmentationId1', 'segmentationId2']; const stats = { key: 'value' }; service.setSegmentationGroupStats(segmentationIds, stats); const returnedStats = service.getSegmentationGroupStats(segmentationIds); expect(returnedStats).toEqual(stats); }); }); describe('getSegmentationGroupStats', () => { it('should get the segmentation group stats', () => { const segmentationIds = ['segmentationId1', 'segmentationId2']; const stats = { key: 'value' }; let returnedStats = service.getSegmentationGroupStats(segmentationIds); expect(returnedStats).toEqual(undefined); service.setSegmentationGroupStats(segmentationIds, stats); returnedStats = service.getSegmentationGroupStats(segmentationIds); expect(returnedStats).toEqual(stats); }); }); describe('toggleSegmentationRepresentationVisibility', () => { it('should toggle the visibility of the segmentation representation', () => { jest.spyOn(service, 'getSegmentationRepresentations').mockReturnValue(representations); jest .spyOn(cstSegmentation.config.visibility, 'getHiddenSegmentIndices') .mockReturnValue(new Set()); jest .spyOn(cstSegmentation.config.visibility, 'setSegmentationRepresentationVisibility') .mockReturnValue(undefined); service.toggleSegmentationRepresentationVisibility(viewportId, { segmentationId: representations[0].segmentationId, type: representations[0].type, }); expect(service.getSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(service.getSegmentationRepresentations).toHaveBeenCalledWith(viewportId, { segmentationId: representations[0].segmentationId, type: representations[0].type, }); expect(cstSegmentation.config.visibility.getHiddenSegmentIndices).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.visibility.getHiddenSegmentIndices).toHaveBeenCalledWith( viewportId, { segmentationId: representations[0].segmentationId, type: representations[0].type, } ); expect( cstSegmentation.config.visibility.setSegmentationRepresentationVisibility ).toHaveBeenCalledTimes(1); expect( cstSegmentation.config.visibility.setSegmentationRepresentationVisibility ).toHaveBeenCalledWith( viewportId, { segmentationId: representations[0].segmentationId, type: representations[0].type, }, false ); }); }); describe('setSegmentationRepresentationVisibility', () => { it('should early return if the representation is not found', () => { jest.spyOn(service, 'getSegmentationRepresentations').mockReturnValueOnce([]); jest.spyOn(console, 'debug').mockReturnValue(undefined); jest.spyOn(cstSegmentation.config.visibility, 'setSegmentationRepresentationVisibility'); service['_setSegmentationRepresentationVisibility']( viewportId, representations[0].segmentationId, representations[0].type, true ); expect(console.debug).toHaveBeenCalledTimes(1); expect(console.debug).toHaveBeenCalledWith( 'No segmentation representation found for the given viewportId and segmentationId' ); expect( cstSegmentation.config.visibility.setSegmentationRepresentationVisibility ).not.toHaveBeenCalled(); }); }); describe('getViewportIdsWithSegmentation', () => { it('should get the viewport ids with the segmentation', () => { const segmentationId = 'segmentationId'; const viewportIds = ['viewportId1', 'viewportId2']; jest .spyOn(cstSegmentation.state, 'getViewportIdsWithSegmentation') .mockReturnValue(viewportIds); const returnedViewportIds = service.getViewportIdsWithSegmentation(segmentationId); expect(cstSegmentation.state.getViewportIdsWithSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getViewportIdsWithSegmentation).toHaveBeenCalledWith( segmentationId ); expect(returnedViewportIds).toEqual(viewportIds); }); }); describe('clearSegmentationRepresentations', () => { it('should clear the segmentation representations', () => { const viewportId = 'viewportId'; jest.spyOn(service, 'removeRepresentationsFromViewport').mockReturnValue(undefined); service.clearSegmentationRepresentations(viewportId); expect(service.removeRepresentationsFromViewport).toHaveBeenCalledTimes(1); expect(service.removeRepresentationsFromViewport).toHaveBeenCalledWith(viewportId); }); }); describe('remove', () => { it('should remove the segmentation', () => { const segmentationId = 'segmentationId'; jest.spyOn(cstSegmentation.state, 'removeSegmentation').mockReturnValue(undefined); service.remove(segmentationId); expect(cstSegmentation.state.removeSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.removeSegmentation).toHaveBeenCalledWith(segmentationId); }); }); describe('removeAllSegmentations', () => { it('should remove all segmentations', () => { jest.spyOn(cstSegmentation.state, 'removeAllSegmentations').mockReturnValue(undefined); service.removeAllSegmentations(); expect(cstSegmentation.state.removeAllSegmentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.removeAllSegmentations).toHaveBeenCalledWith(); }); }); describe('removeRepresentationsFromViewport', () => { it('should remove the segmentation representations', () => { const viewportId = 'viewportId'; const specifier = { segmentationId: 'segmentationId', type: csToolsEnums.SegmentationRepresentations.Labelmap, }; jest.spyOn(cstSegmentation, 'removeSegmentationRepresentations').mockReturnValue(undefined); service.removeRepresentationsFromViewport(viewportId, specifier); expect(cstSegmentation.removeSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(cstSegmentation.removeSegmentationRepresentations).toHaveBeenCalledWith( viewportId, specifier ); }); }); describe('jumpToSegmentCenter', () => { it('should early return if the center is not found', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; jest.spyOn(console, 'warn').mockReturnValue(undefined); jest.spyOn(cstSegmentation.state, 'getSegmentation').mockReturnValue(null); jest.spyOn(service, 'getViewportIdsWithSegmentation'); service.jumpToSegmentCenter(segmentationId, segmentIndex); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( 'No center found for segmentation', segmentationId, segmentIndex ); expect(service.getViewportIdsWithSegmentation).not.toHaveBeenCalled(); }); it('should correctly handle scenario where viewportId is provided', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; const viewportId = 'viewportId'; const viewport = { jumpToWorld: jest.fn() }; const segmentationWithCenter = { ...mockCornerstoneSegmentation, segments: { ...mockCornerstoneSegmentation.segments, [segmentIndex]: { ...mockCornerstoneSegmentation.segments[segmentIndex], cachedStats: { center: { image: [1, 1, 1], world: [10, 10, 10] } }, }, }, }; jest.spyOn(cstSegmentation.state, 'getSegmentation').mockReturnValue(segmentationWithCenter); jest.spyOn(service, 'getViewportIdsWithSegmentation'); // @ts-expect-error - mock only needed properties getEnabledElementByViewportId.mockReturnValue({ viewport }); jest.spyOn(service, 'highlightSegment').mockReturnValue(undefined); service.jumpToSegmentCenter(segmentationId, segmentIndex, viewportId); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith(segmentationId); expect(service.getViewportIdsWithSegmentation).not.toHaveBeenCalled(); expect(viewport.jumpToWorld).toHaveBeenCalledTimes(1); expect(viewport.jumpToWorld).toHaveBeenCalledWith([10, 10, 10]); expect(service.highlightSegment).toHaveBeenCalledTimes(1); expect(service.highlightSegment).toHaveBeenCalledWith( segmentationId, segmentIndex, viewportId, 0.9, 750, false, EasingFunctionEnum.EASE_IN_OUT ); }); it('navigates a native viewport via setViewReference (no jumpToWorld) and still highlights', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; const viewportId = 'viewportId'; // A native PlanarViewport: csUtils.isGenericViewport is true (setDisplaySets + // setDisplaySetPresentation + setViewState) and it has setViewReference but NO // jumpToWorld, so jumpToSegmentCenter routes to the next twin. const viewport = { setDisplaySets: jest.fn(), setDisplaySetPresentation: jest.fn(), setViewState: jest.fn(), setViewReference: jest.fn(), render: jest.fn(), }; const segmentationWithCenter = { ...mockCornerstoneSegmentation, segments: { ...mockCornerstoneSegmentation.segments, [segmentIndex]: { ...mockCornerstoneSegmentation.segments[segmentIndex], cachedStats: { center: { image: [1, 1, 1], world: [10, 10, 10] } }, }, }, }; jest.spyOn(cstSegmentation.state, 'getSegmentation').mockReturnValue(segmentationWithCenter); // @ts-expect-error - mock only needed properties getEnabledElementByViewportId.mockReturnValue({ viewport }); jest.spyOn(service, 'highlightSegment').mockReturnValue(undefined); service.jumpToSegmentCenter(segmentationId, segmentIndex, viewportId); // native jump: a view reference centered on the segment world point, then render expect(viewport.setViewReference).toHaveBeenCalledTimes(1); expect(viewport.setViewReference).toHaveBeenCalledWith({ cameraFocalPoint: [10, 10, 10] }); expect(viewport.render).toHaveBeenCalledTimes(1); // the recenter happened, so the highlight still runs expect(service.highlightSegment).toHaveBeenCalledTimes(1); }); it('should correctly handle custom animation parameters', () => { const segmentationId = 'segmentationId'; const segmentIndex = 1; const viewportId = 'viewportId'; const viewport = { jumpToWorld: jest.fn() }; const segmentationWithNamedStatsCenter = { ...mockCornerstoneSegmentation, segments: { ...mockCornerstoneSegmentation.segments, [segmentIndex]: { ...mockCornerstoneSegmentation.segments[segmentIndex], cachedStats: { namedStats: { center: { value: [1, 1, 1] } }, }, }, }, }; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(segmentationWithNamedStatsCenter); jest.spyOn(service, 'getViewportIdsWithSegmentation').mockReturnValue([viewportId]); // @ts-expect-error - mock only needed properties getEnabledElementByViewportId.mockReturnValue({ viewport }); jest.spyOn(service, 'highlightSegment').mockReturnValue(undefined); service.jumpToSegmentCenter( segmentationId, segmentIndex, undefined, 0.8, true, 800, false, EasingFunctionEnum.LINEAR ); expect(service.getViewportIdsWithSegmentation).toHaveBeenCalledTimes(1); expect(service.getViewportIdsWithSegmentation).toHaveBeenCalledWith(segmentationId); expect(viewport.jumpToWorld).toHaveBeenCalledTimes(1); expect(viewport.jumpToWorld).toHaveBeenCalledWith([1, 1, 1]); expect(service.highlightSegment).toHaveBeenCalledTimes(1); expect(service.highlightSegment).toHaveBeenCalledWith( segmentationId, segmentIndex, viewportId, 0.8, 800, false, EasingFunctionEnum.LINEAR ); }); }); describe('highlightSegment', () => { describe('LABELMAP Segmentation', () => { it('should correctly handle scenario where viewportId is provided', () => { const segmentIndex = 1; const viewportId = 'viewportId'; const initialFillAlpha = 0.3; const animationDuration = 750; const animationFunctionConstant = 0.85; const easingFunction = jest.fn(() => animationFunctionConstant); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(service, 'getViewportIdsWithSegmentation'); jest.spyOn(service, 'getSegmentationRepresentations').mockReturnValue(representations); jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({ fillAlpha: initialFillAlpha, }); jest.spyOn(window, 'requestAnimationFrame').mockReturnValue(undefined); jest.spyOn(EasingFunctionMap, 'get').mockReturnValue(easingFunction); service.highlightSegment( representations[0].segmentationId, segmentIndex, viewportId, 0.9, animationDuration, false, EasingFunctionEnum.EASE_IN_OUT ); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledTimes(1); expect(cstSegmentation.state.getSegmentation).toHaveBeenCalledWith( representations[0].segmentationId ); expect(service.getViewportIdsWithSegmentation).not.toHaveBeenCalled(); expect(service.getSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(service.getSegmentationRepresentations).toHaveBeenCalledWith(viewportId, { segmentationId: representations[0].segmentationId, }); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledWith({ viewportId, segmentationId: representations[0].segmentationId, type: csToolsEnums.SegmentationRepresentations.Labelmap, segmentIndex, }); expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1); expect(window.requestAnimationFrame).toHaveBeenCalledWith(expect.any(Function)); const animationCallback = jest.mocked(window.requestAnimationFrame).mock.calls[0][0]; // during animation call animationCallback(0); expect(EasingFunctionMap.get).toHaveBeenCalledTimes(1); expect(EasingFunctionMap.get).toHaveBeenCalledWith(EasingFunctionEnum.EASE_IN_OUT); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledWith( { segmentationId: representations[0].segmentationId, segmentIndex, type: csToolsEnums.SegmentationRepresentations.Labelmap, }, { fillAlpha: animationFunctionConstant, } ); expect(easingFunction).toHaveBeenCalledTimes(1); expect(easingFunction).toHaveBeenCalledWith(0, initialFillAlpha); expect(window.requestAnimationFrame).toHaveBeenCalledTimes(2); expect(window.requestAnimationFrame).toHaveBeenCalledWith(expect.any(Function)); // end of animation call animationCallback(animationDuration); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledTimes(3); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledWith( { segmentationId: representations[0].segmentationId, segmentIndex, type: csToolsEnums.SegmentationRepresentations.Labelmap, }, {}, false ); expect(window.requestAnimationFrame).not.toHaveBeenCalledTimes(3); }); it('should throw if hideOthers is true', () => { const segmentIndex = 1; jest.spyOn(window, 'clearInterval').mockReturnValue(undefined); jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(service, 'getViewportIdsWithSegmentation').mockReturnValue([viewportId]); jest.spyOn(service, 'getSegmentationRepresentations').mockReturnValue(representations); expect(() => service.highlightSegment(representations[0].segmentationId, segmentIndex) ).toThrow('hideOthers is not working right now'); expect(window.clearInterval).not.toHaveBeenCalled(); expect(service.getViewportIdsWithSegmentation).toHaveBeenCalledTimes(1); expect(service.getViewportIdsWithSegmentation).toHaveBeenCalledWith( representations[0].segmentationId ); expect(service.getSegmentationRepresentations).toHaveBeenCalledTimes(1); expect(service.getSegmentationRepresentations).toHaveBeenCalledWith(viewportId, { segmentationId: representations[0].segmentationId, }); }); }); describe('CONTOUR Segmentation', () => { it('should correctly handle scenario where viewportId is provided', () => { const segmentIndex = 1; const viewportId = 'viewportId'; const initialOutlineWidth = 3; const animationDuration = 750; const animationFunctionConstant = 5; const easingFunction = jest.fn(() => animationFunctionConstant); const contourRepresentations = [ { ...representations[0], type: csToolsEnums.SegmentationRepresentations.Contour, }, ]; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(service, 'getViewportIdsWithSegmentation'); jest .spyOn(service, 'getSegmentationRepresentations') .mockReturnValue(contourRepresentations); jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({ outlineWidth: initialOutlineWidth, }); jest.spyOn(window, 'requestAnimationFrame').mockReturnValue(undefined); jest.spyOn(EasingFunctionMap, 'get').mockReturnValue(easingFunction); jest.spyOn(cstSegmentation.config.style, 'resetToGlobalStyle').mockReturnValue(undefined); service.highlightSegment( contourRepresentations[0].segmentationId, segmentIndex, viewportId, 0.9, animationDuration, false, EasingFunctionEnum.LINEAR ); expect(service.getViewportIdsWithSegmentation).not.toHaveBeenCalled(); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.getStyle).toHaveBeenCalledWith({ type: csToolsEnums.SegmentationRepresentations.Contour, }); expect(window.requestAnimationFrame).toHaveBeenCalledTimes(1); expect(window.requestAnimationFrame).toHaveBeenCalledWith(expect.any(Function)); const animationCallback = jest.mocked(window.requestAnimationFrame).mock.calls[0][0]; const approximateStartTime = performance.now(); // during animation call animationCallback(approximateStartTime + 200); expect(EasingFunctionMap.get).toHaveBeenCalledTimes(1); expect(EasingFunctionMap.get).toHaveBeenCalledWith(EasingFunctionEnum.LINEAR); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledWith( { segmentationId: contourRepresentations[0].segmentationId, segmentIndex, type: csToolsEnums.SegmentationRepresentations.Contour, }, { outlineWidth: animationFunctionConstant, } ); expect(easingFunction).toHaveBeenCalledTimes(1); expect(easingFunction).toHaveBeenCalledWith(expect.any(Number), initialOutlineWidth, 5); expect(window.requestAnimationFrame).toHaveBeenCalledTimes(2); expect(window.requestAnimationFrame).toHaveBeenCalledWith(expect.any(Function)); // end of animation call animationCallback(approximateStartTime + animationDuration); expect(cstSegmentation.config.style.setStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.resetToGlobalStyle).toHaveBeenCalledTimes(1); expect(cstSegmentation.config.style.resetToGlobalStyle).toHaveBeenCalledWith(); expect(window.requestAnimationFrame).not.toHaveBeenCalledTimes(3); }); }); it('should clear interval if it exists', () => { expect(service.highlightIntervalId).toBe(null); service.highlightIntervalId = 'intervalId'; jest .spyOn(cstSegmentation.state, 'getSegmentation') .mockReturnValue(mockCornerstoneSegmentation); jest.spyOn(service, 'getViewportIdsWithSegmentation'); jest.spyOn(service, 'getSegmentationRepresentations').mockReturnValue(representations); jest.spyOn(cstSegmentation.config.style, 'getStyle').mockReturnValue({ fillAlpha: 0.3, }); jest.spyOn(window, 'requestAnimationFrame').mockReturnValue(undefined); jest.spyOn(window, 'clearInterval').mockReturnValue(undefined); service.highlightSegment(representations[0].segmentationId, 1, viewportId, 0.9, 750, false); expect(window.clearInterval).toHaveBeenCalledTimes(1); expect(window.clearInterval).toHaveBeenCalledWith('intervalId'); }); }); describe('_onSegmentationDataModifiedFromSource', () => { it('should broadcast the event', () => { jest.spyOn(eventTarget, 'addEventListener').mockReturnValue(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_DATA_MODIFIED, callback); service.onModeEnter(); const _onSegmentationDataModifiedFromSource = jest.mocked(eventTarget.addEventListener).mock .calls[2][1]; _onSegmentationDataModifiedFromSource({ detail: { segmentationId: 'segmentationId', }, }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: 'segmentationId', }); }); }); describe('_onSegmentationRepresentationModifiedFromSource', () => { it('should broadcast the event', () => { jest.spyOn(eventTarget, 'addEventListener').mockReturnValue(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_REPRESENTATION_MODIFIED, callback); service.onModeEnter(); const _onSegmentationRepresentationModifiedFromSource = jest.mocked( eventTarget.addEventListener ).mock.calls[3][1]; _onSegmentationRepresentationModifiedFromSource({ detail: { segmentationId: 'segmentationId', viewportId: 'viewportId', }, }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: 'segmentationId', viewportId: 'viewportId', }); }); }); describe('_onSegmentationModifiedFromSource', () => { it('should broadcast the event', () => { jest.spyOn(eventTarget, 'addEventListener').mockReturnValue(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_MODIFIED, callback); service.onModeEnter(); const _onSegmentationModifiedFromSource = jest.mocked(eventTarget.addEventListener).mock .calls[0][1]; _onSegmentationModifiedFromSource({ detail: { segmentationId: 'segmentationId', }, }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: 'segmentationId', }); }); }); describe('_onSegmentationAddedFromSource', () => { it('should broadcast the event', () => { jest.spyOn(eventTarget, 'addEventListener').mockReturnValue(undefined); const callback = jest.fn(); service.subscribe(service.EVENTS.SEGMENTATION_ADDED, callback); service.onModeEnter(); const _onSegmentationAddedFromSource = jest.mocked(eventTarget.addEventListener).mock .calls[6][1]; _onSegmentationAddedFromSource({ detail: { segmentationId: 'segmentationId', }, }); expect(callback).toHaveBeenCalledTimes(1); expect(callback).toHaveBeenCalledWith({ segmentationId: 'segmentationId', }); }); }); });