From b24f0397356e887f17820fb5e951aaea857c2dfc Mon Sep 17 00:00:00 2001 From: diattamo Date: Tue, 23 Jun 2026 01:52:35 -0400 Subject: [PATCH] test(contour): Add the ContourSegmentToggleLock.spec.ts test file to test contour locking (#6072) --- tests/ContourSegNavigation.spec.ts | 21 ++- tests/ContourSegmentDuplicate.spec.ts | 36 ++--- tests/ContourSegmentRename.spec.ts | 20 ++- tests/ContourSegmentToggleLock.spec.ts | 130 +++++++++++++++++++ tests/ContourSegmentToggleVisibility.spec.ts | 20 ++- tests/pages/RightPanelPageObject.ts | 15 ++- tests/utils/assertions.ts | 10 +- tests/utils/contourShowOnlyNthSegment.ts | 16 +++ tests/utils/index.ts | 9 +- tests/utils/visitStudyAndHydrate.ts | 31 +++++ 10 files changed, 253 insertions(+), 55 deletions(-) create mode 100644 tests/ContourSegmentToggleLock.spec.ts create mode 100644 tests/utils/contourShowOnlyNthSegment.ts create mode 100644 tests/utils/visitStudyAndHydrate.ts diff --git a/tests/ContourSegNavigation.spec.ts b/tests/ContourSegNavigation.spec.ts index 7c357d308..07fd226c9 100644 --- a/tests/ContourSegNavigation.spec.ts +++ b/tests/ContourSegNavigation.spec.ts @@ -1,7 +1,7 @@ import { expect, test, - visitStudy, + visitStudyAndHydrate, getSvgAttribute, navigateWithViewportArrow, waitForViewportsRendered, @@ -10,17 +10,14 @@ import { expectRowSelected } from './utils/assertions'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; -test.beforeEach(async ({ - page, - leftPanelPageObject, - DOMOverlayPageObject, - rightPanelPageObject -}) => { - const mode = 'segmentation'; - await visitStudy(page, studyInstanceUID, mode, 2000); - await leftPanelPageObject.loadSeriesByModality('RTSTRUCT'); - await page.waitForTimeout(5000); - await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject, rightPanelPageObject }) => { + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); // Click segment 0 in the right panel to establish a known starting position await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(0).click(); await page.waitForTimeout(5000); diff --git a/tests/ContourSegmentDuplicate.spec.ts b/tests/ContourSegmentDuplicate.spec.ts index 31930f22d..d32e98602 100644 --- a/tests/ContourSegmentDuplicate.spec.ts +++ b/tests/ContourSegmentDuplicate.spec.ts @@ -1,18 +1,23 @@ -import { expect, test, visitStudy, waitForViewportsRendered, getSvgAttribute } from './utils'; +import { + contourShowOnlyNthSegment, + expect, + getSvgAttribute, + test, + visitStudyAndHydrate, +} from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; const defaultSegment0Name = 'Threshold'; const defaultSegment1Name = 'Big Sphere'; test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject }) => { - const mode = 'segmentation'; - await visitStudy(page, studyInstanceUID, mode, 2000); - - await leftPanelPageObject.loadSeriesByModality('RTSTRUCT'); - await waitForViewportsRendered(page); - await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible(); - - await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); }); test('should duplicate a contour segment and add a new row to the panel', async ({ @@ -79,10 +84,9 @@ test('should render the duplicated contour on the viewport', async ({ const panel = rightPanelPageObject.contourSegmentationPanel.panel; // Hide everything, to be able to grab only the SVG path of the segment to duplicate - await rightPanelPageObject.contourSegmentationPanel.segmentsVisibilityToggle.click(); - const segment0 = panel.nthSegment(0); - await segment0.toggleVisibility(); - await segment0.click(); + const segment0 = await contourShowOnlyNthSegment({ + segmentationPanel: rightPanelPageObject.contourSegmentationPanel, + }); const sourceSvgPath = await getSvgAttribute({ viewportPageObject, @@ -175,9 +179,9 @@ test('should navigate to the correct instance number when a duplicated contour s ).toHaveText('I:46 (46/47)'); //verify the svg paths are the same for the original and duplicated segments - await rightPanelPageObject.contourSegmentationPanel.segmentsVisibilityToggle.click(); - await originalSegment.toggleVisibility(); - await originalSegment.click(); + await contourShowOnlyNthSegment({ + segmentationPanel: rightPanelPageObject.contourSegmentationPanel, + }); const originalSegmentSvgPath = await getSvgAttribute({ viewportPageObject, svgInnerElement: 'path', diff --git a/tests/ContourSegmentRename.spec.ts b/tests/ContourSegmentRename.spec.ts index 44dc664ef..875e48ba9 100644 --- a/tests/ContourSegmentRename.spec.ts +++ b/tests/ContourSegmentRename.spec.ts @@ -1,19 +1,17 @@ -import { expect, test, visitStudy } from './utils'; +import { expect, test, visitStudyAndHydrate } from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; const defautSegment0Name = 'Threshold'; const defautSegment1Name = 'Big Sphere'; -test.beforeEach(async ({ - page, - leftPanelPageObject, - DOMOverlayPageObject -}) => { - const mode = 'segmentation'; - await visitStudy(page, studyInstanceUID, mode, 2000); - await leftPanelPageObject.loadSeriesByModality('RTSTRUCT'); - await page.waitForTimeout(5000); - await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject }) => { + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); }); test('should rename contour segments', async ({ diff --git a/tests/ContourSegmentToggleLock.spec.ts b/tests/ContourSegmentToggleLock.spec.ts new file mode 100644 index 000000000..57a06a20b --- /dev/null +++ b/tests/ContourSegmentToggleLock.spec.ts @@ -0,0 +1,130 @@ +import { + contourShowOnlyNthSegment, + expect, + getSvgAttribute, + simulateNormalizedDragOnElement, + test, + visitStudyAndHydrate, +} from './utils'; +import { expectRowLocked, expectRowUnlocked } from './utils/assertions'; + +const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; +const defaultSegment0Name = 'Threshold'; + +test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject }) => { + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); +}); + +test('should show the lock indicator and flip the menu label between Lock and Unlock', async ({ + rightPanelPageObject, +}) => { + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const segment0 = panel.nthSegment(0); + await expect(segment0.title).toHaveText(defaultSegment0Name); + + // The lock icon is only rendered inside the row once locked. + await expectRowUnlocked(segment0); + + let lockToggle = await segment0.actions.lockToggleMenuItem(); + await expect(lockToggle, 'Expected the menu item to read "Lock" before locking').toHaveText( + 'Lock' + ); + + // Lock the segment by clicking the already-open menu item. + await lockToggle.click(); + await expectRowLocked(segment0); + + // Reopen the actions menu: the toggle label should now read "Unlock". + lockToggle = await segment0.actions.lockToggleMenuItem(); + await expect(lockToggle, 'Expected the menu item to read "Unlock" after locking').toHaveText('Unlock'); + + // Unlock the segment again by clicking the already-open menu item. + await lockToggle.click(); + await expectRowUnlocked(segment0); + + lockToggle = await segment0.actions.lockToggleMenuItem(); + await expect( + lockToggle, + 'Expected the menu item to read "Lock" again after unlocking' + ).toHaveText('Lock'); +}); + +test('should stop a locked contour segment from responding to drag edits', async ({ + rightPanelPageObject, + viewportPageObject, +}) => { + const panel = rightPanelPageObject.contourSegmentationPanel.panel; + const segment0 = panel.nthSegment(0); + await expect( + segment0.title, + 'Expected the first segment to be the Threshold segment' + ).toHaveText(defaultSegment0Name); + + // Isolate segment 0 so the viewport renders exactly one contour path and it is + // the active segment for the drag edits below. + await contourShowOnlyNthSegment({ + segmentationPanel: rightPanelPageObject.contourSegmentationPanel, + }); + + const svgPath = (await viewportPageObject.getById('default')).svg('path'); + await expect(svgPath, 'Expected exactly one visible contour path').toHaveCount(1); + + const originalSvgPathDAttribute = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); + expect(originalSvgPathDAttribute, 'Expected a visible SVG path for the unlocked segment').not.toBeNull(); + + // Lock the segment via the actions menu and confirm the lock state has propagated + // to the row before dragging. + await segment0.actions.toggleLock(); + await expectRowLocked(segment0); + + // Drag the path while locked. + await simulateNormalizedDragOnElement({ + locator: svgPath, + start: { x: 0.8, y: 0.8 }, + end: { x: 0.2, y: 0.2 }, + }); + + const svgPathDAttributeAfterDragWhileLocked = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); + + // The path should not have changed after the drag attempt while locked. + expect( + svgPathDAttributeAfterDragWhileLocked, + 'Expected the contour path to remain unchanged after dragging while locked' + ).toBe(originalSvgPathDAttribute); + + // Unlock the segment and confirm the lock indicator is gone before dragging again. + await segment0.actions.toggleLock(); + await expectRowUnlocked(segment0); + + // Drag with the same motion as while locked. + await simulateNormalizedDragOnElement({ + locator: svgPath, + start: { x: 0.8, y: 0.8 }, + end: { x: 0.2, y: 0.2 }, + }); + + const svgPathDAfterDragWhileUnlocked = await getSvgAttribute({ + viewportPageObject, + svgInnerElement: 'path', + attributeName: 'd', + }); + // The path should change after dragging while unlocked. + expect( + svgPathDAfterDragWhileUnlocked, + 'Expected the contour path to change after dragging while unlocked' + ).not.toBe(originalSvgPathDAttribute); +}); diff --git a/tests/ContourSegmentToggleVisibility.spec.ts b/tests/ContourSegmentToggleVisibility.spec.ts index 722faf13b..9a89276dc 100644 --- a/tests/ContourSegmentToggleVisibility.spec.ts +++ b/tests/ContourSegmentToggleVisibility.spec.ts @@ -1,14 +1,22 @@ -import { expect, test, visitStudy, getSvgAttribute, navigateWithViewportArrow } from './utils'; +import { + expect, + test, + visitStudyAndHydrate, + getSvgAttribute, + navigateWithViewportArrow, +} from './utils'; const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501'; test.beforeEach( async ({ page, leftPanelPageObject, DOMOverlayPageObject, rightPanelPageObject }) => { - const mode = 'segmentation'; - await visitStudy(page, studyInstanceUID, mode, 2000); - await leftPanelPageObject.loadSeriesByModality('RTSTRUCT'); - await page.waitForTimeout(5000); - await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); + await visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality: 'RTSTRUCT', + }); await rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(0).click(); await page.waitForTimeout(1000); } diff --git a/tests/pages/RightPanelPageObject.ts b/tests/pages/RightPanelPageObject.ts index 49153b23b..1757f7182 100644 --- a/tests/pages/RightPanelPageObject.ts +++ b/tests/pages/RightPanelPageObject.ts @@ -37,6 +37,7 @@ export class RightPanelPageObject { private getActionsMenu(row: Locator) { const actionsButton = row.getByTestId('actionsMenuTrigger'); + const lockToggle = this.page.getByTestId('LockToggle'); return { button: actionsButton, @@ -47,13 +48,14 @@ export class RightPanelPageObject { await actionsButton.click(); await this.page.getByTestId('Delete').click(); }, + // Opens the actions menu and returns the lock/unlock menu item locator + lockToggleMenuItem: async () => { + await actionsButton.click(); + return lockToggle; + }, toggleLock: async () => { await actionsButton.click(); - await this.page.getByTestId('LockToggle').click(); - }, - unlock: async () => { - await actionsButton.click(); - await this.page.getByTestId('Unlock').click(); + await lockToggle.click(); }, rename: async (text: string) => { await actionsButton.click(); @@ -106,6 +108,9 @@ export class RightPanelPageObject { get title() { return row.getByTestId('data-row-title'); }, + get lockIcon() { + return row.locator('g#Lock'); + }, get rowDataColorHex() { return row.getByTestId('data-row-colorhex'); }, diff --git a/tests/utils/assertions.ts b/tests/utils/assertions.ts index 42ff6473f..e1e26a367 100644 --- a/tests/utils/assertions.ts +++ b/tests/utils/assertions.ts @@ -1,4 +1,4 @@ -import { Page } from '@playwright/test'; +import { Locator, Page } from '@playwright/test'; import { expect } from 'playwright-test-coverage'; import { DOMOverlayPageObject } from '../pages'; @@ -65,3 +65,11 @@ export async function assertBoundingBoxIsContainedWithin({ export async function expectRowSelected(rowObject) { await expect(rowObject.locator).toContainClass('bg-popover'); } + +export async function expectRowLocked(rowObject: { lockIcon: Locator }) { + await expect(rowObject.lockIcon, 'Expected the row to show the lock icon').toHaveCount(1); +} + +export async function expectRowUnlocked(rowObject: { lockIcon: Locator }) { + await expect(rowObject.lockIcon, 'Expected the row to not show the lock icon').toHaveCount(0); +} diff --git a/tests/utils/contourShowOnlyNthSegment.ts b/tests/utils/contourShowOnlyNthSegment.ts new file mode 100644 index 000000000..3851d9877 --- /dev/null +++ b/tests/utils/contourShowOnlyNthSegment.ts @@ -0,0 +1,16 @@ +import type { RightPanelPageObject } from '../pages'; + +// Hide all segments, to show only the nth one and makes it the active segment +export async function contourShowOnlyNthSegment({ + segmentationPanel, + index = 0, +}: { + segmentationPanel: RightPanelPageObject['contourSegmentationPanel']; + index?: number; +}) { + await segmentationPanel.segmentsVisibilityToggle.click(); + const segment = segmentationPanel.panel.nthSegment(index); + await segment.toggleVisibility(); + await segment.click(); + return segment; +} diff --git a/tests/utils/index.ts b/tests/utils/index.ts index 281b9eb64..2d2e60008 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -1,8 +1,5 @@ import { visitStudy } from './visitStudy'; -import { - addOHIFConfiguration, - addOHIFGlobalCustomizations, -} from './OHIFConfiguration'; +import { addOHIFConfiguration, addOHIFGlobalCustomizations } from './OHIFConfiguration'; import { checkForScreenshot } from './checkForScreenshot'; import { screenShotPaths } from './screenShotPaths'; import { @@ -26,6 +23,8 @@ import { attemptAction } from './attemptAction'; import { addLengthMeasurement } from './addLengthMeasurement'; import { getSvgAttribute } from './getSvgAttribute'; import { navigateWithViewportArrow } from './navigateWithViewportArrow'; +import { contourShowOnlyNthSegment } from './contourShowOnlyNthSegment'; +import { visitStudyAndHydrate } from './visitStudyAndHydrate'; import { test, expect } from './fixture'; import { subscribeToMeasurementAdded } from './subscribeToMeasurement'; import { @@ -60,6 +59,8 @@ export { subscribeToMeasurementAdded, getSvgAttribute, navigateWithViewportArrow, + contourShowOnlyNthSegment, + visitStudyAndHydrate, waitForAnyViewportNeedsRender, waitForViewportsRendered, waitForViewportRenderCycle, diff --git a/tests/utils/visitStudyAndHydrate.ts b/tests/utils/visitStudyAndHydrate.ts new file mode 100644 index 000000000..8af69a3b4 --- /dev/null +++ b/tests/utils/visitStudyAndHydrate.ts @@ -0,0 +1,31 @@ +import { Page } from '@playwright/test'; +import { expect } from 'playwright-test-coverage'; + +import type { DOMOverlayPageObject, LeftPanelPageObject } from '../pages'; +import { visitStudy } from './visitStudy'; +import { waitForViewportsRendered } from './waitForViewportsRendered'; + +// Visits a study and accepts the segmentation hydration prompt +export async function visitStudyAndHydrate({ + page, + leftPanelPageObject, + DOMOverlayPageObject, + studyInstanceUID, + modality, + mode = 'segmentation', +}: { + page: Page; + leftPanelPageObject: LeftPanelPageObject; + DOMOverlayPageObject: DOMOverlayPageObject; + studyInstanceUID: string; + modality: string; + mode?: string; +}) { + await visitStudy(page, studyInstanceUID, mode, 2000); + + await leftPanelPageObject.loadSeriesByModality(modality); + await waitForViewportsRendered(page); + await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible(); + + await DOMOverlayPageObject.viewport.segmentationHydration.yes.click(); +}