diff --git a/addOns/externals/devDependencies/package.json b/addOns/externals/devDependencies/package.json index b90ef9b5330..dc7bc7e446d 100644 --- a/addOns/externals/devDependencies/package.json +++ b/addOns/externals/devDependencies/package.json @@ -52,10 +52,10 @@ "extract-css-chunks-webpack-plugin": "4.10.0", "html-webpack-plugin": "5.6.3", "husky": "3.1.0", - "jest": "29.7.0", + "jest": "30.2.0", "jest-canvas-mock": "2.5.2", - "jest-environment-jsdom": "29.7.0", - "jest-junit": "6.4.0", + "jest-environment-jsdom": "30.2.0", + "jest-junit": "16.0.0", "lerna": "9.0.4", "lint-staged": "9.5.0", "mini-css-extract-plugin": "2.9.2", diff --git a/babel.config.js b/babel.config.js index bcc3570792e..ad8f27e6189 100644 --- a/babel.config.js +++ b/babel.config.js @@ -26,7 +26,9 @@ module.exports = { '@babel/preset-typescript', ], plugins: [ - 'babel-plugin-istanbul', + // jest's babel coverage provider injects babel-plugin-istanbul when + // --collectCoverage is set; adding it here too makes babel 7 (pulled in + // by jest 30) throw "Duplicate plugin/preset detected". '@babel/plugin-transform-object-rest-spread', '@babel/plugin-syntax-dynamic-import', '@babel/plugin-transform-regenerator', diff --git a/bunfig.toml b/bunfig.toml index 3bfa8645410..6cc71934c45 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,2 +1,3 @@ [install] frozenLockfile = true +minimumReleaseAge = 172800 diff --git a/bunfig.update-lockfile.toml b/bunfig.update-lockfile.toml index c4961dc06b3..259f1fc7de0 100644 --- a/bunfig.update-lockfile.toml +++ b/bunfig.update-lockfile.toml @@ -1,2 +1,3 @@ [install] frozenLockfile = false +minimumReleaseAge = 172800 diff --git a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts index 60d1c471418..43433361ea4 100644 --- a/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts +++ b/extensions/cornerstone-dicom-sr/src/tools/DICOMSRDisplayTool.ts @@ -69,7 +69,12 @@ export default class DICOMSRDisplayTool extends AnnotationTool { trackingUniqueIdentifiers.includes(annotation.data?.TrackingUniqueIdentifier) ); - if (!viewport._actors?.size) { + const hasActors = + typeof viewport.getActors === 'function' + ? viewport.getActors().length > 0 + : Boolean(viewport._actors?.size); + + if (!hasActors) { return; } diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx index edb0b22b19e..b7d3f004a2e 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportImageScrollbar.tsx @@ -1,7 +1,8 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; -import { Enums, VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core'; +import { Enums, utilities as csUtils } from '@cornerstonejs/core'; import { ImageScrollbar } from '@ohif/ui-next'; +import { isVolume3DViewportType } from '../../utils/getLegacyViewportType'; function CornerstoneImageScrollbar({ viewportData, @@ -40,7 +41,7 @@ function CornerstoneImageScrollbar({ const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport || viewport instanceof VolumeViewport3D) { + if (!viewport || isVolume3DViewportType(viewport)) { return; } @@ -69,7 +70,7 @@ function CornerstoneImageScrollbar({ const updateIndex = event => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport || viewport instanceof VolumeViewport3D) { + if (!viewport || isVolume3DViewportType(viewport)) { return; } const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail; diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar.tsx b/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar.tsx index 2e5987fe0e5..d58becb40be 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar.tsx +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/ViewportSliceProgressScrollbar.tsx @@ -1,6 +1,7 @@ import React, { useMemo } from 'react'; import PropTypes from 'prop-types'; -import { VolumeViewport3D, utilities as csUtils } from '@cornerstonejs/core'; +import { utilities as csUtils } from '@cornerstonejs/core'; +import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType'; import { SmartScrollbar, SmartScrollbarTrack, @@ -102,7 +103,7 @@ function ViewportSliceProgressScrollbar({ const onScrollbarValueChange = targetImageIndex => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport || viewport instanceof VolumeViewport3D) { + if (!viewport || isVolume3DViewportType(viewport)) { return; } diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/helpers.ts b/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/helpers.ts index 2f21d8238d4..0022ba277c7 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/helpers.ts +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/helpers.ts @@ -1,5 +1,6 @@ -import { Enums, VolumeViewport3D } from '@cornerstonejs/core'; +import { Enums } from '@cornerstonejs/core'; import { ViewportData } from './types'; +import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType'; export function getImageIndexFromEvent(event): number | undefined { const { imageIndex, newImageIdIndex = imageIndex, imageIdIndex } = event.detail; @@ -19,7 +20,7 @@ export function getViewportImageIds(viewportData: ViewportData): string[] { } export function isProgressFullMode(viewportData: ViewportData, viewport): boolean { - if (!viewportData || !viewport || viewport instanceof VolumeViewport3D) { + if (!viewportData || !viewport || isVolume3DViewportType(viewport)) { return false; } diff --git a/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/hooks.ts b/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/hooks.ts index b5dd76b5673..a0ca184483e 100644 --- a/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/hooks.ts +++ b/extensions/cornerstone/src/Viewport/Overlays/ViewportSliceProgressScrollbar/hooks.ts @@ -4,9 +4,9 @@ import { Enums, eventTarget, utilities, - VolumeViewport3D, } from '@cornerstonejs/core'; import { useByteArray } from '@ohif/ui-next'; +import { isVolume3DViewportType } from '../../../utils/getLegacyViewportType'; import { getImageIdFromCacheEvent, getImageIndexFromEvent, isProgressFullMode } from './helpers'; import { ImageSliceData, ViewportData } from './types'; @@ -98,7 +98,7 @@ export function useViewportSliceSync({ } const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (viewport && !(viewport instanceof VolumeViewport3D)) { + if (viewport && !isVolume3DViewportType(viewport)) { try { const currentImageIndex = viewport.getCurrentImageIdIndex(); const currentNumberOfSlices = viewport.getNumberOfSlices(); @@ -120,7 +120,7 @@ export function useViewportSliceSync({ const updateIndex = event => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport || viewport instanceof VolumeViewport3D) { + if (!viewport || isVolume3DViewportType(viewport)) { return; } diff --git a/extensions/cornerstone/src/commandsModule.ts b/extensions/cornerstone/src/commandsModule.ts index 0e6ea7687d5..98e3d495b99 100644 --- a/extensions/cornerstone/src/commandsModule.ts +++ b/extensions/cornerstone/src/commandsModule.ts @@ -1,11 +1,7 @@ import { getEnabledElement, - StackViewport, - VolumeViewport, utilities as csUtils, - Enums as CoreEnums, Types as CoreTypes, - BaseVolumeViewport, getRenderingEngines, } from '@cornerstonejs/core'; import { @@ -38,6 +34,12 @@ import { getFirstAnnotationSelected } from './utils/measurementServiceMappings/u import { getViewportEnabledElement } from './utils/getViewportEnabledElement'; import getActiveViewportEnabledElement from './utils/getActiveViewportEnabledElement'; import toggleVOISliceSync from './utils/toggleVOISliceSync'; +import { + isStackViewportType, + isOrthographicViewportType, + isVolume3DViewportType, + isVolumeViewportType, +} from './utils/getLegacyViewportType'; import { usePositionPresentationStore, useSegmentationPresentationStore, @@ -324,7 +326,7 @@ function commandsModule({ // Todo: check if PMAP modality should be handled such as SEG displaySet.Modality !== 'SEG' ? SegmentationRepresentations.Contour - : viewport.type === CoreEnums.ViewportType.VOLUME_3D + : isVolume3DViewportType(viewport) ? SegmentationRepresentations.Surface : SegmentationRepresentations.Labelmap; @@ -910,7 +912,7 @@ function commandsModule({ const { lower, upper } = csUtils.windowLevel.toLowHighRange(windowWidthNum, windowCenterNum); - if (viewport instanceof BaseVolumeViewport) { + if (isVolumeViewportType(viewport)) { const volumeId = actions.getVolumeIdForDisplaySet({ viewportId, displaySetInstanceUID, @@ -986,8 +988,12 @@ function commandsModule({ }, getVolumeIdForDisplaySet: ({ viewportId, displaySetInstanceUID }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (viewport instanceof BaseVolumeViewport) { - const volumeIds = viewport.getAllVolumeIds(); + // `instanceof BaseVolumeViewport` is false for GenericViewport compat adapters, + // so use the capability guard instead (cornerstone 5.x generic-viewport guide). + // A stack-mode adapter passes the guard but getAllVolumeIds() returns [], so the + // result is the same null as before on stacks. + if (csUtils.viewportSupportsVolumeId(viewport)) { + const volumeIds = (viewport as CoreTypes.IVolumeViewport).getAllVolumeIds(); const volumeId = volumeIds.find(id => id.includes(displaySetInstanceUID)); return volumeId; } @@ -1256,7 +1262,7 @@ function commandsModule({ } const { viewport } = enabledElement; - if (viewport instanceof StackViewport) { + if (isStackViewportType(viewport)) { if (direction) { const { parallelScale } = viewport.getCamera(); viewport.setCamera({ parallelScale: parallelScale * scaleFactor }); @@ -1286,9 +1292,9 @@ function commandsModule({ // -> Copied from cornerstone3D jumpToSlice\_getImageSliceData() let numberOfSlices = 0; - if (viewport instanceof StackViewport) { + if (isStackViewportType(viewport)) { numberOfSlices = viewport.getImageIds().length; - } else if (viewport instanceof VolumeViewport) { + } else if (isOrthographicViewportType(viewport)) { numberOfSlices = csUtils.getImageSliceDataForVolumeViewport(viewport).numberOfSlices; } else { throw new Error('Unsupported viewport type'); @@ -1344,11 +1350,11 @@ function commandsModule({ // HP takes priority over the default opacity colormap = { ...colormap, opacity: hpOpacity || opacity }; - if (viewport instanceof StackViewport) { + if (isStackViewportType(viewport)) { viewport.setProperties({ colormap }); } - if (viewport instanceof VolumeViewport) { + if (isOrthographicViewportType(viewport)) { if (!displaySetInstanceUID) { const { viewports } = viewportGridService.getState(); displaySetInstanceUID = viewports.get(viewportId)?.displaySetInstanceUIDs[0]; @@ -2145,7 +2151,7 @@ function commandsModule({ setViewportOrientation: ({ viewportId, orientation }) => { const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId); - if (!viewport || viewport.type !== CoreEnums.ViewportType.ORTHOGRAPHIC) { + if (!viewport || !isOrthographicViewportType(viewport)) { console.warn('Orientation can only be set on volume viewports'); return; } @@ -2227,7 +2233,7 @@ function commandsModule({ const { viewport } = enabledElement; - if (viewport instanceof BaseVolumeViewport) { + if (isVolumeViewportType(viewport)) { const camera = viewport.getCamera(); const rotAngle = (rotation * Math.PI) / 180; const rotMat = mat4.identity(new Float32Array(16)); diff --git a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx index 51cfe3ee668..c26dc134ea4 100644 --- a/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx +++ b/extensions/cornerstone/src/components/ViewportWindowLevel/ViewportWindowLevel.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useCallback, useState, ReactElement, useMemo } from ' import PropTypes from 'prop-types'; import debounce from 'lodash.debounce'; import { PanelSection, WindowLevel } from '@ohif/ui-next'; -import { BaseVolumeViewport, Enums, eventTarget } from '@cornerstonejs/core'; +import { Enums, eventTarget, utilities as csUtils, Types } from '@cornerstonejs/core'; import { useActiveViewportDisplaySets } from '@ohif/core'; import { getNodeOpacity, @@ -27,10 +27,16 @@ const ViewportWindowLevel = ({ const getViewportsWithVolumeIds = useCallback( (volumeIds: string[]) => { const renderingEngine = cornerstoneViewportService.getRenderingEngine(); - const viewports = renderingEngine.getVolumeViewports(); + // getVolumeViewports() was removed in the GenericViewport architecture + // (a PLANAR_NEXT viewport can be volume-capable without being a VolumeViewport). + // Official replacement: getViewports() + the viewportSupportsVolumeCompatibility + // capability guard (cornerstone codemod cornerstone3d/5/generic-viewport). + const viewports = renderingEngine + .getViewports() + .filter(csUtils.viewportSupportsVolumeCompatibility); return viewports.filter(vp => { - const viewportVolumeIds = vp instanceof BaseVolumeViewport ? vp.getAllVolumeIds() : []; + const viewportVolumeIds = (vp as Types.IVolumeViewport).getAllVolumeIds(); return ( volumeIds.length === viewportVolumeIds.length && volumeIds.every(volumeId => viewportVolumeIds.includes(volumeId)) @@ -124,8 +130,9 @@ const ViewportWindowLevel = ({ return; } - const viewportVolumeIds = - viewport instanceof BaseVolumeViewport ? viewport.getAllVolumeIds() : []; + const viewportVolumeIds = csUtils.viewportSupportsVolumeId(viewport) + ? (viewport as Types.IVolumeViewport).getAllVolumeIds() + : []; const viewports = getViewportsWithVolumeIds(viewportVolumeIds); viewports.forEach(vp => { diff --git a/extensions/cornerstone/src/hooks/useMeasurementTracking.ts b/extensions/cornerstone/src/hooks/useMeasurementTracking.ts index 02558503f96..2712e039190 100644 --- a/extensions/cornerstone/src/hooks/useMeasurementTracking.ts +++ b/extensions/cornerstone/src/hooks/useMeasurementTracking.ts @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { useSystem } from '@ohif/core/src'; import { useViewportDisplaySets } from './useViewportDisplaySets'; -import { BaseVolumeViewport } from '@cornerstonejs/core'; +import { isVolumeViewportType } from '../utils/getLegacyViewportType'; /** * Hook that provides measurement tracking information for a viewport @@ -54,7 +54,7 @@ export function useMeasurementTracking({ viewportId }: { viewportId: string }) { const viewport = cornerstoneViewportService?.getCornerstoneViewport(viewportId); const SeriesInstanceUID = backgroundDisplaySet.SeriesInstanceUID; - if (viewport instanceof BaseVolumeViewport) { + if (isVolumeViewportType(viewport)) { const currentImageId = viewport?.getCurrentImageId(); if (!currentImageId) { diff --git a/extensions/cornerstone/src/hooks/useViewportRendering.tsx b/extensions/cornerstone/src/hooks/useViewportRendering.tsx index a7a12f35373..c134657f916 100644 --- a/extensions/cornerstone/src/hooks/useViewportRendering.tsx +++ b/extensions/cornerstone/src/hooks/useViewportRendering.tsx @@ -1,16 +1,13 @@ import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { useSystem } from '@ohif/core'; import { useViewportDisplaySets } from './useViewportDisplaySets'; -import { - StackViewport, - Types, - VolumeViewport3D, - utilities, - Enums, - BaseVolumeViewport, - cache, -} from '@cornerstonejs/core'; +import { Types, utilities, Enums, cache } from '@cornerstonejs/core'; import { getDataIdForViewport } from '../utils/getDataIdForViewport'; +import { + isStackViewportType, + isVolumeViewportType, + isVolume3DViewportType, +} from '../utils/getLegacyViewportType'; import { WindowLevelPreset } from '../types/WindowLevel'; import { ColorbarPositionType, ColorbarOptions, ColorbarProperties } from '../types/Colorbar'; import { VolumeRenderingConfig } from '../types/VolumeRenderingConfig'; @@ -130,7 +127,7 @@ export function useViewportRendering( const [viewport, setViewport] = useState(() => viewportId ? (cornerstoneViewportService.getCornerstoneViewport(viewportId) ?? null) : null ); - const [is3DVolume, setIs3DVolume] = useState(viewport instanceof VolumeViewport3D); + const [is3DVolume, setIs3DVolume] = useState(isVolume3DViewportType(viewport)); const [opacity, setOpacityState] = useState(); const [opacityLinear, setOpacityLinearState] = useState(); const [threshold, setThresholdState] = useState(); @@ -219,7 +216,7 @@ export function useViewportRendering( return; } - if (!(viewport instanceof BaseVolumeViewport)) { + if (!isVolumeViewportType(viewport)) { return; } @@ -264,7 +261,7 @@ export function useViewportRendering( }, [allWindowLevelPresets, activeDisplaySetInstanceUID]); useEffect(() => { - setIs3DVolume(viewport instanceof VolumeViewport3D); + setIs3DVolume(isVolume3DViewportType(viewport)); if (!viewport || !activeDisplaySetInstanceUID) { return; @@ -573,7 +570,7 @@ export function useViewportRendering( const setOpacity = useCallback( (opacityValue: number) => { - if (!viewport || !(viewport instanceof BaseVolumeViewport)) { + if (!viewport || !isVolumeViewportType(viewport)) { return; } @@ -624,7 +621,7 @@ export function useViewportRendering( const setThreshold = useCallback( (thresholdValue: number) => { - if (!viewport || !(viewport instanceof BaseVolumeViewport)) { + if (!viewport || !isVolumeViewportType(viewport)) { return; } @@ -665,7 +662,7 @@ export function useViewportRendering( return null; } - if (viewport instanceof StackViewport) { + if (isStackViewportType(viewport)) { const { colormap } = viewport.getProperties(); if (!colormap) { return ( diff --git a/extensions/cornerstone/src/init.tsx b/extensions/cornerstone/src/init.tsx index c12f4b8bd7f..cc870af9acc 100644 --- a/extensions/cornerstone/src/init.tsx +++ b/extensions/cornerstone/src/init.tsx @@ -74,6 +74,10 @@ export default async function init({ rendering: { ...cornerstone.getConfiguration().rendering, strictZSpacingForVolumeViewport: appConfig.strictZSpacingForVolumeViewport, + // Opt-in: route legacy viewport types through the new GenericViewport render + // paths while keeping the legacy public API via compatibility adapters. + // No-op on cornerstone builds that predate the GenericViewport architecture. + useGenericViewport: Boolean(appConfig.useGenericViewport), }, }); diff --git a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts index ca37bf4d2bb..a5cc6b8617c 100644 --- a/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts +++ b/extensions/cornerstone/src/services/SegmentationService/SegmentationService.ts @@ -10,6 +10,10 @@ import { metaData, } from '@cornerstonejs/core'; import { ViewportType } from '@cornerstonejs/core/enums'; +import { + isVolume3DViewportType, + isVolumeViewportType, +} from '../../utils/getLegacyViewportType'; import { Enums as csToolsEnums, @@ -312,7 +316,7 @@ class SegmentationService extends PubSubService { let isConverted = false; const defaultRepresentationType: csToolsEnums.SegmentationRepresentations = - csViewport.type === ViewportType.VOLUME_3D ? SURFACE : LABELMAP; + isVolume3DViewportType(csViewport) ? SURFACE : LABELMAP; let representationTypeToUse = type || defaultRepresentationType; if (representationTypeToUse === LABELMAP) { @@ -1591,8 +1595,7 @@ class SegmentationService extends PubSubService { } private determineViewportAndSegmentationType(csViewport, segmentation) { - const isVolumeViewport = - csViewport.type === ViewportType.ORTHOGRAPHIC || csViewport.type === ViewportType.VOLUME_3D; + const isVolumeViewport = isVolumeViewportType(csViewport); const isVolumeSegmentation = 'volumeId' in segmentation.representationData[LABELMAP]; return { isVolumeViewport, isVolumeSegmentation }; } @@ -1623,7 +1626,7 @@ class SegmentationService extends PubSubService { } private async handleVolumeViewportCase(csViewport, segmentation, isVolumeSegmentation) { - if (csViewport.type === ViewportType.VOLUME_3D) { + if (isVolume3DViewportType(csViewport)) { return { representationTypeToUse: SURFACE, isConverted: false, diff --git a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts index 1197922bce8..9b437ae6f1f 100644 --- a/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts +++ b/extensions/cornerstone/src/services/SyncGroupService/createHydrateSegmentationSynchronizer.ts @@ -7,6 +7,7 @@ import { } from '@cornerstonejs/tools'; import { isAnyDisplaySetCommon } from '../../utils/isAnyDisplaySetCommon'; +import { isVolume3DViewportType } from '../../utils/getLegacyViewportType'; const { createSynchronizer } = SynchronizerManager; const { SEGMENTATION_REPRESENTATION_MODIFIED } = Enums.Events; @@ -89,7 +90,7 @@ const segmentationRepresentationModifiedCallback = async ( // Ensure the segmentation representation aligns with the target viewport type. const type: Enums.SegmentationRepresentations = - viewport.type === CoreEnums.ViewportType.VOLUME_3D + isVolume3DViewportType(viewport) ? Enums.SegmentationRepresentations.Surface : ((segmentationRepresentationType as Enums.SegmentationRepresentations) ?? Enums.SegmentationRepresentations.Labelmap); diff --git a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts index b2ad4b7cd90..e833fdd8a0d 100644 --- a/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts +++ b/extensions/cornerstone/src/services/ViewportService/CornerstoneViewportService.ts @@ -3,16 +3,11 @@ import { PubSubService } from '@ohif/core'; import { Types as OhifTypes } from '@ohif/core'; import { RenderingEngine, - StackViewport, Types, getRenderingEngine, utilities as csUtils, - VolumeViewport, - VolumeViewport3D, - ECGViewport, cache, Enums as csEnums, - BaseVolumeViewport, } from '@cornerstonejs/core'; import { utilities as csToolsUtils, Enums as csToolsEnums } from '@cornerstonejs/tools'; @@ -39,6 +34,12 @@ import { usePositionPresentationStore } from '../../stores/usePositionPresentati import { useSynchronizersStore } from '../../stores/useSynchronizersStore'; import { useSegmentationPresentationStore } from '../../stores/useSegmentationPresentationStore'; import getClosestOrientationFromIOP from '../../utils/isReferenceViewable'; +import { + getLegacyViewportType, + isStackViewportType, + isVolume3DViewportType, + isVolumeViewportType, +} from '../../utils/getLegacyViewportType'; import { BlendModes } from '@cornerstonejs/core/enums'; const EVENTS = { @@ -49,10 +50,19 @@ const EVENTS = { const MIN_STACK_VIEWPORTS_TO_ENQUEUE_RESIZE = 12; const MIN_VOLUME_VIEWPORTS_TO_ENQUEUE_RESIZE = 6; +// Actor class names that represent a primary volume binding in a viewport. +// Legacy ORTHOGRAPHIC / VOLUME_3D render the source volume as a 'vtkVolume' +// actor. Under the GenericViewport-backed compat path (useGenericViewport), +// the MPR slice render path emits 'vtkImageSlice' actors instead, so the +// volume-set-vs-add optimization below must recognize both. (3D still uses +// 'vtkVolume'.) Without this, the optimization silently no-ops and a full +// setVolumes() teardown blanks MPR during SEG hydration. +const VOLUME_ACTOR_CLASS_NAMES = new Set(['vtkVolume', 'vtkImageSlice']); + function getVolumeActorReferencedIds(viewport: Types.IVolumeViewport): string[] { const actors = viewport.getActors?.() ?? []; return actors - .filter(ac => ac.actor?.getClassName?.() === 'vtkVolume') + .filter(ac => VOLUME_ACTOR_CLASS_NAMES.has(ac.actor?.getClassName?.() ?? '')) .map(ac => ac.referencedId) .filter(Boolean) as string[]; } @@ -81,7 +91,7 @@ function viewportMatchesDesiredVolumePresentation( desiredViewportInfo: ViewportInfo ): boolean { const desiredType = desiredViewportInfo.getViewportType(); - if (viewport.type !== desiredType) { + if (getLegacyViewportType(viewport) !== desiredType) { return false; } return ( @@ -370,7 +380,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return { viewportType: viewportInfo.getViewportType(), - viewReference: csViewport instanceof VolumeViewport3D ? null : csViewport.getViewReference(), + viewReference: isVolume3DViewportType(csViewport) ? null : csViewport.getViewReference(), viewPresentation: csViewport.getViewPresentation({ pan: true, zoom: true }), viewportId, }; @@ -398,10 +408,9 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi return properties; }; - const properties = - csViewport instanceof BaseVolumeViewport - ? new Map() - : cleanProperties(csViewport.getProperties()); + const properties = isVolumeViewportType(csViewport) + ? new Map() + : cleanProperties(csViewport.getProperties()); if (properties instanceof Map) { const volumeIds = (csViewport as Types.IBaseVolumeViewport).getAllVolumeIds(); @@ -840,10 +849,14 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi _presentations: Presentations = {} ): Promise { const [displaySet] = viewportData.data; - return viewport.setDataIds(displaySet.imageIds, { - groupId: displaySet.displaySetInstanceUID, - viewReference: viewportInfo.getViewReference(), - }); + // CS3D's "redo viewports" replaced setDataIds with the generic + // setDisplaySets({ displaySetId }) API; the legacy adapters key off + // imageIds[0] as the displaySetId, so do the same here. + await viewport.setDisplaySets({ displaySetId: displaySet.imageIds[0] }); + const viewReference = viewportInfo.getViewReference(); + if (viewReference) { + viewport.setViewReference(viewReference); + } } private async _setStackViewport( @@ -1141,7 +1154,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi throw new Error('Background display set not found'); } - if (viewport.type === csEnums.ViewportType.VOLUME_3D) { + if (isVolume3DViewportType(viewport)) { timeoutViewportCallback(() => { viewportGridService.setDisplaySetsForViewport({ viewportId: viewport.id, @@ -1311,7 +1324,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi let displaySetPromise; - if (viewport instanceof VolumeViewport || viewport instanceof VolumeViewport3D) { + if (isVolumeViewportType(viewport)) { displaySetPromise = this._setVolumeViewport(viewport, viewportData, viewportInfo).then(() => { if (keepCamera) { viewport.setCamera(viewportCamera); @@ -1320,7 +1333,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi }); } - if (viewport instanceof StackViewport) { + if (isStackViewportType(viewport)) { displaySetPromise = this._setStackViewport(viewport, viewportData, viewportInfo); } @@ -1338,7 +1351,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi viewportInfo: ViewportInfo, presentations: Presentations = {} ): Promise { - if (viewport instanceof StackViewport) { + if (isStackViewportType(viewport)) { return this._setStackViewport( viewport, viewportData as StackViewportData, @@ -1347,7 +1360,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi ); } - if ([VolumeViewport, VolumeViewport3D].some(type => viewport instanceof type)) { + if (isVolumeViewportType(viewport)) { return this._setVolumeViewport( viewport as Types.IVolumeViewport, viewportData as VolumeViewportData, @@ -1356,7 +1369,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi ); } - if (viewport instanceof ECGViewport) { + if (getLegacyViewportType(viewport) === csEnums.ViewportType.ECG) { return this._setEcgViewport( viewport as unknown as Types.IECGViewport, viewportData as StackViewportData @@ -1508,7 +1521,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi } const { properties } = lutPresentation; - if (viewport instanceof BaseVolumeViewport) { + if (isVolumeViewportType(viewport)) { if (properties instanceof Map) { properties.forEach((propertiesEntry, volumeId) => { viewport.setProperties(propertiesEntry, volumeId); @@ -1557,7 +1570,7 @@ class CornerstoneViewportService extends PubSubService implements IViewportServi const { segmentationId, type, hydrated } = presentationItem; const { Labelmap, Surface } = csToolsEnums.SegmentationRepresentations; - const isVolume3D = viewport.type === csEnums.ViewportType.VOLUME_3D; + const isVolume3D = isVolume3DViewportType(viewport); // Determine the appropriate segmentation representation for the viewport. // If the current type is Surface but the viewport is not 3D, fallback to Labelmap. diff --git a/extensions/cornerstone/src/services/ViewportService/Viewport.ts b/extensions/cornerstone/src/services/ViewportService/Viewport.ts index ae557934c18..dadae5878ce 100644 --- a/extensions/cornerstone/src/services/ViewportService/Viewport.ts +++ b/extensions/cornerstone/src/services/ViewportService/Viewport.ts @@ -2,10 +2,10 @@ import { Types, Enums, getEnabledElementByViewportId, - VolumeViewport, utilities, } from '@cornerstonejs/core'; import { StackViewportData, VolumeViewportData } from '../../types/CornerstoneCacheService'; +import { isOrthographicViewportType } from '../../utils/getLegacyViewportType'; import getCornerstoneBlendMode from '../../utils/getCornerstoneBlendMode'; import getCornerstoneOrientation from '../../utils/getCornerstoneOrientation'; import getCornerstoneViewportType from '../../utils/getCornerstoneViewportType'; @@ -105,7 +105,7 @@ const dataContains = ({ data, displaySetUID, imageId, viewport }): boolean => { return !!data.imageIds.find(dataId => dataId === imageId); } - if (imageId && (data.volumeId || viewport instanceof VolumeViewport)) { + if (imageId && (data.volumeId || isOrthographicViewportType(viewport))) { const isAcquisition = !!viewport.getCurrentImageId(); if (!isAcquisition) { diff --git a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx index f5d036e6f5c..84958713c18 100644 --- a/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx +++ b/extensions/cornerstone/src/tools/ImageOverlayViewerTool.tsx @@ -1,8 +1,9 @@ -import { VolumeViewport, metaData, utilities } from '@cornerstonejs/core'; +import { metaData, utilities } from '@cornerstonejs/core'; import { IStackViewport, IVolumeViewport } from '@cornerstonejs/core/types'; import { AnnotationDisplayTool, drawing } from '@cornerstonejs/tools'; import { guid, b64toBlob } from '@ohif/core/src/utils'; import OverlayPlaneModuleProvider from './OverlayPlaneModuleProvider'; +import { isOrthographicViewportType } from '../utils/getLegacyViewportType'; interface CachedStat { color: number[]; // [r, g, b, a] @@ -49,7 +50,7 @@ class ImageOverlayViewerTool extends AnnotationDisplayTool { onSetToolDisabled = (): void => {}; protected getReferencedImageId(viewport: IStackViewport | IVolumeViewport): string { - if (viewport instanceof VolumeViewport) { + if (isOrthographicViewportType(viewport)) { return; } diff --git a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx index edf2590cd79..61627f12607 100644 --- a/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx +++ b/extensions/cornerstone/src/utils/CornerstoneViewportDownloadForm.tsx @@ -1,9 +1,10 @@ import { utils } from '@ohif/core'; import React, { useEffect, useState } from 'react'; import html2canvas from 'html2canvas'; -import { getEnabledElement, StackViewport, BaseVolumeViewport } from '@cornerstonejs/core'; +import { getEnabledElement } from '@cornerstonejs/core'; import { ToolGroupManager, segmentation, Enums } from '@cornerstonejs/tools'; import { getEnabledElement as OHIFgetEnabledElement } from '../state'; +import { isStackViewportType, isVolumeViewportType } from './getLegacyViewportType'; import { useSystem } from '@ohif/core/src'; const { downloadUrl } = utils; @@ -127,10 +128,10 @@ const CornerstoneViewportDownloadForm = ({ const viewPresentation = viewport.getViewPresentation?.(); const viewRef = viewport.getViewReference?.(); - if (downloadViewport instanceof StackViewport) { + if (isStackViewportType(downloadViewport)) { const imageId = viewport.getCurrentImageId(); await downloadViewport.setStack([imageId]); - } else if (downloadViewport instanceof BaseVolumeViewport) { + } else if (isVolumeViewportType(downloadViewport)) { const volumeIds = viewport.getAllVolumeIds(); await downloadViewport.setVolumes([{ volumeId: volumeIds[0] }]); } diff --git a/extensions/cornerstone/src/utils/createSegmentationForViewport.ts b/extensions/cornerstone/src/utils/createSegmentationForViewport.ts index 497050623a8..24e40e867ab 100644 --- a/extensions/cornerstone/src/utils/createSegmentationForViewport.ts +++ b/extensions/cornerstone/src/utils/createSegmentationForViewport.ts @@ -36,7 +36,7 @@ type CreateSegmentationForViewportParams = { export async function createSegmentationForViewport( servicesManager: ServicesManager, { viewportId, options = {}, segmentationType }: CreateSegmentationForViewportParams -): Promise { +): Promise { const { viewportGridService, displaySetService, segmentationService } = servicesManager.services; const { viewports } = viewportGridService.getState(); const targetViewportId = viewportId; @@ -44,7 +44,15 @@ export async function createSegmentationForViewport( const viewport = viewports.get(targetViewportId); // Todo: add support for multiple display sets - const displaySetInstanceUID = options.displaySetInstanceUID || viewport.displaySetInstanceUIDs[0]; + // Guard against the grid not having this viewport yet (a transient race when + // "Add segmentation" runs before the viewport is registered in the grid). Bail + // gracefully instead of throwing, which would surface an uncaught error overlay. + const displaySetInstanceUID = + options.displaySetInstanceUID || viewport?.displaySetInstanceUIDs?.[0]; + + if (!displaySetInstanceUID) { + return undefined; + } const segs = segmentationService.getSegmentations(); diff --git a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts index 170d262653a..2d1607e80c4 100644 --- a/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts +++ b/extensions/cornerstone/src/utils/generateSegmentationCSVReport.test.ts @@ -30,10 +30,14 @@ const mockDocument = { }, }; -Object.defineProperty(global, 'document', { - writable: true, - value: mockDocument, -}); +// jsdom (jest 30) makes the global `document` non-configurable, so it can't be +// replaced wholesale. Route the methods the code under test uses onto the real +// jsdom document; assertions still target mockDocument's jest.fn() references. +const doc = document as unknown as typeof mockDocument; +doc.addEventListener = mockDocument.addEventListener; +doc.createElement = mockDocument.createElement; +document.body.appendChild = mockDocument.body.appendChild as typeof document.body.appendChild; +document.body.removeChild = mockDocument.body.removeChild as typeof document.body.removeChild; describe('generateSegmentationCSVReport', () => { const mockInfo = { diff --git a/extensions/cornerstone/src/utils/getLegacyViewportType.ts b/extensions/cornerstone/src/utils/getLegacyViewportType.ts new file mode 100644 index 00000000000..c36c320496e --- /dev/null +++ b/extensions/cornerstone/src/utils/getLegacyViewportType.ts @@ -0,0 +1,66 @@ +import { Enums as csEnums, Types as csTypes } from '@cornerstonejs/core'; + +const { ViewportType } = csEnums; + +type ViewportLike = { + requestedType?: csEnums.ViewportType; + type?: csEnums.ViewportType; +}; + +/** + * Returns the legacy/requested viewport type, transparent across GenericViewport + * compatibility remapping. + * + * When `rendering.useGenericViewport` is enabled, a viewport requested as + * `STACK` or `ORTHOGRAPHIC` has runtime `viewport.type === PLANAR_NEXT`, and a + * `VOLUME_3D` request has `VOLUME_3D_NEXT`. The rendering engine records the + * original request on `viewport.requestedType`, which this reads. Falls back to + * `viewport.type` for legacy/non-remapped viewports (and older cornerstone builds + * that do not populate `requestedType`). + * + * Use this instead of `viewport instanceof StackViewport/VolumeViewport/...` or + * raw `viewport.type === ViewportType.X` when branching on the legacy viewport + * type. For "does this viewport support operation X" questions, prefer the + * cornerstone capability guards (`utilities.viewportSupports*`) instead. + */ +export function getLegacyViewportType( + viewport: unknown +): csEnums.ViewportType | undefined { + const vp = viewport as ViewportLike | null | undefined; + return vp?.requestedType ?? vp?.type; +} + +/** Legacy STACK viewport (image stack). Replaces `instanceof StackViewport`. */ +export function isStackViewportType( + viewport: unknown +): viewport is csTypes.IStackViewport { + return getLegacyViewportType(viewport) === ViewportType.STACK; +} + +/** Legacy ORTHOGRAPHIC (MPR) viewport. Replaces `instanceof VolumeViewport`. */ +export function isOrthographicViewportType( + viewport: unknown +): viewport is csTypes.IVolumeViewport { + return getLegacyViewportType(viewport) === ViewportType.ORTHOGRAPHIC; +} + +/** Legacy VOLUME_3D viewport. Replaces `instanceof VolumeViewport3D`. */ +export function isVolume3DViewportType( + viewport: unknown +): viewport is csTypes.IVolumeViewport { + return getLegacyViewportType(viewport) === ViewportType.VOLUME_3D; +} + +/** + * Legacy ORTHOGRAPHIC or VOLUME_3D viewport (i.e. a `BaseVolumeViewport`). + * Replaces `instanceof BaseVolumeViewport`. + */ +export function isVolumeViewportType( + viewport: unknown +): viewport is csTypes.IVolumeViewport { + const legacyType = getLegacyViewportType(viewport); + return ( + legacyType === ViewportType.ORTHOGRAPHIC || + legacyType === ViewportType.VOLUME_3D + ); +} diff --git a/extensions/tmtv/src/commandsModule.ts b/extensions/tmtv/src/commandsModule.ts index 3ddbab2aca2..bce7a706f29 100644 --- a/extensions/tmtv/src/commandsModule.ts +++ b/extensions/tmtv/src/commandsModule.ts @@ -362,9 +362,7 @@ const commandsModule = ({ servicesManager, commandsManager, extensionManager }: } const referencedVolume = - csTools.utilities.segmentation.getReferenceVolumeForSegmentationVolume( - labelmapVolume.volumeId - ); + csTools.utilities.segmentation.getReferenceVolumeForSegmentation(segmentationId); if (!referencedVolume) { report[id] = segReport; diff --git a/package.json b/package.json index d2e98bc74e4..c3e4faf6d73 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "test:e2e:debug": "cross-env TEST_ENV=true npx playwright test --debug", "test:e2e:dist": "lerna run test:e2e:dist --stream", "test:e2e:serve": "yarn test:data && lerna run test:e2e:serve --stream", + "review:screenshots": "node scripts/screenshot-reviewer.mjs", "see-changed": "lerna changed", "docs:preview": "lerna run docs:preview --stream", "docs:publish": "chmod +x ./build-and-publish-docs.sh && ./build-and-publish-docs.sh", diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts index 48248c05ee3..dca0919ff36 100644 --- a/platform/core/src/types/AppTypes.ts +++ b/platform/core/src/types/AppTypes.ts @@ -95,6 +95,14 @@ declare global { useNorm16Texture?: boolean; useCPURendering?: boolean; strictZSpacingForVolumeViewport?: boolean; + /** + * Routes legacy cornerstone viewport types (STACK / ORTHOGRAPHIC / VOLUME_3D / etc.) + * through the new GenericViewport-backed render paths while preserving the legacy + * public API via compatibility adapters. Maps to cornerstone + * config.rendering.useGenericViewport. Defaults to false (legacy classes). + * Requires @cornerstonejs/core >= 5.0.0-beta (GenericViewport architecture). + */ + useGenericViewport?: boolean; useCursors?: boolean; maxCacheSize?: number; max3DTextureSize?: number; diff --git a/platform/core/src/utils/Queue.test.js b/platform/core/src/utils/Queue.test.js index 9ea302aab8e..079321e1f67 100644 --- a/platform/core/src/utils/Queue.test.js +++ b/platform/core/src/utils/Queue.test.js @@ -30,7 +30,7 @@ describe('Queue', () => { // }); // const end = await timer(threshold); // expect(end - start >= 2 * threshold).toBe(true); - // expect(mockedTimeout).toBeCalledTimes(2); + // expect(mockedTimeout).toHaveBeenCalledTimes(2); // }); it('should prevent task execution when queue limit is reached', async () => { const queue = new Queue(1); @@ -46,7 +46,7 @@ describe('Queue', () => { } const elapsed = await promise; expect(elapsed >= threshold && elapsed < 2 * threshold).toBe(true); - expect(mockedTimeout).toBeCalledTimes(1); + expect(mockedTimeout).toHaveBeenCalledTimes(1); }); it('should safely bind tasks to the queue', async () => { const queue = new Queue(1); @@ -57,13 +57,13 @@ describe('Queue', () => { const promise = timer(threshold).then(time => time - start); await timer(threshold); expect(Date.now() - start < threshold).toBe(true); - expect(mockedErrorHandler).toBeCalledTimes(1); - expect(mockedErrorHandler).nthCalledWith( + expect(mockedErrorHandler).toHaveBeenCalledTimes(1); + expect(mockedErrorHandler).toHaveBeenNthCalledWith( 1, expect.objectContaining({ message: 'Queue limit reached' }) ); const elapsed = await promise; expect(elapsed >= threshold && elapsed < 2 * threshold).toBe(true); - expect(mockedTimeout).toBeCalledTimes(1); + expect(mockedTimeout).toHaveBeenCalledTimes(1); }); }); diff --git a/platform/core/src/utils/absoluteUrl.test.js b/platform/core/src/utils/absoluteUrl.test.js index 41cc64a1d30..57985feba5d 100644 --- a/platform/core/src/utils/absoluteUrl.test.js +++ b/platform/core/src/utils/absoluteUrl.test.js @@ -1,17 +1,24 @@ +/** + * @jest-environment node + * + * Runs in node (not jsdom): jsdom (jest 30) exposes `window.location` as an + * unforgeable, non-configurable property that can't be replaced or stubbed. + * absoluteUrl only reads `window.location.origin` and this suite imports nothing + * that needs the DOM, so we run without jsdom and provide a plain `window`. + */ import absoluteUrl from './absoluteUrl'; describe('absoluteUrl', () => { + const setOrigin = url => { + global.window = { location: { origin: url } }; + }; + + afterEach(() => { + delete global.window; + }); + test('should return /path_1/path_2/path_3/path_to_destination when the window.location.origin is http://dummy.com/path_1/path_2 and the path is /path_3/path_to_destination', () => { - let global = { - window: Object.create(window), - }; - const url = 'http://dummy.com/path_1/path_2'; - Object.defineProperty(window, 'location', { - value: { - origin: url, - }, - writable: true, - }); + setOrigin('http://dummy.com/path_1/path_2'); const absoluteUrlOutput = absoluteUrl('/path_3/path_to_destination'); expect(absoluteUrlOutput).toEqual('/path_1/path_2/path_3/path_to_destination'); }); @@ -22,22 +29,13 @@ describe('absoluteUrl', () => { }); test('should return the original path when there path in the window.origin after the domain and port', () => { - delete global.window.location; - const url = 'http://dummy.com'; - global.window.location = { - origin: url, - }; + setOrigin('http://dummy.com'); const absoluteUrlOutput = absoluteUrl('path_1/path_2/path_3'); expect(absoluteUrlOutput).toEqual('/path_1/path_2/path_3'); }); test('should be able to return the absolute path even when the path contains duplicates', () => { - global.window ||= Object.create(window); - const url = 'http://dummy.com'; - delete global.window.location; - global.window.location = { - origin: url, - }; + setOrigin('http://dummy.com'); const absoluteUrlOutput = absoluteUrl('path_1/path_1/path_1'); expect(absoluteUrlOutput).toEqual('/path_1/path_1/path_1'); }); diff --git a/platform/core/src/utils/addServer.test.js b/platform/core/src/utils/addServer.test.js index dc6a7159380..ee988575daa 100644 --- a/platform/core/src/utils/addServer.test.js +++ b/platform/core/src/utils/addServer.test.js @@ -31,7 +31,7 @@ describe('addServers', () => { test('should be able to add a server and dispatch to the store successfuly', () => { addServers(servers, store); - expect(store.dispatch).toBeCalledWith({ + expect(store.dispatch).toHaveBeenCalledWith({ server: { authority: 'http://127.0.0.1/auth/realms/ohif', client_id: 'ohif-viewer', @@ -43,7 +43,7 @@ describe('addServers', () => { }, type: 'ADD_SERVER', }); - expect(store.dispatch).toBeCalledWith({ + expect(store.dispatch).toHaveBeenCalledWith({ server: { imageRendering: 'wadors', name: 'DCM4CHEE', @@ -59,19 +59,19 @@ describe('addServers', () => { }); test('should throw an error if servers list is not defined', () => { - expect(() => addServers(undefined, store)).toThrowError( + expect(() => addServers(undefined, store)).toThrow( new Error('The servers and store must be defined') ); }); test('should throw an error if store is not defined', () => { - expect(() => addServers(servers, undefined)).toThrowError( + expect(() => addServers(servers, undefined)).toThrow( new Error('The servers and store must be defined') ); }); test('should throw an error when both server and store are not defined', () => { - expect(() => addServers(undefined, undefined)).toThrowError( + expect(() => addServers(undefined, undefined)).toThrow( new Error('The servers and store must be defined') ); }); diff --git a/platform/core/src/utils/hierarchicalListUtils.test.js b/platform/core/src/utils/hierarchicalListUtils.test.js index fbf41e9fa87..1b5f8344535 100644 --- a/platform/core/src/utils/hierarchicalListUtils.test.js +++ b/platform/core/src/utils/hierarchicalListUtils.test.js @@ -69,12 +69,12 @@ describe('hierarchicalListUtils', function () { const fn = jest.fn(); forEach(sharedList, fn); expect(fn).toHaveBeenCalledTimes(6); - expect(fn).nthCalledWith(1, '1.2.3.1', '1.2.3.1.1'); - expect(fn).nthCalledWith(2, '1.2.3.1', '1.2.3.1.2'); - expect(fn).nthCalledWith(3, '1.2.3.2'); - expect(fn).nthCalledWith(4, '1.2.3.3', '1.2.3.3.1'); - expect(fn).nthCalledWith(5, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1'); - expect(fn).nthCalledWith(6, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2'); + expect(fn).toHaveBeenNthCalledWith(1, '1.2.3.1', '1.2.3.1.1'); + expect(fn).toHaveBeenNthCalledWith(2, '1.2.3.1', '1.2.3.1.2'); + expect(fn).toHaveBeenNthCalledWith(3, '1.2.3.2'); + expect(fn).toHaveBeenNthCalledWith(4, '1.2.3.3', '1.2.3.3.1'); + expect(fn).toHaveBeenNthCalledWith(5, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.1'); + expect(fn).toHaveBeenNthCalledWith(6, '1.2.3.3', '1.2.3.3.2', '1.2.3.3.2.2'); }); }); diff --git a/platform/core/src/utils/progressTrackingUtils.test.js b/platform/core/src/utils/progressTrackingUtils.test.js index 3a74c8c95f8..333cd34e0f0 100644 --- a/platform/core/src/utils/progressTrackingUtils.test.js +++ b/platform/core/src/utils/progressTrackingUtils.test.js @@ -43,7 +43,7 @@ describe('progressTrackingUtils', () => { const promises = [Promise.resolve('A'), Promise.resolve('B'), Promise.resolve('C')]; promises.forEach(promise => void utils.waitOn(list, promise)); return Promise.all(promises).then(() => { - expect(observer).toBeCalledTimes(6); + expect(observer).toHaveBeenCalledTimes(6); [ { failures: 0, @@ -83,7 +83,7 @@ describe('progressTrackingUtils', () => { }, ].forEach((item, i) => { const result = expect.objectContaining(item); - expect(observer).nthCalledWith(i + 1, result, list); + expect(observer).toHaveBeenNthCalledWith(i + 1, result, list); }); expect(utils.getOverallProgress(list)).toStrictEqual({ failures: 0, @@ -102,11 +102,11 @@ describe('progressTrackingUtils', () => { download.deferred.resolve(fakeRequest(update)); utils.waitOn(list, processing); return processing.then(() => { - expect(update).toBeCalledTimes(4); + expect(update).toHaveBeenCalledTimes(4); [0.25, 0.5, 0.75, 1.0].forEach( - (value, i) => void expect(update).nthCalledWith(i + 1, value) + (value, i) => void expect(update).toHaveBeenNthCalledWith(i + 1, value) ); - expect(observer).toBeCalledTimes(7); + expect(observer).toHaveBeenCalledTimes(7); [ { failures: 0, @@ -152,7 +152,7 @@ describe('progressTrackingUtils', () => { }, ].forEach((item, i) => { const result = expect.objectContaining(item); - expect(observer).nthCalledWith(i + 1, result, list); + expect(observer).toHaveBeenNthCalledWith(i + 1, result, list); }); }); }); diff --git a/rsbuild.config.ts b/rsbuild.config.ts index 59af6c7ce64..ff3aafff836 100644 --- a/rsbuild.config.ts +++ b/rsbuild.config.ts @@ -29,6 +29,7 @@ const OHIF_OPEN = process.env.OHIF_OPEN !== 'false'; // Ignore node_modules except @cornerstonejs (symlinked local development). const WATCH_IGNORED = /node_modules[\\/](?!@cornerstonejs(?:[\\/]|$))/; +const WATCH_AGGREGATE_TIMEOUT = Number(process.env.WATCH_AGGREGATE_TIMEOUT || 1500); export default defineConfig({ dev: { @@ -91,6 +92,7 @@ export default defineConfig({ watchOptions: { ignored: WATCH_IGNORED, followSymlinks: true, + aggregateTimeout: WATCH_AGGREGATE_TIMEOUT, }, }, }, diff --git a/scripts/screenshot-reviewer.html b/scripts/screenshot-reviewer.html new file mode 100644 index 00000000000..c1c17169acc --- /dev/null +++ b/scripts/screenshot-reviewer.html @@ -0,0 +1,700 @@ + + + + + + Screenshot Review + + + + +
+ + +
+
+
+
+ + + +
+ +
+ + + +
+
+
+
+ +
+ + + + + +
+ +
+
No changed screenshots found.
+
+ +
+
+ Notes + +
+ +
+
+
+ + + + diff --git a/scripts/screenshot-reviewer.mjs b/scripts/screenshot-reviewer.mjs new file mode 100644 index 00000000000..2462a404fa9 --- /dev/null +++ b/scripts/screenshot-reviewer.mjs @@ -0,0 +1,218 @@ +#!/usr/bin/env node + +import { createServer } from 'node:http'; +import { readFile } from 'node:fs/promises'; +import { createReadStream } from 'node:fs'; +import { extname, join, normalize, relative, resolve } from 'node:path'; +import { spawn } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = resolve(fileURLToPath(new URL('..', import.meta.url))); +const htmlPath = resolve( + fileURLToPath(new URL('./screenshot-reviewer.html', import.meta.url)) +); +const screenshotRoot = 'tests/screenshots'; +const port = Number(process.env.SCREENSHOT_REVIEW_PORT || 4777); + +function runGit(args, options = {}) { + return new Promise((resolvePromise, reject) => { + const child = spawn('git', args, { + cwd: repoRoot, + stdio: ['ignore', 'pipe', 'pipe'], + ...options, + }); + + const stdout = []; + const stderr = []; + + child.stdout.on('data', (chunk) => stdout.push(chunk)); + child.stderr.on('data', (chunk) => stderr.push(chunk)); + child.on('error', reject); + child.on('close', (code) => { + const out = Buffer.concat(stdout); + const err = Buffer.concat(stderr).toString('utf8').trim(); + + if (code !== 0) { + const error = new Error(err || `git exited with status ${code}`); + error.stdout = out; + error.stderr = err; + reject(error); + return; + } + + resolvePromise(out); + }); + }); +} + +function sendJson(response, statusCode, payload) { + response.writeHead(statusCode, { + 'content-type': 'application/json; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(JSON.stringify(payload)); +} + +function sendText(response, statusCode, text) { + response.writeHead(statusCode, { + 'content-type': 'text/plain; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(text); +} + +function parseStatusPorcelain(buffer) { + const entries = []; + const parts = buffer.toString('utf8').split('\0').filter(Boolean); + + for (let index = 0; index < parts.length; index++) { + const record = parts[index]; + const x = record[0]; + const y = record[1]; + const rawPath = record.slice(3); + let filePath = rawPath; + + if (x === 'R' || x === 'C') { + index += 1; + filePath = parts[index] || rawPath; + } + + if (!filePath.endsWith('.png') || !filePath.startsWith(`${screenshotRoot}/`)) { + continue; + } + + const segments = filePath.split('/'); + + entries.push({ + path: filePath, + name: segments.at(-1), + spec: segments.slice(0, -1).join('/'), + x, + y, + staged: x !== ' ' && x !== '?', + partiallyStaged: x !== ' ' && y !== ' ', + worktreeChanged: y !== ' ', + }); + } + + entries.sort((a, b) => a.path.localeCompare(b.path)); + return entries; +} + +function resolveScreenshotPath(rawPath) { + if (!rawPath || rawPath.includes('\0')) { + throw new Error('Invalid path'); + } + + const normalizedPath = normalize(rawPath).replaceAll('\\', '/'); + const absolutePath = resolve(repoRoot, normalizedPath); + const relativePath = relative(repoRoot, absolutePath).replaceAll('\\', '/'); + + if ( + relativePath.startsWith('..') || + relativePath === '' || + !relativePath.startsWith(`${screenshotRoot}/`) || + extname(relativePath).toLowerCase() !== '.png' + ) { + throw new Error('Path must be a PNG under tests/screenshots'); + } + + return { + absolutePath, + relativePath, + }; +} + +async function readRequestBody(request) { + const chunks = []; + for await (const chunk of request) { + chunks.push(chunk); + if (Buffer.concat(chunks).length > 1024 * 32) { + throw new Error('Request body too large'); + } + } + + return JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); +} + +async function handleImage(requestUrl, response) { + const path = requestUrl.searchParams.get('path'); + const version = requestUrl.searchParams.get('version'); + const { absolutePath, relativePath } = resolveScreenshotPath(path); + + response.writeHead(200, { + 'content-type': 'image/png', + 'cache-control': 'no-store', + }); + + if (version === 'head') { + const buffer = await runGit(['show', `HEAD:${relativePath}`]); + response.end(buffer); + return; + } + + if (version !== 'worktree') { + throw new Error('Invalid image version'); + } + + createReadStream(absolutePath).pipe(response); +} + +async function handleRequest(request, response) { + const requestUrl = new URL(request.url, `http://${request.headers.host}`); + + try { + if (request.method === 'GET' && requestUrl.pathname === '/') { + const html = await readFile(htmlPath, 'utf8'); + response.writeHead(200, { + 'content-type': 'text/html; charset=utf-8', + 'cache-control': 'no-store', + }); + response.end(html); + return; + } + + if (request.method === 'GET' && requestUrl.pathname === '/api/list') { + const status = await runGit([ + 'status', + '--porcelain=v1', + '-z', + '--', + screenshotRoot, + ]); + sendJson(response, 200, { entries: parseStatusPorcelain(status) }); + return; + } + + if (request.method === 'GET' && requestUrl.pathname === '/api/image') { + await handleImage(requestUrl, response); + return; + } + + if (request.method === 'POST' && requestUrl.pathname === '/api/stage') { + const body = await readRequestBody(request); + const { relativePath } = resolveScreenshotPath(body.path); + await runGit(['add', '--', relativePath]); + sendJson(response, 200, { ok: true, path: relativePath }); + return; + } + + sendText(response, 404, 'Not found'); + } catch (error) { + if (!response.headersSent) { + sendJson(response, 500, { error: error.message }); + return; + } + response.destroy(error); + } +} + +const server = createServer((request, response) => { + handleRequest(request, response); +}); + +server.listen(port, () => { + const url = `http://localhost:${port}`; + console.log(`Screenshot reviewer: ${url}`); + console.log(`Repository: ${repoRoot}`); +}); diff --git a/testdata b/testdata index bd743ed76b4..3f85fe843c9 160000 --- a/testdata +++ b/testdata @@ -1 +1 @@ -Subproject commit bd743ed76b403e6d476e1af0bbfe7479d96b563d +Subproject commit 3f85fe843c9a1ffaccb826845f317dace9c06c45 diff --git a/tests/JumpToMeasurementMPR.spec.ts b/tests/JumpToMeasurementMPR.spec.ts index 0ba0477ed21..c1b373772cb 100644 --- a/tests/JumpToMeasurementMPR.spec.ts +++ b/tests/JumpToMeasurementMPR.spec.ts @@ -1,4 +1,11 @@ -import { checkForScreenshot, screenShotPaths, test, visitStudy } from './utils'; +import { + checkForScreenshot, + screenShotPaths, + test, + visitStudy, + waitForPaintToSettle, + waitForViewportRenderCycle, +} from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; @@ -111,9 +118,17 @@ test('should hydrate in MPR correctly', async ({ screenShotPaths.jumpToMeasurementMPR.jumpInMPR ); + const seriesChangeRenderCycle = waitForViewportRenderCycle(page, { renderedTimeout: 30000 }); + await leftPanelPageObject.loadSeriesByDescription('Lung 3.0 CE'); - await page.waitForTimeout(5000); + await seriesChangeRenderCycle; + // Series change unloads the old volume and progressively streams the new + // one; the wait helper resolves when loadStatus.loaded flips true, but the + // MPR mappers can still be sampling stale low-res frames for one tick. + // Give the streaming tail a chance to upload before screenshotting. + await page.waitForTimeout(2000); + await waitForPaintToSettle(page); await checkForScreenshot( page, diff --git a/tests/Livewire.spec.ts b/tests/Livewire.spec.ts index 378a0e74f99..177b9cb4dc0 100644 --- a/tests/Livewire.spec.ts +++ b/tests/Livewire.spec.ts @@ -43,11 +43,11 @@ test('should display the livewire tool', async ({ const stats = livewires[0].firstTargetStats!; expect(stats.areaUnit).toBe('mm²'); - expect(Math.round(stats.area as number)).toBe(28906); + expect(Math.round(stats.area as number)).toBe(30412); const lines = activeViewport.getSvgAnnotationStatTextLines(livewires[0].annotationUID); await expect(lines).toHaveCount(1); - await expect(lines.nth(0)).toHaveText('Area: 28906 mm²'); + await expect(lines.nth(0)).toHaveText('Area: 30412 mm²'); }); test('should restore viewport interactivity after deleting an in-progress Livewire annotation via context menu', async ({ diff --git a/tests/SEGHydrationFrom3DFourUp.spec.ts b/tests/SEGHydrationFrom3DFourUp.spec.ts index 7c8d5e1d2fa..3456e09ccc0 100644 --- a/tests/SEGHydrationFrom3DFourUp.spec.ts +++ b/tests/SEGHydrationFrom3DFourUp.spec.ts @@ -5,6 +5,7 @@ import { screenShotPaths, test, visitStudy, + waitForPaintToSettle, waitForViewportsRendered, waitForViewportRenderCycle, expect, @@ -40,6 +41,12 @@ test.describe('3D four up SEG hydration', async () => { await leftPanelPageObject.loadSeriesByDescription('SEG'); await viewportRenderCycle; + await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible({ + timeout: 60000, + }); + await waitForViewportsRendered(page, { timeout: 60000 }); + await page.waitForTimeout(3000); + await waitForPaintToSettle(page); await checkForScreenshot( page, @@ -57,6 +64,12 @@ test.describe('3D four up SEG hydration', async () => { // Wait until all viewports have finished rendering await viewportRenderCycle; + // 3D volume rendering keeps streaming refined geometry after the load + // status flips; give the GPU a window to present the final frame before + // screenshotting. + await waitForViewportsRendered(page, { timeout: 180000 }); + await page.waitForTimeout(3000); + await waitForPaintToSettle(page); await checkForScreenshot({ page, diff --git a/tests/SEGHydrationFromMPR.spec.ts b/tests/SEGHydrationFromMPR.spec.ts index 7b40bfa4c6e..1d6e391b824 100644 --- a/tests/SEGHydrationFromMPR.spec.ts +++ b/tests/SEGHydrationFromMPR.spec.ts @@ -3,6 +3,7 @@ import { screenShotPaths, test, visitStudy, + waitForPaintToSettle, waitForViewportRenderCycle, waitForViewportsRendered, } from './utils'; @@ -36,6 +37,10 @@ test('should properly display MPR for MR', async ({ await leftPanelPageObject.loadSeriesByDescription('SEG'); await waitForViewportsRendered(page); + // SEG load triggers an additional progressive labelmap upload after the + // viewports first report 'rendered'; let that finish before screenshotting. + await page.waitForTimeout(1500); + await waitForPaintToSettle(page); await checkForScreenshot( page, @@ -49,6 +54,11 @@ test('should properly display MPR for MR', async ({ await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); await viewportRenderCycle; + // Hydration propagates the labelmap volume to the sagittal/coronal MPR + // viewports asynchronously; wait for that propagation to render before + // capturing. + await page.waitForTimeout(1500); + await waitForPaintToSettle(page); await checkForScreenshot( page, @@ -61,6 +71,8 @@ test('should properly display MPR for MR', async ({ await mainToolbarPageObject.layoutSelection.axialPrimary.click(); await viewportRenderAfterLayoutChange; + await page.waitForTimeout(1000); + await waitForPaintToSettle(page); await checkForScreenshot( page, diff --git a/tests/SEGNoHydrationThenMPR.spec.ts b/tests/SEGNoHydrationThenMPR.spec.ts index 5e3950d0caf..d692a3bd054 100644 --- a/tests/SEGNoHydrationThenMPR.spec.ts +++ b/tests/SEGNoHydrationThenMPR.spec.ts @@ -1,4 +1,10 @@ -import { checkForScreenshot, screenShotPaths, test, visitStudy } from './utils'; +import { + checkForScreenshot, + screenShotPaths, + test, + visitStudy, + waitForPaintToSettle, +} from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.12.2.1107.5.2.32.35162.30000015050317233592200000046'; @@ -27,6 +33,7 @@ test('should launch MPR with unhydrated SEG', async ({ await mainToolbarPageObject.layoutSelection.MPR.click(); await page.waitForTimeout(5000); + await waitForPaintToSettle(page); await checkForScreenshot( page, diff --git a/tests/SRHydration.spec.ts b/tests/SRHydration.spec.ts index 455f0744398..7437c55c04f 100644 --- a/tests/SRHydration.spec.ts +++ b/tests/SRHydration.spec.ts @@ -1,4 +1,11 @@ -import { checkForScreenshot, screenShotPaths, test, visitStudy } from './utils'; +import { + checkForScreenshot, + screenShotPaths, + test, + visitStudy, + waitForPaintToSettle, + waitForViewportsRendered, +} from './utils'; test.beforeEach(async ({ page }) => { const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7695.4007.324475281161490036195179843543'; @@ -16,7 +23,13 @@ test('should hydrate SR reports correctly', async ({ await rightPanelPageObject.toggle(); await rightPanelPageObject.measurementsPanel.select(); await leftPanelPageObject.loadSeriesByModality('SR'); + // The DICOMSRDisplayTool bails out when the viewport has no actors yet + // (see DICOMSRDisplayTool's hasActors guard), so we must wait until the + // underlying image has rendered an actor before screenshotting the SR + // overlay (line/rectangle). + await waitForViewportsRendered(page); await page.waitForTimeout(2000); + await waitForPaintToSettle(page); await checkForScreenshot( page, viewportPageObject.grid, diff --git a/tests/Scoord3dProbe.spec.ts b/tests/Scoord3dProbe.spec.ts index 4cbfd4ed072..c03f0e0126b 100644 --- a/tests/Scoord3dProbe.spec.ts +++ b/tests/Scoord3dProbe.spec.ts @@ -1,5 +1,13 @@ import { Locator } from '@playwright/test'; -import { checkForScreenshot, expect, screenShotPaths, test, visitStudy } from './utils'; +import { + checkForScreenshot, + expect, + screenShotPaths, + test, + visitStudy, + waitForPaintToSettle, waitForViewportRenderCycle, + waitForViewportsRendered, +} from './utils'; async function expectNonEmptyDetailLines(lines: Locator) { const lineCount = await lines.count(); @@ -27,7 +35,7 @@ test.beforeEach(async ({ page }) => { } }); }); - +test.describe.configure({ retries: 1 }); test('should hydrate SCOORD3D probe measurements correctly', async ({ page, DOMOverlayPageObject, @@ -90,8 +98,12 @@ test('should hydrate SCOORD3D probe measurements correctly', async ({ // Click the hydrate button to load the SCOORD3D probe measurements await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); - // Wait for hydration to complete and rendering to stabilize + // Wait for hydration to complete and rendering to stabilize. SCOORD3D + // hydration can swap the displayed series (referenced vs. current volume), + // so we must wait for the new image set to render before screenshotting. + await waitForViewportsRendered(page); await page.waitForTimeout(3000); + await waitForPaintToSettle(page); // Take screenshot after hydration showing the probe measurements - use viewport locator await checkForScreenshot( @@ -129,9 +141,16 @@ test('should hydrate SCOORD3D probe measurements correctly', async ({ viewport.render(); } }); + await waitForViewportsRendered(page, { waitVolumeLoad: false }); // Click on a data row to jump to the measurement + const jumpRenderCycle = waitForViewportRenderCycle(page, { + renderedTimeout: 30000, + waitVolumeLoad: false, + }); await rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0).click(); + await jumpRenderCycle; + await waitForPaintToSettle(page); // Take screenshot showing the jump to measurement functionality - use viewport locator await checkForScreenshot( diff --git a/tests/ScoordRectangle.spec.ts b/tests/ScoordRectangle.spec.ts index b025eaa128e..d997c7638de 100644 --- a/tests/ScoordRectangle.spec.ts +++ b/tests/ScoordRectangle.spec.ts @@ -7,6 +7,8 @@ import { screenShotPaths, test, visitStudy, + waitForPaintToSettle, + waitForViewportsRendered, } from './utils'; async function expectNonEmptyDetailLines(lines: Locator) { @@ -53,10 +55,13 @@ test('should hydrate SCOORD rectangle measurements correctly', async ({ // Double-click on the study browser thumbnail to load the SR await leftPanelPageObject.loadSeriesByModality('SR'); - await page.waitForTimeout(2000); - - // Wait for the SR to load and stabilize before taking screenshot - await page.waitForTimeout(1000); + // The DICOMSRDisplayTool only draws the SCOORD rectangle once the + // underlying image has been rendered (it bails out when the viewport + // has no actors). Wait for the image to render before checking the + // overlay, otherwise the screenshot captures a blank canvas. + await waitForViewportsRendered(page); + await page.waitForTimeout(3000); + await waitForPaintToSettle(page); const activeViewport = await viewportPageObject.active; diff --git a/tests/Spline.spec.ts b/tests/Spline.spec.ts index 7e77423cfba..855c00e0ffa 100644 --- a/tests/Spline.spec.ts +++ b/tests/Spline.spec.ts @@ -42,11 +42,11 @@ test('should display the spline tool', async ({ const stats = splines[0].firstTargetStats!; expect(stats.areaUnit).toBe('mm²'); - expect(Math.round(stats.area as number)).toBe(38811); + expect(Math.round(stats.area as number)).toBe(38963); const lines = activeViewport.getSvgAnnotationStatTextLines(splines[0].annotationUID); await expect(lines).toHaveCount(1); - await expect(lines.nth(0)).toHaveText('Area: 38811 mm²'); + await expect(lines.nth(0)).toHaveText('Area: 38963 mm²'); }); test('should restore viewport interactivity after deleting an in-progress Spline annotation via context menu', async ({ diff --git a/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png b/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png index 9bc84ce183b..9fc3ae3839c 100644 Binary files a/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png and b/tests/screenshots/chromium/3DFourUp.spec.ts/threeDFourUpDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png b/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png index b52a7b753d7..dd5fa3bde68 100644 Binary files a/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png and b/tests/screenshots/chromium/3DMain.spec.ts/threeDMainDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png b/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png index 2e497ad6bfa..b2b2cbc03cf 100644 Binary files a/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png and b/tests/screenshots/chromium/3DOnly.spec.ts/threeDOnlyDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png b/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png index 031948228dd..71407494f65 100644 Binary files a/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png and b/tests/screenshots/chromium/3DPrimary.spec.ts/threeDPrimaryDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png b/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png index 9cf54542183..6a3ff0c698b 100644 Binary files a/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png and b/tests/screenshots/chromium/Angle.spec.ts/angleDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly0.png b/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly0.png index 2c9b0bbb599..f2320482dee 100644 Binary files a/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly0.png and b/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly0.png differ diff --git a/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly1.png b/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly1.png index de6f8b5ce7a..c776ce9d175 100644 Binary files a/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly1.png and b/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly1.png differ diff --git a/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly2.png b/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly2.png index e9c302fb4e7..fde45861360 100644 Binary files a/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly2.png and b/tests/screenshots/chromium/ArrowAnnotate.spec.ts/arrowAnnotateDisplayedCorrectly2.png differ diff --git a/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png b/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png index 054b19d4e35..a8d4e401a25 100644 Binary files a/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png and b/tests/screenshots/chromium/AxialPrimary.spec.ts/axialPrimaryDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png b/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png index 61390a853ab..4ac227ecaf6 100644 Binary files a/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png and b/tests/screenshots/chromium/Bidirectional.spec.ts/bidirectionalDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png b/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png index 29bcf4b78ea..4bc54c4c984 100644 Binary files a/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png and b/tests/screenshots/chromium/Circle.spec.ts/circleDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png b/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png index 2d143d862e0..c969c0d8eff 100644 Binary files a/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png and b/tests/screenshots/chromium/CobbAngle.spec.ts/cobbangleDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/ContextMenu.spec.ts/contextMenuNearBottomEdgeNotClipped.png b/tests/screenshots/chromium/ContextMenu.spec.ts/contextMenuNearBottomEdgeNotClipped.png index 2f216fd869e..829ead02681 100644 Binary files a/tests/screenshots/chromium/ContextMenu.spec.ts/contextMenuNearBottomEdgeNotClipped.png and b/tests/screenshots/chromium/ContextMenu.spec.ts/contextMenuNearBottomEdgeNotClipped.png differ diff --git a/tests/screenshots/chromium/ContextMenu.spec.ts/preContextMenuNearBottomEdge.png b/tests/screenshots/chromium/ContextMenu.spec.ts/preContextMenuNearBottomEdge.png index 8c52f7ee564..5f24b21ff5d 100644 Binary files a/tests/screenshots/chromium/ContextMenu.spec.ts/preContextMenuNearBottomEdge.png and b/tests/screenshots/chromium/ContextMenu.spec.ts/preContextMenuNearBottomEdge.png differ diff --git a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/noOverlay.png b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/noOverlay.png index 66d160553fb..bd748eb3ce0 100644 Binary files a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/noOverlay.png and b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/noOverlay.png differ diff --git a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlay2d-tta-nnU-Net-Segmentation.png b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlay2d-tta-nnU-Net-Segmentation.png index 77f33aede98..3dce977ab1c 100644 Binary files a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlay2d-tta-nnU-Net-Segmentation.png and b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlay2d-tta-nnU-Net-Segmentation.png differ diff --git a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWith2d-tta-nnU-Net-SegmentationSelected.png b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWith2d-tta-nnU-Net-SegmentationSelected.png index 7dc163bedbd..6baa7222f0c 100644 Binary files a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWith2d-tta-nnU-Net-SegmentationSelected.png and b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWith2d-tta-nnU-Net-SegmentationSelected.png differ diff --git a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationOverlaysRemoved.png b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationOverlaysRemoved.png index 1576b01fb13..964f6c22921 100644 Binary files a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationOverlaysRemoved.png and b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationOverlaysRemoved.png differ diff --git a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationSelected.png b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationSelected.png index 09504bdb75d..d49029f4a68 100644 Binary files a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationSelected.png and b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlayMenuWithSegmentationSelected.png differ diff --git a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlaySegmentation.png b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlaySegmentation.png index 70c1d83201d..5e6cc912899 100644 Binary files a/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlaySegmentation.png and b/tests/screenshots/chromium/DataOverlayMenu.spec.ts/overlaySegmentation.png differ diff --git a/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png b/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png index 5f80dfa3d9a..d015cd0ccf6 100644 Binary files a/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png and b/tests/screenshots/chromium/DicomTagBrowser.spec.ts/dicomTagBrowserDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/DicomTagBrowser.spec.ts/scrollBarRenderedProperly.png b/tests/screenshots/chromium/DicomTagBrowser.spec.ts/scrollBarRenderedProperly.png index 7a13cbff193..daaf876117e 100644 Binary files a/tests/screenshots/chromium/DicomTagBrowser.spec.ts/scrollBarRenderedProperly.png and b/tests/screenshots/chromium/DicomTagBrowser.spec.ts/scrollBarRenderedProperly.png differ diff --git a/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png b/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png index 554cc4b0da7..39be028711a 100644 Binary files a/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png and b/tests/screenshots/chromium/Ellipse.spec.ts/ellipseDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png b/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png index 917aea44640..b07bb5ee4a7 100644 Binary files a/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png and b/tests/screenshots/chromium/FlipHorizontal.spec.ts/flipHorizontalDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png b/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png index 37eaaf43d9f..e95d6c5d6ea 100644 Binary files a/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png and b/tests/screenshots/chromium/Invert.spec.ts/invertDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png index 49899bda7f9..442bca0b77d 100644 Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-changeSeriesInMPR.png differ diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png index a07c2a4f041..dad86b92f4d 100644 Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-initialDraw.png differ diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png index bebdbc933de..ecca0ea79aa 100644 Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpInMPR.png differ diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png index 50df3ac155d..ecca0ea79aa 100644 Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementAfterSeriesChange.png differ diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png index a07c2a4f041..dad86b92f4d 100644 Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-jumpToMeasurementStack.png differ diff --git a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png index a60b3382a54..01be870d9a6 100644 Binary files a/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png and b/tests/screenshots/chromium/JumpToMeasurementMPR.spec.ts/jumpToMeasurementMPR-scrollAway.png differ diff --git a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPostEdit.png b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPostEdit.png index a2e30cdb647..0414d92f3a8 100644 Binary files a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPostEdit.png and b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPostEdit.png differ diff --git a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPreEdit.png b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPreEdit.png index d6909ca7127..f16811ca063 100644 Binary files a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPreEdit.png and b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/lockedSegPreEdit.png differ diff --git a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPostEdit.png b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPostEdit.png index 3b59e94f3ea..0e269eab6e6 100644 Binary files a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPostEdit.png and b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPostEdit.png differ diff --git a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPreEdit.png b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPreEdit.png index d6909ca7127..f16811ca063 100644 Binary files a/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPreEdit.png and b/tests/screenshots/chromium/LabelMapSegLocking.spec.ts/unlockedSegPreEdit.png differ diff --git a/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png b/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png index 1335df6a3ef..636d15bc932 100644 Binary files a/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png and b/tests/screenshots/chromium/Length.spec.ts/lengthDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png b/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png index c9e54a54ec3..9e61cdad2ce 100644 Binary files a/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png and b/tests/screenshots/chromium/Livewire.spec.ts/livewireDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png b/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png index 304eee74d9a..3ac722a766c 100644 Binary files a/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png and b/tests/screenshots/chromium/MPR.spec.ts/mprDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPostRTOverlayNoHydration.png b/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPostRTOverlayNoHydration.png index 68e6713b6e0..2e424c23184 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/MPRThenRTOverlayNoHydration.spec.ts/mprPreRTOverlayNoHydration.png b/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPreRTOverlayNoHydration.png index d968c3a5acc..d931763891c 100644 Binary files a/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPreRTOverlayNoHydration.png and b/tests/screenshots/chromium/MPRThenRTOverlayNoHydration.spec.ts/mprPreRTOverlayNoHydration.png differ diff --git a/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPostSEGOverlayNoHydration.png b/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPostSEGOverlayNoHydration.png index 444d4a0fea5..b8aa5e168d3 100644 Binary files a/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPostSEGOverlayNoHydration.png and b/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPostSEGOverlayNoHydration.png differ diff --git a/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPreSEGOverlayNoHydration.png b/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPreSEGOverlayNoHydration.png index a353dad400b..7ea3c92ca5a 100644 Binary files a/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPreSEGOverlayNoHydration.png and b/tests/screenshots/chromium/MPRThenSEGOverlayNoHydration.spec.ts/mprPreSEGOverlayNoHydration.png differ diff --git a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaySEGsAndRTDisplayed.png b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaySEGsAndRTDisplayed.png index e50a17cc569..e0cf3de47be 100644 Binary files a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaySEGsAndRTDisplayed.png 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 index 86b6ce11c8e..42f376f5cbe 100644 Binary files a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/overlaysDisplayed.png 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 index 57085b51480..ca4ec276955 100644 Binary files a/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/threeSegOverlaysInOverlayMenu.png and b/tests/screenshots/chromium/MultipleSegmentationDataOverlays.spec.ts/threeSegOverlaysInOverlayMenu.png differ diff --git a/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png b/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png index 35b43f9fd4d..49b369f0de3 100644 Binary files a/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png and b/tests/screenshots/chromium/Probe.spec.ts/probeDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png b/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png index 1532fcdefb0..1b708ee405b 100644 Binary files a/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png and b/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png differ diff --git a/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png b/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png index 5c2f3ef93ae..e10f1068bd9 100644 Binary files a/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png and b/tests/screenshots/chromium/RTDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png differ diff --git a/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPostMpr.png b/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPostMpr.png index d8af93931ec..e9cc0b673c8 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/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPreMpr.png b/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPreMpr.png index 2f3c4fca9fb..4ac2d2e0830 100644 Binary files a/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPreMpr.png and b/tests/screenshots/chromium/RTDataOverlayNoHydrationThenMPR.spec.ts/rtDataOverlayNoHydrationPreMpr.png differ diff --git a/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png b/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png index 00729d5674e..5ae3e5b551a 100644 Binary files a/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png and b/tests/screenshots/chromium/RTHydration.spec.ts/rtJumpToStructure.png differ diff --git a/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png b/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png index fa796679388..0c11b7d9323 100644 Binary files a/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png and b/tests/screenshots/chromium/RTHydration.spec.ts/rtPostHydration.png differ diff --git a/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png b/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png index 17a9bd29ec4..f66885d3c02 100644 Binary files a/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png and b/tests/screenshots/chromium/RTHydration.spec.ts/rtPreHydration.png differ diff --git a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png index 15882e19735..e69325d95a5 100644 Binary files a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png and b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPostHydration.png differ diff --git a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png index 75b26d11c44..e57a5bdb2b8 100644 Binary files a/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png and b/tests/screenshots/chromium/RTHydration2.spec.ts/rtPreHydration.png differ diff --git a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/firstLoadPostHydration.png b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/firstLoadPostHydration.png index 4b01d0fb49d..41fc10f4d2d 100644 Binary files a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/firstLoadPostHydration.png and b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/firstLoadPostHydration.png differ diff --git a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/secondLoadPostHydration.png b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/secondLoadPostHydration.png index f8f491fc1ef..9e6b592a498 100644 Binary files a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/secondLoadPostHydration.png and b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/secondLoadPostHydration.png differ diff --git a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterFirstDelete.png b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterFirstDelete.png index e4f679cb4c9..c03ff257883 100644 Binary files a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterFirstDelete.png and b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterFirstDelete.png differ diff --git a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterSecondDelete.png b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterSecondDelete.png index e4f679cb4c9..c03ff257883 100644 Binary files a/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterSecondDelete.png and b/tests/screenshots/chromium/RTHydrationDisableConfirmation.spec.ts/viewportAfterSecondDelete.png differ diff --git a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRT.png b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRT.png index 85be6188cd9..3e063e7e442 100644 Binary files a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRT.png and b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRT.png differ diff --git a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydrated.png b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydrated.png index d10422c494b..25d35f6753c 100644 Binary files a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydrated.png and b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydrated.png differ diff --git a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydratedAfterLayoutChange.png b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprAfterRTHydratedAfterLayoutChange.png index 503f0e3c313..53a36032267 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/RTHydrationFromMPR.spec.ts/mprBeforeRT.png b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprBeforeRT.png index 4767dde4997..65a0df41531 100644 Binary files a/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprBeforeRT.png and b/tests/screenshots/chromium/RTHydrationFromMPR.spec.ts/mprBeforeRT.png differ diff --git a/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydration.png b/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydration.png index c287fa1e328..119a972ed51 100644 Binary files a/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydration.png and b/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydration.png differ diff --git a/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydrationMPRAxialPrimary.png b/tests/screenshots/chromium/RTHydrationThenMPR.spec.ts/rtPostHydrationMPRAxialPrimary.png index 2cfd58bb2b7..3602371c292 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 706d2e4444e..123966f3abb 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/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPreMpr.png b/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPreMpr.png index 55978688379..1ef8502132d 100644 Binary files a/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPreMpr.png and b/tests/screenshots/chromium/RTNoHydrationThenMPR.spec.ts/rtNoHydrationPreMpr.png differ diff --git a/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png b/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png index b8f6b7d9854..b06211dd1cc 100644 Binary files a/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png and b/tests/screenshots/chromium/Rectangle.spec.ts/rectangleDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png b/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png index ae700623db5..f9ee420ada2 100644 Binary files a/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png and b/tests/screenshots/chromium/Reset.spec.ts/resetDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png b/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png index 63c5e16e4ed..05c01ec435e 100644 Binary files a/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png and b/tests/screenshots/chromium/RotateRight.spec.ts/rotateRightDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png b/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png index 408126841ac..58f9d513486 100644 Binary files a/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png and b/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayFirstImage.png differ diff --git a/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png b/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png index 58eb045d22c..522944e0b83 100644 Binary files a/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png and b/tests/screenshots/chromium/SEGDataOverlayForUnreferencedDisplaySetNoHydration.spec.ts/overlayMiddleImage.png differ diff --git a/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPostMpr.png b/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPostMpr.png index 86f91ed840a..0267c2e353d 100644 Binary files a/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPostMpr.png and b/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPostMpr.png differ diff --git a/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPreMpr.png b/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPreMpr.png index 3a42950e7a6..63b481d1d28 100644 Binary files a/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPreMpr.png and b/tests/screenshots/chromium/SEGDataOverlayNoHydrationThenMPR.spec.ts/segDataOverlayNoHydrationPreMpr.png differ diff --git a/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/brushTool.png b/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/brushTool.png index d595c66c237..ccdf2458030 100644 Binary files a/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/brushTool.png and b/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/brushTool.png differ diff --git a/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/eraserTool.png b/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/eraserTool.png index f33c0c0a601..0402c7f4aa7 100644 Binary files a/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/eraserTool.png and b/tests/screenshots/chromium/SEGDrawingToolsResizing.spec.ts/eraserTool.png differ diff --git a/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png b/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png index 229fed8cb47..9029a4892b6 100644 Binary files a/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png and b/tests/screenshots/chromium/SEGHydration.spec.ts/segPostHydration.png differ diff --git a/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png b/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png index 992a0d2fb7d..c5512fdf1ef 100644 Binary files a/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png and b/tests/screenshots/chromium/SEGHydration.spec.ts/segPreHydration.png differ diff --git a/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondDelete.png b/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondDelete.png index 0cb3b035817..3de3ec77a13 100644 Binary files a/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondDelete.png and b/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondDelete.png differ diff --git a/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondHydration.png b/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondHydration.png index c4a2327561b..9684c995e2e 100644 Binary files a/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondHydration.png and b/tests/screenshots/chromium/SEGHydrationDeleteAndReload.spec.ts/viewportAfterSecondHydration.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/afterSEGHydrated.png b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/afterSEGHydrated.png index 10799506367..5417486b7de 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/afterSEGHydrated.png and b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/afterSEGHydrated.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/backTo3DFourUp.png b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/backTo3DFourUp.png index 72463dcbb64..fa0a1d44bbc 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/backTo3DFourUp.png and b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/backTo3DFourUp.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSEG.png b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSEG.png index d052ed32258..056f5486291 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSEG.png and b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSEG.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSegHydrated.png b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSegHydrated.png index c355f9c5082..84195676641 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSegHydrated.png and b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpAfterSegHydrated.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpBeforeSEG.png b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpBeforeSEG.png index e23700251e2..2c6de1444c3 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpBeforeSEG.png and b/tests/screenshots/chromium/SEGHydrationFrom3DFourUp.spec.ts/threeDFourUpBeforeSEG.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png index 678d23c5b83..f8ee1ec97ef 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSEG.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png index dc25bf7226c..6a429a67514 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydrated.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png index 70217e997d7..2c03700a69e 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprAfterSegHydratedAfterLayoutChange.png differ diff --git a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png index 73d4d7fd285..7ea3c92ca5a 100644 Binary files a/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png and b/tests/screenshots/chromium/SEGHydrationFromMPR.spec.ts/mprBeforeSEG.png differ diff --git a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png index bb380f78569..0f0cded2d36 100644 Binary files a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png and b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydration.png differ diff --git a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png index c95c457ffb4..d73ee3523f0 100644 Binary files a/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png and b/tests/screenshots/chromium/SEGHydrationThenMPR.spec.ts/segPostHydrationMPRAxialPrimary.png differ diff --git a/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPostMpr.png b/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPostMpr.png index e87d540ec7f..2f6bac3cbe9 100644 Binary files a/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPostMpr.png and b/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPostMpr.png differ diff --git a/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPreMpr.png b/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPreMpr.png index 8a6f854c201..006c659572d 100644 Binary files a/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPreMpr.png and b/tests/screenshots/chromium/SEGNoHydrationThenMPR.spec.ts/segNoHydrationPreMpr.png differ diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png b/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png index 8a99e37f797..4426497c14d 100644 Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srJumpToMeasurement.png differ diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png b/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png index 7441565a546..7f24d0d290b 100644 Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srPostHydration.png differ diff --git a/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png b/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png index 6b4ea8bf1b0..8476f80d84f 100644 Binary files a/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png and b/tests/screenshots/chromium/SRHydration.spec.ts/srPreHydration.png differ diff --git a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeDisplayedCorrectly.png b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeDisplayedCorrectly.png index 4944ffe9325..49603cd09da 100644 Binary files a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeDisplayedCorrectly.png and b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeJumpToMeasurement.png b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeJumpToMeasurement.png index 90fad7e7229..7df0682f288 100644 Binary files a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeJumpToMeasurement.png and b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbeJumpToMeasurement.png differ diff --git a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePostHydration.png b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePostHydration.png index 7923ee718f5..520a378cd5a 100644 Binary files a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePostHydration.png and b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePostHydration.png differ diff --git a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePreHydration.png b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePreHydration.png index 3b84ddbd8fa..f20703423cb 100644 Binary files a/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePreHydration.png and b/tests/screenshots/chromium/Scoord3dProbe.spec.ts/scoord3dProbePreHydration.png differ diff --git a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleDisplayedCorrectly.png b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleDisplayedCorrectly.png index ab0ce4e2651..f3c2bcd3325 100644 Binary files a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleDisplayedCorrectly.png and b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleJumpToMeasurement.png b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleJumpToMeasurement.png index 9ef09944468..57f9141c8c9 100644 Binary files a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleJumpToMeasurement.png and b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectangleJumpToMeasurement.png differ diff --git a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePostHydration.png b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePostHydration.png index fb8a8c4599c..a61e420a450 100644 Binary files a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePostHydration.png and b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePostHydration.png differ diff --git a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePreHydration.png b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePreHydration.png index de648f15f9c..adb054e9e40 100644 Binary files a/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePreHydration.png and b/tests/screenshots/chromium/ScoordRectangle.spec.ts/scoordRectanglePreHydration.png differ diff --git a/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png b/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png index dcfd5920fa2..8491f2f80cf 100644 Binary files a/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png and b/tests/screenshots/chromium/Spline.spec.ts/splineDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/Worklist.spec.ts/scrollBarRenderedProperly.png b/tests/screenshots/chromium/Worklist.spec.ts/scrollBarRenderedProperly.png index fd98dce2ca8..be10c2a6f43 100644 Binary files a/tests/screenshots/chromium/Worklist.spec.ts/scrollBarRenderedProperly.png and b/tests/screenshots/chromium/Worklist.spec.ts/scrollBarRenderedProperly.png differ diff --git a/tests/screenshots/chromium/ZoomIn.spec.ts/magnifyViewportDisplayedCorrectly.png b/tests/screenshots/chromium/ZoomIn.spec.ts/magnifyViewportDisplayedCorrectly.png index 909c553a356..9c33cc97b33 100644 Binary files a/tests/screenshots/chromium/ZoomIn.spec.ts/magnifyViewportDisplayedCorrectly.png and b/tests/screenshots/chromium/ZoomIn.spec.ts/magnifyViewportDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png index 27227adb139..6f2c5c7dc8b 100644 Binary files a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png and b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectly.png differ diff --git a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png index 1fd82a4d3ca..bcb2b498057 100644 Binary files a/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png and b/tests/screenshots/chromium/mpr2.spec.ts/mprDisplayedCorrectlyZoomed.png differ diff --git a/tests/utils/index.ts b/tests/utils/index.ts index a7f7e46a5fc..281b9eb64d2 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -32,6 +32,7 @@ import { waitForAnyViewportNeedsRender, waitForViewportsRendered, waitForViewportRenderCycle, + waitForPaintToSettle, } from './waitForViewportsRendered'; export { @@ -62,6 +63,7 @@ export { waitForAnyViewportNeedsRender, waitForViewportsRendered, waitForViewportRenderCycle, + waitForPaintToSettle, test, expect, }; diff --git a/tests/utils/waitForViewportsRendered.ts b/tests/utils/waitForViewportsRendered.ts index 261a0e43a9e..f3482c8f576 100644 --- a/tests/utils/waitForViewportsRendered.ts +++ b/tests/utils/waitForViewportsRendered.ts @@ -11,6 +11,12 @@ type WaitForViewportsRenderedOptions = { * viewports to report loaded. */ waitVolumeLoad?: boolean; + /** + * If true (default), inserts a post-rendered "settle" step that waits two + * animation frames and a short idle window so the GPU has presented the + * final paint before screenshots are taken. + */ + settle?: boolean; }; type WaitForRenderCycleToCompleteOptions = { @@ -28,6 +34,10 @@ type WaitForRenderCycleToCompleteOptions = { * viewports to report loaded during the rendered phase. */ waitVolumeLoad?: boolean; + /** + * If true (default), inserts a post-rendered "settle" step before resolving. + */ + settle?: boolean; }; /** @@ -39,9 +49,14 @@ const waitForViewportRenderCycle = async ( page: Page, options: WaitForRenderCycleToCompleteOptions = {} ) => { - const { needsRenderTimeout = 5000, renderedTimeout = 15000, waitVolumeLoad = true } = options; + const { + needsRenderTimeout = 5000, + renderedTimeout = 15000, + waitVolumeLoad = true, + settle = true, + } = options; await waitForAnyViewportNeedsRender(page, { timeout: needsRenderTimeout }); - await waitForViewportsRendered(page, { timeout: renderedTimeout, waitVolumeLoad }); + await waitForViewportsRendered(page, { timeout: renderedTimeout, waitVolumeLoad, settle }); }; /** @@ -93,7 +108,7 @@ const waitForViewportsRendered = async ( page: Page, options: WaitForViewportsRenderedOptions = {} ) => { - const { timeout = 15000, waitVolumeLoad = true } = options; + const { timeout = 15000, waitVolumeLoad = true, settle = true } = options; await page.waitForFunction( ({ waitVolumeLoad }) => { @@ -158,6 +173,45 @@ const waitForViewportsRendered = async ( { waitVolumeLoad }, { timeout } ); + + if (settle) { + await waitForPaintToSettle(page); + } }; -export { waitForViewportsRendered, waitForAnyViewportNeedsRender, waitForViewportRenderCycle }; +/** + * After the render-cycle predicate has resolved, give the browser two animation + * frames (RAF fires *before* the paint commits, so we wait for two consecutive + * frames plus a microtask) followed by a short idle window. This drains the + * tail of any progressive texture uploads that landed in the same tick the + * `loadStatus.loaded` flag was flipped, so screenshots capture the final paint + * rather than a mid-stream frame. Mirrors the `scheduleAfterPaint` deferral in + * BaseStreamingImageVolume on the cs3d side. + */ +const waitForPaintToSettle = async (page: Page) => { + await page.evaluate( + () => + new Promise(resolve => { + const raf = (cb: FrameRequestCallback) => + typeof requestAnimationFrame === 'function' + ? requestAnimationFrame(cb) + : (setTimeout(cb, 16) as unknown as number); + raf(() => + raf(() => { + Promise.resolve().then(resolve); + }) + ); + }) + ); + // Final guard against stragglers (e.g. label-outline edge detection that + // queues an extra render via requestAnimationFrame from inside an event + // handler). + await page.waitForTimeout(150); +}; + +export { + waitForViewportsRendered, + waitForAnyViewportNeedsRender, + waitForViewportRenderCycle, + waitForPaintToSettle, +};