fix: ohif tests to run with cornerstone 3d 5.0 (#6043)
* chore(tests): Update multiple screenshot test images for various specs * feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts * fix(DICOMSRDisplayTool): Improve actor presence check in viewport * chore(tests): Update multiple screenshot assets for various specs * chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability * chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs * test: update progressive loading screenshots * jest 30 test fixes for compatibility with pnpm cs3d * Use correct setDisplaySets instead of setDataId * fix: Naming change for LegacyVolumeViewport3D * Update to allow tolerance for contour tests * update * fix * refactor: Replace instanceof checks with utility functions for viewport type validation * fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully * bun lock * fix: Install cs3d with pnpm instead of bun * Update node version for playwright * Update to v5.0.0 of cs3d * fix: Build dependency * audit * Change to a web await retry assert * Fix timing related test failures * fix: Freehand close --------- Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
No files matched your search
@@ -1,4 +1,11 @@
|
||||
import { expect, test, visitStudy, getSvgAttribute, navigateWithViewportArrow } from './utils';
|
||||
import {
|
||||
expect,
|
||||
test,
|
||||
visitStudy,
|
||||
getSvgAttribute,
|
||||
navigateWithViewportArrow,
|
||||
waitForViewportsRendered,
|
||||
} from './utils';
|
||||
import { expectRowSelected } from './utils/assertions';
|
||||
|
||||
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
|
||||
@@ -20,12 +27,21 @@ test.beforeEach(async ({
|
||||
});
|
||||
|
||||
test('should navigate the contours when clicking each segments in the right panel', async ({
|
||||
page,
|
||||
rightPanelPageObject,
|
||||
viewportPageObject,
|
||||
}) => {
|
||||
const getSegment = (index: number) =>
|
||||
rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(index);
|
||||
|
||||
// Clicking a segment navigates the viewport to that segment's slice and
|
||||
// re-renders its contour. Wait for the render to settle before reading the
|
||||
// SVG path so we don't capture the previous segment's path on slower CI.
|
||||
const selectSegment = async (index: number) => {
|
||||
await getSegment(index).click();
|
||||
await waitForViewportsRendered(page);
|
||||
};
|
||||
|
||||
const seg0 = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -33,7 +49,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
});
|
||||
expect(seg0, 'Segment at index 0: expected a non-null SVG path').not.toBeNull();
|
||||
|
||||
await getSegment(3).click();
|
||||
await selectSegment(3);
|
||||
const seg3 = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -42,7 +58,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
expect(seg3, 'Segment at index 3: expected a non-null SVG path').not.toBeNull();
|
||||
await expectRowSelected(getSegment(3));
|
||||
|
||||
await getSegment(2).click();
|
||||
await selectSegment(2);
|
||||
const seg2 = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -51,7 +67,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
expect(seg2, 'Segment at index 2: expected a non-null SVG path').not.toBeNull();
|
||||
await expectRowSelected(getSegment(2));
|
||||
|
||||
await getSegment(1).click();
|
||||
await selectSegment(1);
|
||||
const seg1 = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -61,7 +77,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
await expectRowSelected(getSegment(1));
|
||||
|
||||
// Clicking segments again should return the original paths
|
||||
await getSegment(2).click();
|
||||
await selectSegment(2);
|
||||
const seg2Again = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -70,7 +86,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
expect(seg2Again, 'Segment 2 again: expected to match the original segment 2 path').toBe(seg2);
|
||||
await expectRowSelected(getSegment(2));
|
||||
|
||||
await getSegment(1).click();
|
||||
await selectSegment(1);
|
||||
const seg1Again = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -79,7 +95,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
expect(seg1Again, 'Segment 1 again: expected to match the original segment 1 path').toBe(seg1);
|
||||
await expectRowSelected(getSegment(1));
|
||||
|
||||
await getSegment(0).click();
|
||||
await selectSegment(0);
|
||||
const seg0Again = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -88,7 +104,7 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
expect(seg0Again, 'Segment 0 again: expected to match the original segment 0 path').toBe(seg0);
|
||||
await expectRowSelected(getSegment(0));
|
||||
|
||||
await getSegment(3).click();
|
||||
await selectSegment(3);
|
||||
const seg3Again = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -99,12 +115,20 @@ test('should navigate the contours when clicking each segments in the right pane
|
||||
});
|
||||
|
||||
test('should navigate the segmentations using the Viewport arrow buttons', async ({
|
||||
page,
|
||||
rightPanelPageObject,
|
||||
viewportPageObject,
|
||||
}) => {
|
||||
const getSegment = (index: number) =>
|
||||
rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(index);
|
||||
|
||||
// Arrow navigation jumps to the next/prev segment and re-renders its contour.
|
||||
// Wait for the render to settle before reading the SVG path.
|
||||
const navigate = async (direction: 'next' | 'prev') => {
|
||||
await navigateWithViewportArrow(viewportPageObject, direction);
|
||||
await waitForViewportsRendered(page);
|
||||
};
|
||||
|
||||
const initialSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -114,7 +138,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
// Expect the correct segment to be selected in the right panel
|
||||
await expectRowSelected(getSegment(0));
|
||||
|
||||
await navigateWithViewportArrow(viewportPageObject, 'next');
|
||||
await navigate('next');
|
||||
const secondSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -123,7 +147,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
expect(secondSvgPath, 'Segment at index 1: expected a different SVG path from segment 0').not.toBe(initialSvgPath);
|
||||
await expectRowSelected(getSegment(1));
|
||||
|
||||
await navigateWithViewportArrow(viewportPageObject, 'next');
|
||||
await navigate('next');
|
||||
const thirdSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -132,7 +156,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
expect(thirdSvgPath, 'Segment at index 2: expected a different SVG path from segment 1').not.toBe(secondSvgPath);
|
||||
await expectRowSelected(getSegment(2));
|
||||
|
||||
await navigateWithViewportArrow(viewportPageObject, 'next');
|
||||
await navigate('next');
|
||||
const fourthSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -142,7 +166,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
await expectRowSelected(getSegment(3));
|
||||
|
||||
// Wraparound test — next from last should return to first segment
|
||||
await navigateWithViewportArrow(viewportPageObject, 'next');
|
||||
await navigate('next');
|
||||
const svgPathWraparoundWithNext = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -155,7 +179,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
await expectRowSelected(getSegment(0));
|
||||
|
||||
// Wraparound test — prev from first should return to last segment
|
||||
await navigateWithViewportArrow(viewportPageObject, 'prev');
|
||||
await navigate('prev');
|
||||
const svgPathWraparoundWithPrev = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -167,7 +191,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
).toBe(fourthSvgPath);
|
||||
await expectRowSelected(getSegment(3));
|
||||
|
||||
await navigateWithViewportArrow(viewportPageObject, 'prev');
|
||||
await navigate('prev');
|
||||
const backToThirdSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -176,7 +200,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
expect(backToThirdSvgPath, 'Expected path to match third segment after going prev from fourth').toBe(thirdSvgPath);
|
||||
await expectRowSelected(getSegment(2));
|
||||
|
||||
await navigateWithViewportArrow(viewportPageObject, 'prev');
|
||||
await navigate('prev');
|
||||
const backToSecondSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
@@ -185,7 +209,7 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
|
||||
expect(backToSecondSvgPath, 'Expected path to match second segment after going prev from third').toBe(secondSvgPath);
|
||||
await expectRowSelected(getSegment(1));
|
||||
|
||||
await navigateWithViewportArrow(viewportPageObject, 'prev');
|
||||
await navigate('prev');
|
||||
const backToFirstSvgPath = await getSvgAttribute({
|
||||
viewportPageObject,
|
||||
svgInnerElement: 'path',
|
||||
|
||||
@@ -126,8 +126,9 @@ test.describe('Crosshairs Test', async () => {
|
||||
await viewportPageObject.crosshairs.axial.rotate();
|
||||
await viewportPageObject.crosshairs.sagittal.rotate();
|
||||
await viewportPageObject.crosshairs.coronal.rotate();
|
||||
const afterRotate = await getCrosshairsSignature(page);
|
||||
expect(afterRotate).not.toEqual(beforeRotate);
|
||||
await expect
|
||||
.poll(() => getCrosshairsSignature(page), { timeout: CROSSHAIRS_DOM_TIMEOUT_MS })
|
||||
.not.toEqual(beforeRotate);
|
||||
});
|
||||
|
||||
test('should allow the user to adjust the slab thickness', async ({
|
||||
@@ -143,8 +144,9 @@ test.describe('Crosshairs Test', async () => {
|
||||
await viewportPageObject.crosshairs.axial.increase();
|
||||
await viewportPageObject.crosshairs.sagittal.increase();
|
||||
await viewportPageObject.crosshairs.coronal.increase();
|
||||
const afterIncrease = await getLayerSignature(page, 'svg-layer-mpr-axial');
|
||||
expect(afterIncrease).not.toEqual(beforeIncrease);
|
||||
await expect
|
||||
.poll(() => getLayerSignature(page, 'svg-layer-mpr-axial'), { timeout: CROSSHAIRS_DOM_TIMEOUT_MS })
|
||||
.not.toEqual(beforeIncrease);
|
||||
});
|
||||
|
||||
test('should reset the crosshairs to the initial position when reset is clicked', async ({
|
||||
@@ -160,14 +162,27 @@ test.describe('Crosshairs Test', async () => {
|
||||
await viewportPageObject.crosshairs.axial.rotate();
|
||||
await viewportPageObject.crosshairs.sagittal.rotate();
|
||||
await viewportPageObject.crosshairs.coronal.rotate();
|
||||
const rotatedPosition = await getCrosshairsSignature(page);
|
||||
expect(rotatedPosition).not.toEqual(initialPosition);
|
||||
await expect
|
||||
.poll(() => getCrosshairsSignature(page), { timeout: CROSSHAIRS_DOM_TIMEOUT_MS })
|
||||
.not.toEqual(initialPosition);
|
||||
|
||||
await mainToolbarPageObject.moreTools.reset.click();
|
||||
await expectCrosshairsReady(page);
|
||||
expect(await areLayerLinesAxisAligned(page, 'svg-layer-mpr-axial')).toBeTruthy();
|
||||
expect(await areLayerLinesAxisAligned(page, 'svg-layer-mpr-sagittal')).toBeTruthy();
|
||||
expect(await areLayerLinesAxisAligned(page, 'svg-layer-mpr-coronal')).toBeTruthy();
|
||||
await expect
|
||||
.poll(() => areLayerLinesAxisAligned(page, 'svg-layer-mpr-axial'), {
|
||||
timeout: CROSSHAIRS_DOM_TIMEOUT_MS,
|
||||
})
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => areLayerLinesAxisAligned(page, 'svg-layer-mpr-sagittal'), {
|
||||
timeout: CROSSHAIRS_DOM_TIMEOUT_MS,
|
||||
})
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => areLayerLinesAxisAligned(page, 'svg-layer-mpr-coronal'), {
|
||||
timeout: CROSSHAIRS_DOM_TIMEOUT_MS,
|
||||
})
|
||||
.toBe(true);
|
||||
});
|
||||
|
||||
test('should reset the crosshairs when a new displayset is loaded', async ({
|
||||
@@ -184,13 +199,26 @@ test.describe('Crosshairs Test', async () => {
|
||||
await viewportPageObject.crosshairs.axial.rotate();
|
||||
await viewportPageObject.crosshairs.sagittal.rotate();
|
||||
await viewportPageObject.crosshairs.coronal.rotate();
|
||||
const rotatedPosition = await getCrosshairsSignature(page);
|
||||
expect(rotatedPosition).not.toEqual(initialPosition);
|
||||
await expect
|
||||
.poll(() => getCrosshairsSignature(page), { timeout: CROSSHAIRS_DOM_TIMEOUT_MS })
|
||||
.not.toEqual(initialPosition);
|
||||
|
||||
await leftPanelPageObject.loadSeriesByDescription('Recon 3: LIVER 3 PHASE (AP)');
|
||||
await expectCrosshairsReady(page);
|
||||
expect(await areLayerLinesAxisAligned(page, 'svg-layer-mpr-axial')).toBeTruthy();
|
||||
expect(await areLayerLinesAxisAligned(page, 'svg-layer-mpr-sagittal')).toBeTruthy();
|
||||
expect(await areLayerLinesAxisAligned(page, 'svg-layer-mpr-coronal')).toBeTruthy();
|
||||
await expect
|
||||
.poll(() => areLayerLinesAxisAligned(page, 'svg-layer-mpr-axial'), {
|
||||
timeout: CROSSHAIRS_DOM_TIMEOUT_MS,
|
||||
})
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => areLayerLinesAxisAligned(page, 'svg-layer-mpr-sagittal'), {
|
||||
timeout: CROSSHAIRS_DOM_TIMEOUT_MS,
|
||||
})
|
||||
.toBe(true);
|
||||
await expect
|
||||
.poll(() => areLayerLinesAxisAligned(page, 'svg-layer-mpr-coronal'), {
|
||||
timeout: CROSSHAIRS_DOM_TIMEOUT_MS,
|
||||
})
|
||||
.toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -59,12 +59,25 @@ test('rectangle and freehand at identical coordinates should yield comparable ar
|
||||
// ANNOTATION_COMPLETED). Ending at the start point would enter interactive close-
|
||||
// preview mode and suppress the completion event; ending further away
|
||||
// would draw a diagonal that distorts the rectangular shape.
|
||||
//
|
||||
// The closing gap must be a fixed number of canvas pixels, not a normalized
|
||||
// fraction: cs3d's close-proximity threshold is in pixels, so a fractional gap
|
||||
// (0.01 × height) grew past the threshold on taller CI viewports and the
|
||||
// contour never auto-closed (intermittent "no cachedStats"). Derive it from the
|
||||
// viewport bbox so the gap is viewport-size-independent.
|
||||
const CLOSE_GAP_PX = 6;
|
||||
const viewportBox = await activeViewport.pane.boundingBox();
|
||||
if (!viewportBox) {
|
||||
throw new Error('Active viewport bounding box not found');
|
||||
}
|
||||
const closeY = 0.3 + CLOSE_GAP_PX / viewportBox.height;
|
||||
|
||||
const corners = [
|
||||
{ x: 0.3, y: 0.3 },
|
||||
{ x: 0.55, y: 0.3 },
|
||||
{ x: 0.55, y: 0.55 },
|
||||
{ x: 0.3, y: 0.55 },
|
||||
{ x: 0.3, y: 0.31 },
|
||||
{ x: 0.3, y: closeY },
|
||||
];
|
||||
const [topLeft, , bottomRight] = corners;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -168,14 +168,14 @@ test('checks if measurement item can be deleted under Measurements panel', async
|
||||
|
||||
// Open measurement panel, confirm 3 measurements
|
||||
await rightPanelPageObject.measurementsPanel.select();
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(3);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(3);
|
||||
|
||||
// Delete from measurement
|
||||
await rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0).actions.delete();
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
// Confirm one measurement is gone
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(2);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(2);
|
||||
|
||||
// Delete all measurements via main Measurement Panel delete button and untrack
|
||||
await rightPanelPageObject.measurementsPanel.panel.deleteAll();
|
||||
@@ -189,7 +189,7 @@ test('checks if measurement item can be deleted under Measurements panel', async
|
||||
|
||||
// Check dialog closed and measurements gone
|
||||
await expect(DOMOverlayPageObject.dialog.title).toBeHidden();
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(0);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(0);
|
||||
|
||||
const measurementsPanel = rightPanelPageObject.measurementsPanel.panel.locator;
|
||||
await expect(measurementsPanel).toContainText('No tracked measurements');
|
||||
@@ -219,7 +219,7 @@ test('checks if measurement item can be deleted through the context menu on the
|
||||
// Open measurement panel and confirm measurement is gone
|
||||
await rightPanelPageObject.measurementsPanel.select();
|
||||
await expect(activeViewport.nthAnnotation(0).locator).toBeHidden();
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(0);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('checks that undo after delete-all restores measurements as tracked', async ({
|
||||
@@ -243,13 +243,13 @@ test('checks that undo after delete-all restores measurements as tracked', async
|
||||
});
|
||||
|
||||
await rightPanelPageObject.measurementsPanel.select();
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(2);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(2);
|
||||
|
||||
await rightPanelPageObject.measurementsPanel.panel.deleteAll();
|
||||
await expect(DOMOverlayPageObject.dialog.title).toHaveText('Untrack Study');
|
||||
await DOMOverlayPageObject.dialog.confirmation.confirm.click();
|
||||
await expect(DOMOverlayPageObject.dialog.title).toBeHidden();
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(0);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(0);
|
||||
|
||||
await mainToolbarPageObject.undo.click();
|
||||
|
||||
@@ -326,14 +326,14 @@ test.describe('simplified tracking mode', () => {
|
||||
});
|
||||
|
||||
await rightPanelPageObject.measurementsPanel.select();
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(2);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(2);
|
||||
|
||||
await rightPanelPageObject.measurementsPanel.panel.deleteAll();
|
||||
await expect(DOMOverlayPageObject.dialog.title).toHaveText('Untrack Study');
|
||||
await DOMOverlayPageObject.dialog.confirmation.confirm.click();
|
||||
await expect(DOMOverlayPageObject.dialog.title).toBeHidden();
|
||||
|
||||
expect(await rightPanelPageObject.measurementsPanel.panel.getMeasurementCount()).toBe(0);
|
||||
await expect(rightPanelPageObject.measurementsPanel.panel.rows).toHaveCount(0);
|
||||
|
||||
await mainToolbarPageObject.undo.click();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
|
Before Width: | Height: | Size: 282 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 230 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 270 KiB |
|
Before Width: | Height: | Size: 250 KiB After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 271 KiB |
|
Before Width: | Height: | Size: 271 KiB After Width: | Height: | Size: 269 KiB |
|
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 267 KiB |
|
Before Width: | Height: | Size: 274 KiB After Width: | Height: | Size: 272 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 173 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 201 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 207 KiB After Width: | Height: | Size: 206 KiB |
|
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 748 B |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 182 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 163 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 219 KiB After Width: | Height: | Size: 218 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 197 KiB After Width: | Height: | Size: 196 KiB |
|
Before Width: | Height: | Size: 214 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 145 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 188 KiB After Width: | Height: | Size: 187 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 175 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 150 KiB After Width: | Height: | Size: 149 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 95 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 180 KiB After Width: | Height: | Size: 179 KiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 162 KiB After Width: | Height: | Size: 162 KiB |
|
Before Width: | Height: | Size: 256 KiB After Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 264 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 175 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 355 KiB After Width: | Height: | Size: 356 KiB |
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 359 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 159 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 369 KiB After Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 503 KiB After Width: | Height: | Size: 506 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 285 KiB |
|
Before Width: | Height: | Size: 549 KiB After Width: | Height: | Size: 546 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 190 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 175 KiB |