diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index 3a463ef48..ee29fcf36 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -650,25 +650,27 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi csToolsUtils.stackContextPrefetch.enable(element); }); - let imageIdsToSet = imageIds; - const overlayProcessingResult = this._processExtraDisplaySetsForViewport(viewport); - imageIdsToSet = overlayProcessingResult?.imageIds ?? imageIdsToSet; + const overlayProcessingResults = this._processExtraDisplaySetsForViewport(viewport); const referencedImageId = presentations?.positionPresentation?.viewReference?.referencedImageId; if (referencedImageId) { - initialImageIndexToUse = imageIdsToSet.indexOf(referencedImageId); + initialImageIndexToUse = imageIds.indexOf(referencedImageId); } if (initialImageIndexToUse === undefined || initialImageIndexToUse === null) { initialImageIndexToUse = this._getInitialImageIndexForViewport(viewportInfo, imageIds) || 0; } - return viewport.setStack(imageIdsToSet, initialImageIndexToUse).then(() => { + return viewport.setStack(imageIds, initialImageIndexToUse).then(() => { viewport.setProperties({ ...properties }); this.setPresentations(viewport.id, presentations, viewportInfo); - if (overlayProcessingResult?.addOverlayFn) { - overlayProcessingResult.addOverlayFn(); + if (overlayProcessingResults?.length) { + overlayProcessingResults.forEach(overlayProcessingResult => { + if (overlayProcessingResult?.addOverlayFn) { + overlayProcessingResult.addOverlayFn(); + } + }); } if (displayArea) { @@ -874,28 +876,35 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi }); // For SEG and RT viewports - const { addOverlayFn, imageIds } = this._processExtraDisplaySetsForViewport(viewport) || {}; + const overlayProcessingResults = this._processExtraDisplaySetsForViewport(viewport) || []; + if (!filteredVolumeInputArray.length && overlayProcessingResults?.length) { + overlayProcessingResults.forEach(({ imageIds, addOverlayFn }) => { + if (addOverlayFn) { + // if there is no volume input array, and there is an addOverlayFn, means we need to take + // care of the background overlay display set first then the addOverlayFn will add the + // SEG displaySet + const sampleImageId = imageIds[0]; + const backgroundDisplaySet = displaySetService.getDisplaySetsBy( + displaySet => + !displaySet.isOverlayDisplaySet && + displaySet.images.some(image => image.imageId === sampleImageId) + ); - if (!filteredVolumeInputArray.length && addOverlayFn) { - // if there is no volume input array, and there is an addOverlayFn, means we need to take - // care of the background overlay display set first then the addOverlayFn will add the - // SEG displaySet - const sampleImageId = imageIds[0]; - const backgroundDisplaySet = displaySetService.getDisplaySetsBy( - displaySet => - !displaySet.isOverlayDisplaySet && - displaySet.images.some(image => image.imageId === sampleImageId) - ); - - if (backgroundDisplaySet.length !== 1) { - throw new Error('Background display set not found'); - } + if (backgroundDisplaySet.length !== 1) { + throw new Error('Background display set not found'); + } + } + }); } await viewport.setVolumes(volumeInputArray); - if (addOverlayFn) { - addOverlayFn(); + if (overlayProcessingResults?.length) { + overlayProcessingResults.forEach(({ addOverlayFn }) => { + if (addOverlayFn) { + addOverlayFn(); + } + }); } viewport.render(); @@ -934,30 +943,31 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const displaySetInstanceUIDs = this.viewportsDisplaySets.get(viewport.id); // Find overlay display sets (e.g. SEG, RTSTRUCT) - const overlayDisplaySet = displaySetInstanceUIDs + const overlayDisplaySets = displaySetInstanceUIDs .map(displaySetService.getDisplaySetByUID) - .find(displaySet => displaySet?.isOverlayDisplaySet); + .filter(displaySet => displaySet?.isOverlayDisplaySet); // if it is only the overlay displaySet, then we need to get the reference // displaySet imageIds and set them as the imageIds for the viewport, // here we can do some logic if the reference is missing // then find the most similar match of displaySet instead - if (!overlayDisplaySet) { + if (!overlayDisplaySets?.length) { return; } - let imageIds; - if (overlayDisplaySet.referencedDisplaySetInstanceUID) { - const referenceDisplaySet = displaySetService.getDisplaySetByUID( - overlayDisplaySet.referencedDisplaySetInstanceUID - ); - imageIds = referenceDisplaySet.images.map(image => image.imageId); - } - - return { - imageIds, - addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport), - }; + return overlayDisplaySets.map(overlayDisplaySet => { + let imageIds; + if (overlayDisplaySet.referencedDisplaySetInstanceUID) { + const referenceDisplaySet = displaySetService.getDisplaySetByUID( + overlayDisplaySet.referencedDisplaySetInstanceUID + ); + imageIds = referenceDisplaySet.images.map(image => image.imageId); + } + return { + imageIds, + addOverlayFn: () => this.addOverlayRepresentationForDisplaySet(overlayDisplaySet, viewport), + }; + }); } private addOverlayRepresentationForDisplaySet( diff --git a/platform/core/src/services/UINotificationService/index.ts b/platform/core/src/services/UINotificationService/index.ts index eada24005..8e88374a3 100644 --- a/platform/core/src/services/UINotificationService/index.ts +++ b/platform/core/src/services/UINotificationService/index.ts @@ -41,6 +41,10 @@ class UINotificationService { * @returns undefined */ public hide(id: string) { + if (process.env.TEST_ENV === 'true') { + return; + } + return serviceImplementation._hide(id); } @@ -105,6 +109,10 @@ class UINotificationService { onClick: () => void; }; }): string { + if (process.env.TEST_ENV === 'true') { + return; + } + if (promise && promiseMessages) { const loadingId = serviceImplementation._show({ title, diff --git a/playwright.config.ts b/playwright.config.ts index 008943039..11d087b64 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -21,7 +21,7 @@ export default defineConfig({ use: { baseURL: 'http://localhost:3335', trace: 'on-first-retry', - video: 'on', + video: 'on-first-retry', testIdAttribute: 'data-cy', actionTimeout: 10_000, }, diff --git a/tests/MPRThenRTOverlayNoHydration.spec.ts b/tests/MPRThenRTOverlayNoHydration.spec.ts index 14192ae60..1f36c8fe6 100644 --- a/tests/MPRThenRTOverlayNoHydration.spec.ts +++ b/tests/MPRThenRTOverlayNoHydration.spec.ts @@ -35,9 +35,9 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me // Wait 5 seconds for RT to load. This is necessary in particular when screen shots are added or replaced. await page.waitForTimeout(5000); - await checkForScreenshot( + await checkForScreenshot({ page, - page, - screenShotPaths.mprThenRTOverlayNoHydration.mprPostRTOverlayNoHydration - ); + screenshotPath: screenShotPaths.mprThenRTOverlayNoHydration.mprPostRTOverlayNoHydration, + normalizedClip: { x: 0, y: 0, width: 1.0, height: 0.75 }, // clip to avoid any popups concerning surface creation and clipping + }); }); diff --git a/tests/MultipleSegmentationDataOverlays.spec.ts b/tests/MultipleSegmentationDataOverlays.spec.ts new file mode 100644 index 000000000..64166fabb --- /dev/null +++ b/tests/MultipleSegmentationDataOverlays.spec.ts @@ -0,0 +1,77 @@ +import { test } from 'playwright-test-coverage'; +import { visitStudy, checkForScreenshot, screenShotPaths } from './utils'; +import { press } from './utils/keyboardUtils'; + +test.beforeEach(async ({ page }) => { + const studyInstanceUID = '1.3.6.1.4.1.32722.99.99.239341353911714368772597187099978969331'; + const mode = 'viewer'; + await visitStudy(page, studyInstanceUID, mode, 2000); +}); + +test('should display multiple segmentation overlays (both SEG and RT)', async ({ page }) => { + await page.getByTestId('side-panel-header-right').click(); + + // Add multiple segmentation overlays and ensure the overlay menu reflects this change. + await page.getByTestId('dataOverlayMenu-default-btn').click(); + + await page.getByTestId('AddSegmentationDataOverlay-default').click(); + await page.getByText('SELECT A SEGMENTATION').click(); + await page.getByTestId('2d-tta_nnU-Net_Segmentation').click(); + + await page.getByTestId('AddSegmentationDataOverlay-default').click(); + await page.getByText('SELECT A SEGMENTATION').click(); + await page.getByTestId('Segmentation').click(); + + await page.getByTestId('AddSegmentationDataOverlay-default').click(); + await page.getByText('SELECT A SEGMENTATION').click(); + await page.getByTestId('3d_lowres-tta_nnU-Net_Segmentation').click(); + + await checkForScreenshot({ + page, + screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.threeSegOverlaysInOverlayMenu, + }); + + // Hide the overlay menu and then show it again. The overlays from before should still be displayed. + await page.getByTestId('dataOverlayMenu-default-btn').click(); // hide + await page.getByTestId('dataOverlayMenu-default-btn').click(); // show + + await checkForScreenshot({ + page, + screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.threeSegOverlaysInOverlayMenu, + }); + + await page.getByTestId('dataOverlayMenu-default-btn').click(); // hide + + // Navigate to image 56. + await press({ page, key: 'ArrowDown', nTimes: 55 }); + + await page.waitForTimeout(5000); + + await checkForScreenshot({ + page, + screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaysDisplayed, + }); + + // Now add the RT overlay + await page.getByTestId('dataOverlayMenu-default-btn').click(); + + await page.getByTestId('AddSegmentationDataOverlay-default').click(); + await page.getByText('SELECT A SEGMENTATION').click(); + await page.getByTestId('Series 3 - RTSTRUCT').click(); + + await page.waitForTimeout(5000); + + await checkForScreenshot({ + page, + screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaySEGsAndRTDisplayed, + }); + + // Hide the overlay menu and then show it again. The overlays from before should still be displayed. + await page.getByTestId('dataOverlayMenu-default-btn').click(); // hide + await page.getByTestId('dataOverlayMenu-default-btn').click(); // show + + await checkForScreenshot({ + page, + screenshotPath: screenShotPaths.multipleSegmentationDataOverlays.overlaySEGsAndRTDisplayed, + }); +}); diff --git a/tests/RTDataOverlayNoHydrationThenMPR.spec.ts b/tests/RTDataOverlayNoHydrationThenMPR.spec.ts index 06ff28b5f..437460bf3 100644 --- a/tests/RTDataOverlayNoHydrationThenMPR.spec.ts +++ b/tests/RTDataOverlayNoHydrationThenMPR.spec.ts @@ -19,6 +19,8 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me // Hide the overlay menu. await page.getByTestId('dataOverlayMenu-default-btn').click(); + await page.waitForTimeout(5000); + await checkForScreenshot( page, page, @@ -28,6 +30,8 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me await page.getByTestId('Layout').click(); await page.getByTestId('MPR').click(); + await page.waitForTimeout(5000); + await checkForScreenshot( page, page, diff --git a/tests/RTHydrationFromMPR.spec.ts b/tests/RTHydrationFromMPR.spec.ts index 63b4b98c1..f908d4c93 100644 --- a/tests/RTHydrationFromMPR.spec.ts +++ b/tests/RTHydrationFromMPR.spec.ts @@ -13,14 +13,20 @@ test('should hydrate an RTSTRUCT from MPR', async ({ page }) => { await page.getByTestId('Layout').click(); await page.getByTestId('MPR').click(); + await page.waitForTimeout(5000); + await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprBeforeRT); await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); + await page.waitForTimeout(5000); + await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRT); await page.getByTestId('yes-hydrate-btn').click(); + await page.waitForTimeout(5000); + await checkForScreenshot(page, page, screenShotPaths.rtHydrationFromMPR.mprAfterRTHydrated); await page.getByTestId('Layout').click(); diff --git a/tests/RTHydrationThenMPR.spec.ts b/tests/RTHydrationThenMPR.spec.ts index c6e16d221..7e2ceec81 100644 --- a/tests/RTHydrationThenMPR.spec.ts +++ b/tests/RTHydrationThenMPR.spec.ts @@ -13,11 +13,15 @@ test('should hydrate an RTSTRUCT and then launch MPR', async ({ page }) => { await page.getByTestId('yes-hydrate-btn').click(); + await page.waitForTimeout(5000); + await checkForScreenshot(page, page, screenShotPaths.rtHydrationThenMPR.rtPostHydration); await page.getByTestId('Layout').click(); await page.getByTestId('Axial Primary').click(); + await page.waitForTimeout(5000); + await checkForScreenshot( page, page, diff --git a/tests/RTNoHydrationThenMPR.spec.ts b/tests/RTNoHydrationThenMPR.spec.ts index 3b34e66e8..84d364062 100644 --- a/tests/RTNoHydrationThenMPR.spec.ts +++ b/tests/RTNoHydrationThenMPR.spec.ts @@ -11,10 +11,14 @@ test('should launch MPR with unhydrated RTSTRUCT', async ({ page }) => { await page.getByTestId('side-panel-header-right').click(); await page.getByTestId('study-browser-thumbnail-no-image').dblclick(); + await page.waitForTimeout(5000); + await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPreMPR); await page.getByTestId('Layout').click(); await page.getByTestId('MPR').click(); + await page.waitForTimeout(5000); + await checkForScreenshot(page, page, screenShotPaths.rtNoHydrationThenMPR.rtNoHydrationPostMPR); }); diff --git a/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPostRTOverlayNoHydration.png b/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPostRTOverlayNoHydration.png index 2ff004d43..c43023205 100644 Binary files a/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPostRTOverlayNoHydration.png and b/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPostRTOverlayNoHydration.png differ diff --git a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaySEGsAndRTDisplayed.png b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaySEGsAndRTDisplayed.png new file mode 100644 index 000000000..583070fc8 Binary files /dev/null and b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaySEGsAndRTDisplayed.png differ diff --git a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaysDisplayed.png b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaysDisplayed.png new file mode 100644 index 000000000..694459489 Binary files /dev/null and b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaysDisplayed.png differ diff --git a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/threeSegOverlaysInOverlayMenu.png b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/threeSegOverlaysInOverlayMenu.png new file mode 100644 index 000000000..84005802a Binary files /dev/null and b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/threeSegOverlaysInOverlayMenu.png differ diff --git a/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPostMpr.png b/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPostMpr.png index d507a1bef..d69a33157 100644 Binary files a/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPostMpr.png and b/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPostMpr.png differ diff --git a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydratedAfterLayoutChange.png b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydratedAfterLayoutChange.png index b6db95ff0..ce6eb32fc 100644 Binary files a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydratedAfterLayoutChange.png and b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydratedAfterLayoutChange.png differ diff --git a/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydrationMPRAxialPrimary.png b/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydrationMPRAxialPrimary.png index 0ddc408a4..8e937e81f 100644 Binary files a/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydrationMPRAxialPrimary.png and b/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydrationMPRAxialPrimary.png differ diff --git a/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPostMpr.png b/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPostMpr.png index a9f5f903a..042a9681e 100644 Binary files a/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPostMpr.png and b/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPostMpr.png differ diff --git a/tests/utils/checkForScreenshot.ts b/tests/utils/checkForScreenshot.ts index 931cffe21..36054d3c1 100644 --- a/tests/utils/checkForScreenshot.ts +++ b/tests/utils/checkForScreenshot.ts @@ -9,6 +9,12 @@ type CheckForScreenshotProps = { delay?: number; maxDiffPixelRatio?: number; threshold?: number; + normalizedClip?: { + x: number; + y: number; + width: number; + height: number; + }; }; const _checkForScreenshot = async (props: CheckForScreenshotProps) => { @@ -20,15 +26,34 @@ const _checkForScreenshot = async (props: CheckForScreenshotProps) => { delay = 500, maxDiffPixelRatio = 0.02, threshold = 0.05, + normalizedClip, } = props; await page.waitForLoadState('networkidle'); for (let i = 0; i < attempts; i++) { try { + let clip; + if (normalizedClip) { + let boundingBox; + if (locator === page) { + boundingBox = { x: 0, y: 0, ...(await page.viewportSize()) }; + } else { + boundingBox = await (locator as Locator).boundingBox(); + } + + clip = { + x: normalizedClip.x * boundingBox.width, + y: normalizedClip.y * boundingBox.height, + width: normalizedClip.width * boundingBox.width, + height: normalizedClip.height * boundingBox.height, + }; + } + await expect(locator).toHaveScreenshot(screenshotPath, { maxDiffPixelRatio, threshold, + clip, }); return true; } catch (error) { diff --git a/tests/utils/screenShotPaths.ts b/tests/utils/screenShotPaths.ts index 1d1459643..48ca6400a 100644 --- a/tests/utils/screenShotPaths.ts +++ b/tests/utils/screenShotPaths.ts @@ -173,6 +173,11 @@ const screenShotPaths = { overlaySegmentation: 'overlaySegmentation.png', noOverlay: 'noOverlay.png', }, + multipleSegmentationDataOverlays: { + threeSegOverlaysInOverlayMenu: 'threeSegOverlaysInOverlayMenu.png', + overlaysDisplayed: 'overlaysDisplayed.png', + overlaySEGsAndRTDisplayed: 'overlaySEGsAndRTDisplayed.png', + }, }; export { screenShotPaths };