From afa4608c2181056ee27f1d2a8a10a83620adfb21 Mon Sep 17 00:00:00 2001 From: diattamo Date: Wed, 24 Jun 2026 05:32:59 -0400 Subject: [PATCH 1/6] feat(contour): add contour drawing tools and segment button to RightPanelPageObject --- tests/ContourSegmentationDrawingTools.spec.ts | 188 ++++++++++++++++++ tests/pages/RightPanelPageObject.ts | 43 ++++ 2 files changed, 231 insertions(+) create mode 100644 tests/ContourSegmentationDrawingTools.spec.ts diff --git a/tests/ContourSegmentationDrawingTools.spec.ts b/tests/ContourSegmentationDrawingTools.spec.ts new file mode 100644 index 00000000000..4e671769f92 --- /dev/null +++ b/tests/ContourSegmentationDrawingTools.spec.ts @@ -0,0 +1,188 @@ +import { expect, test, visitStudy, waitForViewportsRendered, getSvgAttribute } from './utils'; + +const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; + + +const FIRST_SLICE_OVERLAY= 'I:1 (1/47)'; + +// A closed loop in the centre of the viewport, clear of the hydrated RTSTRUCT +// contour so a freshly drawn contour renders as its own SVG . +const loop = [ + { x: 0.4, y: 0.4 }, + { x: 0.6, y: 0.4 }, + { x: 0.6, y: 0.6 }, + { x: 0.4, y: 0.6 }, + { x: 0.4, y: 0.4 }, +]; + +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject, viewportPageObject }) => { + const mode = 'segmentation'; + await visitStudy(page, studyInstanceUID, mode, 2000); + + await leftPanelPageObject.loadSeriesByModality('RTSTRUCT'); + await waitForViewportsRendered(page); + await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible(); + + // Hydrating the RTSTRUCT gives an active Contour segmentation with segments, + // which is what enables the contour drawing tools in the panel toolbox. + await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); + + // Hydration parks the viewport on the first slice; the drawing tests below + // rely on starting from slice 1. + const defaultViewport = await viewportPageObject.getById('default'); + await expect( + defaultViewport.overlayText.bottomRight.instanceNumber, + 'Expected the hydrated RTSTRUCT to open on slice 1 of 47' + ).toHaveText(FIRST_SLICE_OVERLAY); +}); + +test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ + page, + rightPanelPageObject, + viewportPageObject, +}) => { + const activeViewport = await viewportPageObject.active; + const defaultViewport = await viewportPageObject.getById('default'); + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + + const drawnIndex = await paths.count(); + await rightPanelPageObject.contourSegmentationPanel.tools.freehand.click(); + // Freehand auto-closes on mouse-up; the drag traces the loop. + await activeViewport.normalizedPathDragAt({ path: loop, config: { steps: 20, delay: 30 } }); + await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount( + drawnIndex + 1 + ); + + // Capture the drawn contour's geometry. + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: drawnIndex, + }); + expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); + + // Selecting another segment jumps the viewport to that segment's slice, + // navigating away from slice 1. + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); + await expect( + sliceIndicator, + 'Expected selecting another segment to navigate off slice 1' + ).not.toHaveText(FIRST_SLICE_OVERLAY); + + // Scroll back to slice 1 — the freehand contour must still render, unchanged. + await activeViewport.sliceNavigation.toFirstSlice(); + await waitForViewportsRendered(page); + await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: drawnIndex, + }); + expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( + drawnPathD + ); +}); + +test('should keep a freehand contour drawn into an added segment after switching segments and back', async ({ + page, + rightPanelPageObject, + viewportPageObject, +}) => { + const activeViewport = await viewportPageObject.active; + const defaultViewport = await viewportPageObject.getById('default'); + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + // Add a fresh segment and make it the active drawing target. It is empty, so + // selecting it keeps us on slice 1. + const initialCount = await panel.getSegmentCount(); + await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); + await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(initialCount + 1); + const addedSegment = panel.nthSegment(initialCount); + await addedSegment.click(); + await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( + FIRST_SLICE_OVERLAY + ); + + // Draw a freehand contour into the added segment on slice 1. + const drawnIndex = await paths.count(); + await rightPanelPageObject.contourSegmentationPanel.tools.freehand.click(); + await activeViewport.normalizedPathDragAt({ path: loop, config: { steps: 20, delay: 30 } }); + await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount( + drawnIndex + 1 + ); + + // Capture the drawn contour's geometry. + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: drawnIndex, + }); + expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); + + // Switch to another segment (jumps to its slice), then come back to the added segment. + await panel.nthSegment(0).click(); + await expect( + sliceIndicator, + 'Expected switching segments to navigate off slice 1' + ).not.toHaveText(FIRST_SLICE_OVERLAY); + + await addedSegment.click(); + await waitForViewportsRendered(page); + await expect( + sliceIndicator, + 'Expected returning to the added segment to land back on slice 1' + ).toHaveText(FIRST_SLICE_OVERLAY); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: drawnIndex, + }); + expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( + drawnPathD + ); +}); + +test('should draw a new contour with the Spline contour tool', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const activeViewport = await viewportPageObject.active; + const paths = (await viewportPageObject.getById('default')).svg('path'); + + await expect(paths, 'Expected the hydrated RTSTRUCT to render a single contour').toHaveCount(1); + + await rightPanelPageObject.contourSegmentationPanel.tools.spline.click(); + + // Click the vertices then re-click the first vertex to close the contour. + await activeViewport.normalizedClickAt(loop); + + await expect(paths, 'Expected a newly drawn spline contour path to appear').toHaveCount(2); +}); + +test('should draw a new contour with the Livewire contour tool', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const activeViewport = await viewportPageObject.active; + const paths = (await viewportPageObject.getById('default')).svg('path'); + + await expect(paths, 'Expected the hydrated RTSTRUCT to render a single contour').toHaveCount(1); + + await rightPanelPageObject.contourSegmentationPanel.tools.livewire.click(); + + await activeViewport.normalizedClickAt(loop); + + await expect(paths, 'Expected a newly drawn livewire contour path to appear').toHaveCount(2); +}); diff --git a/tests/pages/RightPanelPageObject.ts b/tests/pages/RightPanelPageObject.ts index 49153b23bb7..530bb803e8b 100644 --- a/tests/pages/RightPanelPageObject.ts +++ b/tests/pages/RightPanelPageObject.ts @@ -268,9 +268,21 @@ export class RightPanelPageObject { }; } + /** The "Add Segment" row button of the active segmentation in the visible panel */ + private get addSegmentButton() { + const button = this.page.getByRole('button', { name: 'Add Segment' }); + return { + button, + click: async () => { + await button.click(); + }, + }; + } + get contourSegmentationPanel() { const page = this.page; const addSegmentationButton = this.addSegmentationButton; + const addSegmentButton = this.addSegmentButton; const panel = this.getSegmentationPanel('Contour'); const menuButton = page.getByTestId('panelSegmentationWithToolsContour-btn'); const segmentationSelect = this.getSegmentationSelect('Contour'); @@ -278,6 +290,7 @@ export class RightPanelPageObject { return { addSegmentationButton, + addSegmentButton, menuButton, segmentsVisibilityToggle, panel, @@ -285,6 +298,36 @@ export class RightPanelPageObject { select: async () => { await menuButton.click(); }, + // Contour drawing/editing tools rendered in the panel toolbox. + tools: { + get freehand() { + const button = page.getByTestId('PlanarFreehandContourSegmentationTool-btn'); + return { + button, + click: async () => { + await button.click(); + }, + }; + }, + get spline() { + const button = page.getByTestId('SplineContourSegmentationTool-btn'); + return { + button, + click: async () => { + await button.click(); + }, + }; + }, + get livewire() { + const button = page.getByTestId('LivewireContourSegmentationTool-btn'); + return { + button, + click: async () => { + await button.click(); + }, + }; + }, + }, }; } get labelMapSegmentationPanel() { From 792b60df88cca56cae03f66a216d1fda77a8a153 Mon Sep 17 00:00:00 2001 From: diattamo Date: Mon, 29 Jun 2026 17:53:40 +0100 Subject: [PATCH 2/6] refactor(tests): streamline contour segmentation tests and remove unused addSegmentButton --- tests/ContourSegmentationDrawingTools.spec.ts | 268 +++++++++++++++--- tests/pages/RightPanelPageObject.ts | 11 - 2 files changed, 223 insertions(+), 56 deletions(-) diff --git a/tests/ContourSegmentationDrawingTools.spec.ts b/tests/ContourSegmentationDrawingTools.spec.ts index 4e671769f92..b1f814acece 100644 --- a/tests/ContourSegmentationDrawingTools.spec.ts +++ b/tests/ContourSegmentationDrawingTools.spec.ts @@ -5,9 +5,7 @@ const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; const FIRST_SLICE_OVERLAY= 'I:1 (1/47)'; -// A closed loop in the centre of the viewport, clear of the hydrated RTSTRUCT -// contour so a freshly drawn contour renders as its own SVG . -const loop = [ +const dragShape = [ { x: 0.4, y: 0.4 }, { x: 0.6, y: 0.4 }, { x: 0.6, y: 0.6 }, @@ -41,26 +39,24 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) rightPanelPageObject, viewportPageObject, }) => { - const activeViewport = await viewportPageObject.active; const defaultViewport = await viewportPageObject.getById('default'); const paths = defaultViewport.svg('path'); const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - const drawnIndex = await paths.count(); + // confirm starting state: slice 1, one contour path (outer box) + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); await rightPanelPageObject.contourSegmentationPanel.tools.freehand.click(); // Freehand auto-closes on mouse-up; the drag traces the loop. - await activeViewport.normalizedPathDragAt({ path: loop, config: { steps: 20, delay: 30 } }); - await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount( - drawnIndex + 1 - ); + await defaultViewport.normalizedPathDragAt({ path: dragShape }); + // Confirm that the freehand contour was added to the viewport on slice 1 + await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount(2); // Capture the drawn contour's geometry. const drawnPathD = await getSvgAttribute({ viewportPageObject, svgInnerElement: 'path', attributeName: 'd', - nth: drawnIndex, + nth: 1, }); expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); @@ -73,7 +69,7 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) ).not.toHaveText(FIRST_SLICE_OVERLAY); // Scroll back to slice 1 — the freehand contour must still render, unchanged. - await activeViewport.sliceNavigation.toFirstSlice(); + await defaultViewport.sliceNavigation.toFirstSlice(); await waitForViewportsRendered(page); await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); @@ -81,7 +77,7 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) viewportPageObject, svgInnerElement: 'path', attributeName: 'd', - nth: drawnIndex, + nth: 1, }); expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( @@ -94,49 +90,43 @@ test('should keep a freehand contour drawn into an added segment after switching rightPanelPageObject, viewportPageObject, }) => { - const activeViewport = await viewportPageObject.active; const defaultViewport = await viewportPageObject.getById('default'); const panel = rightPanelPageObject.contourSegmentationPanel.panel; const paths = defaultViewport.svg('path'); const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - // Add a fresh segment and make it the active drawing target. It is empty, so - // selecting it keeps us on slice 1. - const initialCount = await panel.getSegmentCount(); - await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); - await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(initialCount + 1); - const addedSegment = panel.nthSegment(initialCount); - await addedSegment.click(); + // confirm starting state: one contour path (outer box), 1/47 overlay and 4 panel segments + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( FIRST_SLICE_OVERLAY ); + await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); + + + // Add a fresh segment and make it the active drawing target. + await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); + await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); + const addedSegment = panel.nthSegment(4); // The newly added segment is at index 4. + await addedSegment.click(); // Draw a freehand contour into the added segment on slice 1. - const drawnIndex = await paths.count(); await rightPanelPageObject.contourSegmentationPanel.tools.freehand.click(); - await activeViewport.normalizedPathDragAt({ path: loop, config: { steps: 20, delay: 30 } }); - await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount( - drawnIndex + 1 - ); + await defaultViewport.normalizedPathDragAt({ path: dragShape }); + await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount(2); // Capture the drawn contour's geometry. const drawnPathD = await getSvgAttribute({ viewportPageObject, svgInnerElement: 'path', attributeName: 'd', - nth: drawnIndex, + nth: 1, }); expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); // Switch to another segment (jumps to its slice), then come back to the added segment. await panel.nthSegment(0).click(); - await expect( - sliceIndicator, - 'Expected switching segments to navigate off slice 1' - ).not.toHaveText(FIRST_SLICE_OVERLAY); await addedSegment.click(); - await waitForViewportsRendered(page); await expect( sliceIndicator, 'Expected returning to the added segment to land back on slice 1' @@ -146,7 +136,7 @@ test('should keep a freehand contour drawn into an added segment after switching viewportPageObject, svgInnerElement: 'path', attributeName: 'd', - nth: drawnIndex, + nth: 1, }); expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( @@ -154,35 +144,223 @@ test('should keep a freehand contour drawn into an added segment after switching ); }); -test('should draw a new contour with the Spline contour tool', async ({ +test('should keep a spline contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const defaultViewport = await viewportPageObject.getById('default'); + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + // confirm starting state: slice 1, one contour path (outer box) + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await rightPanelPageObject.contourSegmentationPanel.tools.spline.click(); + // Click the vertices then re-click the first vertex to close the contour. + await defaultViewport.normalizedClickAt(dragShape); + // Confirm that the spline contour was added to the viewport on slice 1 + await expect(paths, 'Expected the spline contour to be added on slice 1').toHaveCount(2); + + // Capture the drawn contour's geometry. + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); + + // Selecting another segment jumps the viewport to that segment's slice, + // navigating away from slice 1. + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); + await expect( + sliceIndicator, + 'Expected selecting another segment to navigate off slice 1' + ).not.toHaveText(FIRST_SLICE_OVERLAY); + + // Scroll back to slice 1 — the spline contour must still render, unchanged. + // The spline tool stays active after drawing, so wait on DOM state (overlay + + // re-rendered path) rather than viewport render status, which never settles. + await defaultViewport.sliceNavigation.toFirstSlice(); + await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); + await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( + drawnPathD + ); +}); + +test('should keep a spline contour drawn into an added segment after switching segments and back', async ({ + page, rightPanelPageObject, viewportPageObject, }) => { - const activeViewport = await viewportPageObject.active; - const paths = (await viewportPageObject.getById('default')).svg('path'); + const defaultViewport = await viewportPageObject.getById('default'); + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + // confirm starting state: one contour path (outer box), 1/47 overlay and 4 panel segments + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( + FIRST_SLICE_OVERLAY + ); + await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); - await expect(paths, 'Expected the hydrated RTSTRUCT to render a single contour').toHaveCount(1); + // Add a fresh segment and make it the active drawing target. + await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); + await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); + const addedSegment = panel.nthSegment(4); // The newly added segment is at index 4. + await addedSegment.click(); + // Draw a spline contour into the added segment on slice 1. await rightPanelPageObject.contourSegmentationPanel.tools.spline.click(); + await defaultViewport.normalizedClickAt(dragShape); + await expect(paths, 'Expected the spline contour to be added to the new segment').toHaveCount(2); + + // Capture the drawn contour's geometry. + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); + + // Switch to another segment (jumps to its slice), then come back to the added segment. + await panel.nthSegment(0).click(); + + await addedSegment.click(); + await expect( + sliceIndicator, + 'Expected returning to the added segment to land back on slice 1' + ).toHaveText(FIRST_SLICE_OVERLAY); + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( + drawnPathD + ); +}); + +test('should keep a livewire contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const defaultViewport = await viewportPageObject.getById('default'); + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + // confirm starting state: slice 1, one contour path (outer box) + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await rightPanelPageObject.contourSegmentationPanel.tools.livewire.click(); // Click the vertices then re-click the first vertex to close the contour. - await activeViewport.normalizedClickAt(loop); + await defaultViewport.normalizedClickAt(dragShape); + // Confirm that the livewire contour was added to the viewport on slice 1 + await expect(paths, 'Expected the livewire contour to be added on slice 1').toHaveCount(2); + + // Capture the drawn contour's geometry. + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn livewire contour to render an SVG path').not.toBeNull(); - await expect(paths, 'Expected a newly drawn spline contour path to appear').toHaveCount(2); + // Selecting another segment jumps the viewport to that segment's slice, + // navigating away from slice 1. + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); + await expect( + sliceIndicator, + 'Expected selecting another segment to navigate off slice 1' + ).not.toHaveText(FIRST_SLICE_OVERLAY); + + // Scroll back to slice 1 — the livewire contour must still render, unchanged. + // The livewire tool stays active after drawing, so wait on DOM state (overlay + + // re-rendered path) rather than viewport render status, which never settles. + await defaultViewport.sliceNavigation.toFirstSlice(); + await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); + await expect(paths, 'Expected the livewire contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the livewire contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted livewire contour to match what was drawn').toBe( + drawnPathD + ); }); -test('should draw a new contour with the Livewire contour tool', async ({ +test('should keep a livewire contour drawn into an added segment after switching segments and back', async ({ + page, rightPanelPageObject, viewportPageObject, }) => { - const activeViewport = await viewportPageObject.active; - const paths = (await viewportPageObject.getById('default')).svg('path'); + const defaultViewport = await viewportPageObject.getById('default'); + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - await expect(paths, 'Expected the hydrated RTSTRUCT to render a single contour').toHaveCount(1); + // confirm starting state: one contour path (outer box), 1/47 overlay and 4 panel segments + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( + FIRST_SLICE_OVERLAY + ); + await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); + + + // Add a fresh segment and make it the active drawing target. + await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); + await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); + const addedSegment = panel.nthSegment(4); // The newly added segment is at index 4. + await addedSegment.click(); + // Draw a livewire contour into the added segment on slice 1. await rightPanelPageObject.contourSegmentationPanel.tools.livewire.click(); + await defaultViewport.normalizedClickAt(dragShape); + await expect(paths, 'Expected the livewire contour to be added to the new segment').toHaveCount(2); - await activeViewport.normalizedClickAt(loop); + // Capture the drawn contour's geometry. + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn livewire contour to render an SVG path').not.toBeNull(); + + // Switch to another segment (jumps to its slice), then come back to the added segment. + await panel.nthSegment(0).click(); + + await addedSegment.click(); + await expect( + sliceIndicator, + 'Expected returning to the added segment to land back on slice 1' + ).toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected a newly drawn livewire contour path to appear').toHaveCount(2); + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the livewire contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted livewire contour to match what was drawn').toBe( + drawnPathD + ); }); diff --git a/tests/pages/RightPanelPageObject.ts b/tests/pages/RightPanelPageObject.ts index 08093fbd1af..cc8c5e5257d 100644 --- a/tests/pages/RightPanelPageObject.ts +++ b/tests/pages/RightPanelPageObject.ts @@ -325,17 +325,6 @@ export class RightPanelPageObject { }; } - /** The "Add Segment" row button of the active segmentation in the visible panel */ - private get addSegmentButton() { - const button = this.page.getByRole('button', { name: 'Add Segment' }); - return { - button, - click: async () => { - await button.click(); - }, - }; - } - get contourSegmentationPanel() { const page = this.page; const addSegmentationButton = this.addSegmentationButton; From d85cd8c10820a78f1e79678a6ca01c9531169dd3 Mon Sep 17 00:00:00 2001 From: diattamo Date: Mon, 29 Jun 2026 23:38:03 +0100 Subject: [PATCH 3/6] refactor(tests): refactor ContourSegmentationDrawingTools.spec.ts file to split by tools --- tests/ContourSegmentationDrawingTools.spec.ts | 366 ------------------ 1 file changed, 366 deletions(-) delete mode 100644 tests/ContourSegmentationDrawingTools.spec.ts diff --git a/tests/ContourSegmentationDrawingTools.spec.ts b/tests/ContourSegmentationDrawingTools.spec.ts deleted file mode 100644 index b1f814acece..00000000000 --- a/tests/ContourSegmentationDrawingTools.spec.ts +++ /dev/null @@ -1,366 +0,0 @@ -import { expect, test, visitStudy, waitForViewportsRendered, getSvgAttribute } from './utils'; - -const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; - - -const FIRST_SLICE_OVERLAY= 'I:1 (1/47)'; - -const dragShape = [ - { x: 0.4, y: 0.4 }, - { x: 0.6, y: 0.4 }, - { x: 0.6, y: 0.6 }, - { x: 0.4, y: 0.6 }, - { x: 0.4, y: 0.4 }, -]; - -test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject, viewportPageObject }) => { - const mode = 'segmentation'; - await visitStudy(page, studyInstanceUID, mode, 2000); - - await leftPanelPageObject.loadSeriesByModality('RTSTRUCT'); - await waitForViewportsRendered(page); - await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible(); - - // Hydrating the RTSTRUCT gives an active Contour segmentation with segments, - // which is what enables the contour drawing tools in the panel toolbox. - await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); - - // Hydration parks the viewport on the first slice; the drawing tests below - // rely on starting from slice 1. - const defaultViewport = await viewportPageObject.getById('default'); - await expect( - defaultViewport.overlayText.bottomRight.instanceNumber, - 'Expected the hydrated RTSTRUCT to open on slice 1 of 47' - ).toHaveText(FIRST_SLICE_OVERLAY); -}); - -test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ - page, - rightPanelPageObject, - viewportPageObject, -}) => { - const defaultViewport = await viewportPageObject.getById('default'); - const paths = defaultViewport.svg('path'); - const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - // confirm starting state: slice 1, one contour path (outer box) - await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); - await rightPanelPageObject.contourSegmentationPanel.tools.freehand.click(); - // Freehand auto-closes on mouse-up; the drag traces the loop. - await defaultViewport.normalizedPathDragAt({ path: dragShape }); - // Confirm that the freehand contour was added to the viewport on slice 1 - await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount(2); - - // Capture the drawn contour's geometry. - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); - - // Selecting another segment jumps the viewport to that segment's slice, - // navigating away from slice 1. - await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); - await expect( - sliceIndicator, - 'Expected selecting another segment to navigate off slice 1' - ).not.toHaveText(FIRST_SLICE_OVERLAY); - - // Scroll back to slice 1 — the freehand contour must still render, unchanged. - await defaultViewport.sliceNavigation.toFirstSlice(); - await waitForViewportsRendered(page); - await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( - drawnPathD - ); -}); - -test('should keep a freehand contour drawn into an added segment after switching segments and back', async ({ - page, - rightPanelPageObject, - viewportPageObject, -}) => { - const defaultViewport = await viewportPageObject.getById('default'); - const panel = rightPanelPageObject.contourSegmentationPanel.panel; - const paths = defaultViewport.svg('path'); - const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - // confirm starting state: one contour path (outer box), 1/47 overlay and 4 panel segments - await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); - await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( - FIRST_SLICE_OVERLAY - ); - await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); - - - // Add a fresh segment and make it the active drawing target. - await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); - await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); - const addedSegment = panel.nthSegment(4); // The newly added segment is at index 4. - await addedSegment.click(); - - // Draw a freehand contour into the added segment on slice 1. - await rightPanelPageObject.contourSegmentationPanel.tools.freehand.click(); - await defaultViewport.normalizedPathDragAt({ path: dragShape }); - await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount(2); - - // Capture the drawn contour's geometry. - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); - - // Switch to another segment (jumps to its slice), then come back to the added segment. - await panel.nthSegment(0).click(); - - await addedSegment.click(); - await expect( - sliceIndicator, - 'Expected returning to the added segment to land back on slice 1' - ).toHaveText(FIRST_SLICE_OVERLAY); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( - drawnPathD - ); -}); - -test('should keep a spline contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ - rightPanelPageObject, - viewportPageObject, -}) => { - const defaultViewport = await viewportPageObject.getById('default'); - const paths = defaultViewport.svg('path'); - const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - // confirm starting state: slice 1, one contour path (outer box) - await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); - await rightPanelPageObject.contourSegmentationPanel.tools.spline.click(); - // Click the vertices then re-click the first vertex to close the contour. - await defaultViewport.normalizedClickAt(dragShape); - // Confirm that the spline contour was added to the viewport on slice 1 - await expect(paths, 'Expected the spline contour to be added on slice 1').toHaveCount(2); - - // Capture the drawn contour's geometry. - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); - - // Selecting another segment jumps the viewport to that segment's slice, - // navigating away from slice 1. - await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); - await expect( - sliceIndicator, - 'Expected selecting another segment to navigate off slice 1' - ).not.toHaveText(FIRST_SLICE_OVERLAY); - - // Scroll back to slice 1 — the spline contour must still render, unchanged. - // The spline tool stays active after drawing, so wait on DOM state (overlay + - // re-rendered path) rather than viewport render status, which never settles. - await defaultViewport.sliceNavigation.toFirstSlice(); - await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( - drawnPathD - ); -}); - -test('should keep a spline contour drawn into an added segment after switching segments and back', async ({ - page, - rightPanelPageObject, - viewportPageObject, -}) => { - const defaultViewport = await viewportPageObject.getById('default'); - const panel = rightPanelPageObject.contourSegmentationPanel.panel; - const paths = defaultViewport.svg('path'); - const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - // confirm starting state: one contour path (outer box), 1/47 overlay and 4 panel segments - await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); - await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( - FIRST_SLICE_OVERLAY - ); - await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); - - // Add a fresh segment and make it the active drawing target. - await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); - await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); - const addedSegment = panel.nthSegment(4); // The newly added segment is at index 4. - await addedSegment.click(); - - // Draw a spline contour into the added segment on slice 1. - await rightPanelPageObject.contourSegmentationPanel.tools.spline.click(); - await defaultViewport.normalizedClickAt(dragShape); - await expect(paths, 'Expected the spline contour to be added to the new segment').toHaveCount(2); - - // Capture the drawn contour's geometry. - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); - - // Switch to another segment (jumps to its slice), then come back to the added segment. - await panel.nthSegment(0).click(); - - await addedSegment.click(); - await expect( - sliceIndicator, - 'Expected returning to the added segment to land back on slice 1' - ).toHaveText(FIRST_SLICE_OVERLAY); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( - drawnPathD - ); -}); - -test('should keep a livewire contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ - rightPanelPageObject, - viewportPageObject, -}) => { - const defaultViewport = await viewportPageObject.getById('default'); - const paths = defaultViewport.svg('path'); - const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - // confirm starting state: slice 1, one contour path (outer box) - await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); - await rightPanelPageObject.contourSegmentationPanel.tools.livewire.click(); - // Click the vertices then re-click the first vertex to close the contour. - await defaultViewport.normalizedClickAt(dragShape); - // Confirm that the livewire contour was added to the viewport on slice 1 - await expect(paths, 'Expected the livewire contour to be added on slice 1').toHaveCount(2); - - // Capture the drawn contour's geometry. - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn livewire contour to render an SVG path').not.toBeNull(); - - // Selecting another segment jumps the viewport to that segment's slice, - // navigating away from slice 1. - await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); - await expect( - sliceIndicator, - 'Expected selecting another segment to navigate off slice 1' - ).not.toHaveText(FIRST_SLICE_OVERLAY); - - // Scroll back to slice 1 — the livewire contour must still render, unchanged. - // The livewire tool stays active after drawing, so wait on DOM state (overlay + - // re-rendered path) rather than viewport render status, which never settles. - await defaultViewport.sliceNavigation.toFirstSlice(); - await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the livewire contour to re-render on slice 1').toHaveCount(2); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the livewire contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted livewire contour to match what was drawn').toBe( - drawnPathD - ); -}); - -test('should keep a livewire contour drawn into an added segment after switching segments and back', async ({ - page, - rightPanelPageObject, - viewportPageObject, -}) => { - const defaultViewport = await viewportPageObject.getById('default'); - const panel = rightPanelPageObject.contourSegmentationPanel.panel; - const paths = defaultViewport.svg('path'); - const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; - - // confirm starting state: one contour path (outer box), 1/47 overlay and 4 panel segments - await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); - await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( - FIRST_SLICE_OVERLAY - ); - await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); - - - // Add a fresh segment and make it the active drawing target. - await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); - await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); - const addedSegment = panel.nthSegment(4); // The newly added segment is at index 4. - await addedSegment.click(); - - // Draw a livewire contour into the added segment on slice 1. - await rightPanelPageObject.contourSegmentationPanel.tools.livewire.click(); - await defaultViewport.normalizedClickAt(dragShape); - await expect(paths, 'Expected the livewire contour to be added to the new segment').toHaveCount(2); - - // Capture the drawn contour's geometry. - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn livewire contour to render an SVG path').not.toBeNull(); - - // Switch to another segment (jumps to its slice), then come back to the added segment. - await panel.nthSegment(0).click(); - - await addedSegment.click(); - await expect( - sliceIndicator, - 'Expected returning to the added segment to land back on slice 1' - ).toHaveText(FIRST_SLICE_OVERLAY); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the livewire contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted livewire contour to match what was drawn').toBe( - drawnPathD - ); -}); From 3cc996a41a96dabfa2f6c38357f9cf0b6f566635 Mon Sep 17 00:00:00 2001 From: diattamo Date: Tue, 30 Jun 2026 00:05:44 +0100 Subject: [PATCH 4/6] test(contour): add Freehand and Spline contour drawing tool E2E tests --- tests/FreehandContourSegmentation.spec.ts | 124 ++++++++++++++++++++++ tests/SplineContourSegmentation.spec.ts | 124 ++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 tests/FreehandContourSegmentation.spec.ts create mode 100644 tests/SplineContourSegmentation.spec.ts diff --git a/tests/FreehandContourSegmentation.spec.ts b/tests/FreehandContourSegmentation.spec.ts new file mode 100644 index 00000000000..0725b28369d --- /dev/null +++ b/tests/FreehandContourSegmentation.spec.ts @@ -0,0 +1,124 @@ +import { expect, test, visitStudyAndHydrate, getSvgAttribute } from './utils'; + +const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; + +const FIRST_SLICE_OVERLAY = 'I:1 (1/47)'; + +const dragShape = [ + { x: 0.4, y: 0.4 }, + { x: 0.6, y: 0.4 }, + { x: 0.6, y: 0.6 }, + { x: 0.4, y: 0.6 }, + { x: 0.4, y: 0.4 }, +]; + +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject, viewportPageObject }) => { + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); + + const defaultViewport = await viewportPageObject.getById('default'); + await expect( + defaultViewport.overlayText.bottomRight.instanceNumber, + 'Expected the hydrated RTSTRUCT to open on slice 1 of 47' + ).toHaveText(FIRST_SLICE_OVERLAY); +}); + +test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const defaultViewport = await viewportPageObject.getById('default'); + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await rightPanelPageObject.contourSegmentationPanel.tools.freehandContour.click(); + await defaultViewport.normalizedPathDragAt({ path: dragShape }); + await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount(2); + + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); + + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); + await expect( + sliceIndicator, + 'Expected selecting another segment to navigate off slice 1' + ).not.toHaveText(FIRST_SLICE_OVERLAY); + + await defaultViewport.sliceNavigation.toFirstSlice(); + await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); + await expect(paths, 'Expected the freehand contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( + drawnPathD + ); +}); + +test('should keep a freehand contour drawn into an added segment after switching segments and back', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const defaultViewport = await viewportPageObject.getById('default'); + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( + FIRST_SLICE_OVERLAY + ); + await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); + + await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); + await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); + const addedSegment = panel.nthSegment(4); + await addedSegment.click(); + + await rightPanelPageObject.contourSegmentationPanel.tools.freehandContour.click(); + await defaultViewport.normalizedPathDragAt({ path: dragShape }); + await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount(2); + + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); + + await panel.nthSegment(0).click(); + await addedSegment.click(); + await expect( + sliceIndicator, + 'Expected returning to the added segment to land back on slice 1' + ).toHaveText(FIRST_SLICE_OVERLAY); + await expect(paths, 'Expected the freehand contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( + drawnPathD + ); +}); diff --git a/tests/SplineContourSegmentation.spec.ts b/tests/SplineContourSegmentation.spec.ts new file mode 100644 index 00000000000..1a55718811d --- /dev/null +++ b/tests/SplineContourSegmentation.spec.ts @@ -0,0 +1,124 @@ +import { expect, test, visitStudyAndHydrate, getSvgAttribute } from './utils'; + +const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; + +const FIRST_SLICE_OVERLAY = 'I:1 (1/47)'; + +const dragShape = [ + { x: 0.4, y: 0.4 }, + { x: 0.6, y: 0.4 }, + { x: 0.6, y: 0.6 }, + { x: 0.4, y: 0.6 }, + { x: 0.4, y: 0.4 }, +]; + +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject, viewportPageObject }) => { + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); + + const defaultViewport = await viewportPageObject.getById('default'); + await expect( + defaultViewport.overlayText.bottomRight.instanceNumber, + 'Expected the hydrated RTSTRUCT to open on slice 1 of 47' + ).toHaveText(FIRST_SLICE_OVERLAY); +}); + +test('should keep a spline contour drawn on slice 1 (no segment pre-selected) after navigating away and back', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const defaultViewport = await viewportPageObject.getById('default'); + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await rightPanelPageObject.contourSegmentationPanel.tools.splineContour.click(); + await defaultViewport.normalizedClickAt(dragShape); + await expect(paths, 'Expected the spline contour to be added on slice 1').toHaveCount(2); + + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); + + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); + await expect( + sliceIndicator, + 'Expected selecting another segment to navigate off slice 1' + ).not.toHaveText(FIRST_SLICE_OVERLAY); + + await defaultViewport.sliceNavigation.toFirstSlice(); + await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); + await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( + drawnPathD + ); +}); + +test('should keep a spline contour drawn into an added segment after switching segments and back', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const defaultViewport = await viewportPageObject.getById('default'); + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const paths = defaultViewport.svg('path'); + const sliceIndicator = defaultViewport.overlayText.bottomRight.instanceNumber; + + await expect(paths, 'Expected the starting number of paths to be 1').toHaveCount(1); + await expect(sliceIndicator, 'Expected the empty added segment to stay on slice 1').toHaveText( + FIRST_SLICE_OVERLAY + ); + await expect(panel.rows, 'Expected 4 segments to start').toHaveCount(4); + + await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); + await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); + const addedSegment = panel.nthSegment(4); // newly added segment, index 4 + await addedSegment.click(); + + await rightPanelPageObject.contourSegmentationPanel.tools.splineContour.click(); + await defaultViewport.normalizedClickAt(dragShape); + await expect(paths, 'Expected the spline contour to be added to the new segment').toHaveCount(2); + + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); + + await panel.nthSegment(0).click(); + await addedSegment.click(); + await expect( + sliceIndicator, + 'Expected returning to the added segment to land back on slice 1' + ).toHaveText(FIRST_SLICE_OVERLAY); + await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( + drawnPathD + ); +}); From 9dca7074110a45420e8f8fcb44013171104ce7c3 Mon Sep 17 00:00:00 2001 From: diattamo Date: Tue, 30 Jun 2026 02:00:43 +0100 Subject: [PATCH 5/6] test(contour): use path-count persistence, with exact-geometry check on spline added-segment --- tests/FreehandContourSegmentation.spec.ts | 47 +++-------------------- tests/SplineContourSegmentation.spec.ts | 32 ++++----------- 2 files changed, 14 insertions(+), 65 deletions(-) diff --git a/tests/FreehandContourSegmentation.spec.ts b/tests/FreehandContourSegmentation.spec.ts index 0725b28369d..2dc0926c0a5 100644 --- a/tests/FreehandContourSegmentation.spec.ts +++ b/tests/FreehandContourSegmentation.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, visitStudyAndHydrate, getSvgAttribute } from './utils'; +import { expect, test, visitStudyAndHydrate } from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; @@ -41,14 +41,6 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) await defaultViewport.normalizedPathDragAt({ path: dragShape }); await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount(2); - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); - await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); await expect( sliceIndicator, @@ -57,18 +49,7 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) await defaultViewport.sliceNavigation.toFirstSlice(); await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the freehand contour to re-render on slice 1').toHaveCount(2); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( - drawnPathD - ); + await expect(paths, 'Expected the freehand contour to persist on slice 1').toHaveCount(2); }); test('should keep a freehand contour drawn into an added segment after switching segments and back', async ({ @@ -95,30 +76,14 @@ test('should keep a freehand contour drawn into an added segment after switching await defaultViewport.normalizedPathDragAt({ path: dragShape }); await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount(2); - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); - await panel.nthSegment(0).click(); await addedSegment.click(); await expect( sliceIndicator, 'Expected returning to the added segment to land back on slice 1' ).toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the freehand contour to re-render on slice 1').toHaveCount(2); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( - drawnPathD - ); + await expect( + paths, + 'Expected the freehand contour to persist after switching segments' + ).toHaveCount(2); }); diff --git a/tests/SplineContourSegmentation.spec.ts b/tests/SplineContourSegmentation.spec.ts index 1a55718811d..01250885c5d 100644 --- a/tests/SplineContourSegmentation.spec.ts +++ b/tests/SplineContourSegmentation.spec.ts @@ -1,4 +1,5 @@ -import { expect, test, visitStudyAndHydrate, getSvgAttribute } from './utils'; +import { expect, getSvgAttribute, test, visitStudyAndHydrate } from './utils'; + const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; @@ -41,14 +42,6 @@ test('should keep a spline contour drawn on slice 1 (no segment pre-selected) af await defaultViewport.normalizedClickAt(dragShape); await expect(paths, 'Expected the spline contour to be added on slice 1').toHaveCount(2); - const drawnPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); - await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); await expect( sliceIndicator, @@ -57,18 +50,7 @@ test('should keep a spline contour drawn on slice 1 (no segment pre-selected) af await defaultViewport.sliceNavigation.toFirstSlice(); await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); - - const persistedPathD = await getSvgAttribute({ - viewportPageObject, - svgInnerElement: 'path', - attributeName: 'd', - nth: 1, - }); - expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); - expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( - drawnPathD - ); + await expect(paths, 'Expected the spline contour to persist on slice 1').toHaveCount(2); }); test('should keep a spline contour drawn into an added segment after switching segments and back', async ({ @@ -88,7 +70,7 @@ test('should keep a spline contour drawn into an added segment after switching s await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); - const addedSegment = panel.nthSegment(4); // newly added segment, index 4 + const addedSegment = panel.nthSegment(4); await addedSegment.click(); await rightPanelPageObject.contourSegmentationPanel.tools.splineContour.click(); @@ -109,7 +91,10 @@ test('should keep a spline contour drawn into an added segment after switching s sliceIndicator, 'Expected returning to the added segment to land back on slice 1' ).toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); + await expect( + paths, + 'Expected the spline contour to persist after switching segments' + ).toHaveCount(2); const persistedPathD = await getSvgAttribute({ viewportPageObject, @@ -117,7 +102,6 @@ test('should keep a spline contour drawn into an added segment after switching s attributeName: 'd', nth: 1, }); - expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( drawnPathD ); From 4fe0249296db0a1bf9c1f01578572fa950658761 Mon Sep 17 00:00:00 2001 From: diattamo Date: Tue, 30 Jun 2026 02:27:44 +0100 Subject: [PATCH 6/6] test(contour): restore exact path-geometry persistence assertions --- tests/FreehandContourSegmentation.spec.ts | 47 ++++++++++++++++++++--- tests/SplineContourSegmentation.spec.ts | 32 +++++++++++---- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/tests/FreehandContourSegmentation.spec.ts b/tests/FreehandContourSegmentation.spec.ts index 2dc0926c0a5..0725b28369d 100644 --- a/tests/FreehandContourSegmentation.spec.ts +++ b/tests/FreehandContourSegmentation.spec.ts @@ -1,4 +1,4 @@ -import { expect, test, visitStudyAndHydrate } from './utils'; +import { expect, test, visitStudyAndHydrate, getSvgAttribute } from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; @@ -41,6 +41,14 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) await defaultViewport.normalizedPathDragAt({ path: dragShape }); await expect(paths, 'Expected the freehand contour to be added on slice 1').toHaveCount(2); + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); await expect( sliceIndicator, @@ -49,7 +57,18 @@ test('should keep a freehand contour drawn on slice 1 (no segment pre-selected) await defaultViewport.sliceNavigation.toFirstSlice(); await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the freehand contour to persist on slice 1').toHaveCount(2); + await expect(paths, 'Expected the freehand contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( + drawnPathD + ); }); test('should keep a freehand contour drawn into an added segment after switching segments and back', async ({ @@ -76,14 +95,30 @@ test('should keep a freehand contour drawn into an added segment after switching await defaultViewport.normalizedPathDragAt({ path: dragShape }); await expect(paths, 'Expected the freehand contour to be added to the new segment').toHaveCount(2); + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn freehand contour to render an SVG path').not.toBeNull(); + await panel.nthSegment(0).click(); await addedSegment.click(); await expect( sliceIndicator, 'Expected returning to the added segment to land back on slice 1' ).toHaveText(FIRST_SLICE_OVERLAY); - await expect( - paths, - 'Expected the freehand contour to persist after switching segments' - ).toHaveCount(2); + await expect(paths, 'Expected the freehand contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the freehand contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted freehand contour to match what was drawn').toBe( + drawnPathD + ); }); diff --git a/tests/SplineContourSegmentation.spec.ts b/tests/SplineContourSegmentation.spec.ts index 01250885c5d..1a55718811d 100644 --- a/tests/SplineContourSegmentation.spec.ts +++ b/tests/SplineContourSegmentation.spec.ts @@ -1,5 +1,4 @@ -import { expect, getSvgAttribute, test, visitStudyAndHydrate } from './utils'; - +import { expect, test, visitStudyAndHydrate, getSvgAttribute } from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; @@ -42,6 +41,14 @@ test('should keep a spline contour drawn on slice 1 (no segment pre-selected) af await defaultViewport.normalizedClickAt(dragShape); await expect(paths, 'Expected the spline contour to be added on slice 1').toHaveCount(2); + const drawnPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(drawnPathD, 'Expected the drawn spline contour to render an SVG path').not.toBeNull(); + await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(1).click(); await expect( sliceIndicator, @@ -50,7 +57,18 @@ test('should keep a spline contour drawn on slice 1 (no segment pre-selected) af await defaultViewport.sliceNavigation.toFirstSlice(); await expect(sliceIndicator, 'Expected to scroll back to slice 1').toHaveText(FIRST_SLICE_OVERLAY); - await expect(paths, 'Expected the spline contour to persist on slice 1').toHaveCount(2); + await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); + + const persistedPathD = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + nth: 1, + }); + expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); + expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( + drawnPathD + ); }); test('should keep a spline contour drawn into an added segment after switching segments and back', async ({ @@ -70,7 +88,7 @@ test('should keep a spline contour drawn into an added segment after switching s await rightPanelPageObject.contourSegmentationPanel.addSegmentButton.click(); await expect(panel.rows, 'Expected a new segment row to be added').toHaveCount(5); - const addedSegment = panel.nthSegment(4); + const addedSegment = panel.nthSegment(4); // newly added segment, index 4 await addedSegment.click(); await rightPanelPageObject.contourSegmentationPanel.tools.splineContour.click(); @@ -91,10 +109,7 @@ test('should keep a spline contour drawn into an added segment after switching s sliceIndicator, 'Expected returning to the added segment to land back on slice 1' ).toHaveText(FIRST_SLICE_OVERLAY); - await expect( - paths, - 'Expected the spline contour to persist after switching segments' - ).toHaveCount(2); + await expect(paths, 'Expected the spline contour to re-render on slice 1').toHaveCount(2); const persistedPathD = await getSvgAttribute({ viewportPageObject, @@ -102,6 +117,7 @@ test('should keep a spline contour drawn into an added segment after switching s attributeName: 'd', nth: 1, }); + expect(persistedPathD, 'Expected the spline contour to still render on slice 1').not.toBeNull(); expect(persistedPathD, 'Expected the persisted spline contour to match what was drawn').toBe( drawnPathD );