Skip to content
Draft
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
2b5d01f
feat(tests): Add tests for duplicating contour segments
diattamo May 7, 2026
f8cc5a9
Address review comments
diattamo May 7, 2026
5bf3c5b
Merge branch 'master' into feat/contour-segments-interactions-duplicate
diattamo May 7, 2026
574fc78
Merge branch 'master' into feat/contour-segments-interactions-duplicate
diattamo May 13, 2026
33dbc16
Address review comments
diattamo May 13, 2026
7e65738
Address minor comment
diattamo May 13, 2026
0588987
remove 'd' from the SVG Inner elements
diattamo May 15, 2026
49d43c1
Refactor the getSVGPath to getSVGPAthAttribute - Update other tests t…
diattamo May 16, 2026
9b1509e
Merge branch 'master' into feat/contour-segments-interactions-duplicate
diattamo May 16, 2026
197b32c
Add validation for SVG path count for the contour segment before dupl…
diattamo May 16, 2026
710a50a
Address code review comments - refactor to update getSVGAtrribute calls
diattamo May 20, 2026
894d0c2
Merge remote-tracking branch 'upstream/master' into feat/contour-segm…
diattamo May 20, 2026
0511bf8
fix accidental deletion
diattamo May 20, 2026
97e2b8e
test: add navigation verification for duplicated contour segments
diattamo May 20, 2026
9c3d464
Merge branch 'master' into tests/duplicated-contour-navigation-issue
diattamo May 21, 2026
066df27
Merge branch 'master' into tests/duplicated-contour-navigation-issue
diattamo May 21, 2026
6fcc4fa
Merge branch 'tests/duplicated-contour-navigation-issue' of https://g…
diattamo May 21, 2026
f3fb318
Correct spelling of 'Threshold'
diattamo May 21, 2026
4bee07f
code review updates
diattamo May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 86 additions & 18 deletions tests/ContourSegNavigation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test, visitStudy, getSvgPath, navigateWithViewportArrow } from './utils';
import { expect, test, visitStudy, getSvgAttribute, navigateWithViewportArrow } from './utils';
import { expectRowSelected } from './utils/assertions';

const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
Expand Down Expand Up @@ -26,42 +26,74 @@ test('should navigate the contours when clicking each segments in the right pane
const getSegment = (index: number) =>
rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(index);

const seg0 = await getSvgPath(viewportPageObject);
const seg0 = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg0, 'Segment at index 0: expected a non-null SVG path').not.toBeNull();

await getSegment(3).click();
const seg3 = await getSvgPath(viewportPageObject);
const seg3 = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg3, 'Segment at index 3: expected a non-null SVG path').not.toBeNull();
await expectRowSelected(getSegment(3));

await getSegment(2).click();
const seg2 = await getSvgPath(viewportPageObject);
const seg2 = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg2, 'Segment at index 2: expected a non-null SVG path').not.toBeNull();
await expectRowSelected(getSegment(2));

await getSegment(1).click();
const seg1 = await getSvgPath(viewportPageObject);
const seg1 = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg1, 'Segment at index 1: expected a non-null SVG path').not.toBeNull();
await expectRowSelected(getSegment(1));

// Clicking segments again should return the original paths
await getSegment(2).click();
const seg2Again = await getSvgPath(viewportPageObject);
const seg2Again = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg2Again, 'Segment 2 again: expected to match the original segment 2 path').toBe(seg2);
await expectRowSelected(getSegment(2));

await getSegment(1).click();
const seg1Again = await getSvgPath(viewportPageObject);
const seg1Again = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg1Again, 'Segment 1 again: expected to match the original segment 1 path').toBe(seg1);
await expectRowSelected(getSegment(1));

await getSegment(0).click();
const seg0Again = await getSvgPath(viewportPageObject);
const seg0Again = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg0Again, 'Segment 0 again: expected to match the original segment 0 path').toBe(seg0);
await expectRowSelected(getSegment(0));

await getSegment(3).click();
const seg3Again = await getSvgPath(viewportPageObject);
const seg3Again = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(seg3Again, 'Segment 3 again: expected to match the original segment 3 path').toBe(seg3);
await expectRowSelected(getSegment(3));
});
Expand All @@ -73,29 +105,49 @@ test('should navigate the segmentations using the Viewport arrow buttons', async
const getSegment = (index: number) =>
rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(index);

const initialSvgPath = await getSvgPath(viewportPageObject);
const initialSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(initialSvgPath, 'Segment at index 0: expected a non-null SVG path').not.toBeNull();
// Expect the correct segment to be selected in the right panel
await expectRowSelected(getSegment(0));

await navigateWithViewportArrow(viewportPageObject, 'next');
const secondSvgPath = await getSvgPath(viewportPageObject);
const secondSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
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');
const thirdSvgPath = await getSvgPath(viewportPageObject);
const thirdSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
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');
const fourthSvgPath = await getSvgPath(viewportPageObject);
const fourthSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(fourthSvgPath, 'Segment at index 3: expected a different SVG path from segment 2').not.toBe(thirdSvgPath);
await expectRowSelected(getSegment(3));

// Wraparound test — next from last should return to first segment
await navigateWithViewportArrow(viewportPageObject, 'next');
const svgPathWraparoundWithNext = await getSvgPath(viewportPageObject);
const svgPathWraparoundWithNext = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(
svgPathWraparoundWithNext,
'Expected svg path to match the initial svg path after wrapping around with next navigation'
Expand All @@ -104,25 +156,41 @@ test('should navigate the segmentations using the Viewport arrow buttons', async

// Wraparound test — prev from first should return to last segment
await navigateWithViewportArrow(viewportPageObject, 'prev');
const svgPathWraparoundWithPrev = await getSvgPath(viewportPageObject);
const svgPathWraparoundWithPrev = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(
svgPathWraparoundWithPrev,
'Expected svg path to match the fourth svg path after wrapping around with prev navigation'
).toBe(fourthSvgPath);
await expectRowSelected(getSegment(3));

await navigateWithViewportArrow(viewportPageObject, 'prev');
const backToThirdSvgPath = await getSvgPath(viewportPageObject);
const backToThirdSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(backToThirdSvgPath, 'Expected path to match third segment after going prev from fourth').toBe(thirdSvgPath);
await expectRowSelected(getSegment(2));

await navigateWithViewportArrow(viewportPageObject, 'prev');
const backToSecondSvgPath = await getSvgPath(viewportPageObject);
const backToSecondSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(backToSecondSvgPath, 'Expected path to match second segment after going prev from third').toBe(secondSvgPath);
await expectRowSelected(getSegment(1));

await navigateWithViewportArrow(viewportPageObject, 'prev');
const backToFirstSvgPath = await getSvgPath(viewportPageObject);
const backToFirstSvgPath = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(backToFirstSvgPath, 'Expected path to match first segment after going prev from second').toBe(initialSvgPath);
await expectRowSelected(getSegment(0));
});
160 changes: 160 additions & 0 deletions tests/ContourSegmentDuplicate.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {
expect,
test,
visitStudy,
waitForViewportsRendered,
} from './utils';
import { getSvgAttribute } from './utils/getSvgAttribute';
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

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();
});

test('should duplicate a contour segment and add a new row to the panel', async ({
page,
rightPanelPageObject,
}) => {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const panel = rightPanelPageObject.contourSegmentationPanel.panel;

const initialCount = await panel.getSegmentCount();
expect(initialCount, 'Expected to load with 4 segments').toBe(4);

const segment0 = panel.nthSegment(0);
await expect(segment0.title).toHaveText(defaultSegment0Name);

await segment0.actions.duplicate();

const countAfterDuplicate = await panel.getSegmentCount();
expect(countAfterDuplicate, 'Expected one additional segment row after duplicating').toBe(5);

//New segment's default name is formatted as "Segment {segmentCount}"
const newSegmentLocator = panel.nthSegment(initialCount).title;
await expect(newSegmentLocator, 'Expected correct title for duplicated segment').toHaveText(`Segment 5`);

// Original segment titles should be unchanged
await expect(panel.nthSegment(0).title).toHaveText(defaultSegment0Name);
await expect(panel.nthSegment(1).title).toHaveText(defaultSegment1Name);
});

test('should duplicate the same segment multiple times', async ({
page,
rightPanelPageObject,
}) => {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const panel = rightPanelPageObject.contourSegmentationPanel.panel;

const segment0 = panel.nthSegment(0);

await segment0.actions.duplicate();
expect(await panel.getSegmentCount(), 'Expected one additional segment row after duplicating').toBe(5);

const firstDuplicateTitleLocator = panel.nthSegment(4).title;
await expect(firstDuplicateTitleLocator, 'Expected correct title for first duplicated segment').toHaveText(`Segment 5`);

await segment0.actions.duplicate();
expect(await panel.getSegmentCount(), 'Expected another segment row after duplicating the same segment again').toBe(6);

const secondDuplicateTitleLocator = panel.nthSegment(5).title;
await expect(secondDuplicateTitleLocator, 'Expected correct title for second duplicated segment').toHaveText(`Segment 6`);
});

test('should render the duplicated contour on the viewport', async ({
rightPanelPageObject,
viewportPageObject,
}) => {
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 sourceSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'});
expect(sourceSvgPath, 'Expected a visible SVG path for the source segment').not.toBeNull();
const sourceSvgPaths = (await viewportPageObject.getById('default')).svg('path');
expect(sourceSvgPaths, 'Expected only one SVG path element for the original segment').toHaveCount(1);

// New segment is at index 4
await segment0.actions.duplicate();
const duplicatedSegment = panel.nthSegment(4);

// Hide again to show duplicate only
await segment0.toggleVisibility();
await duplicatedSegment.click();

const duplicatedSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'});
expect(duplicatedSvgPath, 'Expected a visible SVG path for the duplicated segment').not.toBeNull();
const duplicatedSvgPaths = (await viewportPageObject.getById('default')).svg('path');
expect(duplicatedSvgPaths, 'Expected only one SVG path element for the duplicated segment').toHaveCount(1);

expect(
duplicatedSvgPath,
'Expected the duplicated segment to have the same SVG path as the source'
).toBe(sourceSvgPath);
});


test.skip('should navigate to the correct instance number when a duplicated contour segment is selected', async ({
rightPanelPageObject,
viewportPageObject,
}) => {
const panel = rightPanelPageObject.contourSegmentationPanel.panel;

// get instance overlay of the contour segment at index 0
const originalSegment = panel.nthSegment(0);
await originalSegment.click();
const originalSegmentInstanceInfo = (await viewportPageObject.getById('default')).overlayText.bottomRight.instanceNumber;
expect(originalSegmentInstanceInfo, 'Expected instance information to be displayed in the viewport overlay').not.toBeNull();
await expect(originalSegmentInstanceInfo, 'Expected instance information to be 46 for the Threshhold segment').toHaveText('I:46 (46/47)');
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated

// Duplicate segment so new segment is at index 4
await originalSegment.actions.duplicate();

//click another segment to ensure instance number changes accordingly
await panel.nthSegment(2).click();
const anotherSegmentInstanceInfo = (await viewportPageObject.getById('default')).overlayText.bottomRight.instanceNumber;
expect(anotherSegmentInstanceInfo, 'Expected instance information to be displayed in the viewport overlay').not.toBeNull();
await expect(anotherSegmentInstanceInfo, 'Expected instance information to be different from original contour').not.toHaveText('I:46 (46/47)');

//click duplicated segment to ensure instance number is consistent with original segment
const duplicatedSegment = panel.nthSegment(4);
await duplicatedSegment.click();
const duplicatedSegmentInstanceInfoAfter = (await viewportPageObject.getById('default')).overlayText.bottomRight.instanceNumber;
expect(duplicatedSegmentInstanceInfoAfter, 'Expected instance information to be displayed in the viewport overlay').not.toBeNull();
await expect(duplicatedSegmentInstanceInfoAfter, 'Expected instance information to be same as original contour after clicking duplicated segment').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();
const originalSegmentSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'});
expect(originalSegmentSvgPath, 'Expected a visible SVG path for the original segment').not.toBeNull();

// hide original segment to show duplicate only
await originalSegment.toggleVisibility();

await duplicatedSegment.toggleVisibility();
await duplicatedSegment.click();
const duplicatedSegmentSvgPath = await getSvgAttribute({viewportPageObject, svgInnerElement: 'path', attributeName: 'd'});
expect(duplicatedSegmentSvgPath, 'Expected a visible SVG path for the duplicated segment').not.toBeNull();

expect(
duplicatedSegmentSvgPath,
'Expected the duplicated segment to have the same SVG path as the original segment'
).toBe(originalSegmentSvgPath);
} );
14 changes: 11 additions & 3 deletions tests/ContourSegmentToggleVisibility.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { expect, test, visitStudy, getSvgPath, navigateWithViewportArrow } from './utils';
import { expect, test, visitStudy, getSvgAttribute, navigateWithViewportArrow } from './utils';

const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';

Expand Down Expand Up @@ -78,7 +78,11 @@ test('should restore svg paths when segment visibility is toggled on/off', async

const segment0 = rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(0);
await segment0.toggleVisibility();
const svgPathBefore = await getSvgPath(viewportPageObject);
const svgPathBefore = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(svgPathBefore, 'Expected a visible SVG path for segment 0').not.toBeNull();

await segment0.toggleVisibility();
Expand All @@ -88,7 +92,11 @@ test('should restore svg paths when segment visibility is toggled on/off', async
).toHaveCount(0);

await segment0.toggleVisibility();
const svgPathAfter = await getSvgPath(viewportPageObject);
const svgPathAfter = await getSvgAttribute({
viewportPageObject,
svgInnerElement: 'path',
attributeName: 'd',
});
expect(svgPathAfter, 'Expected SVG path to be restored after toggling visibility back on').toBe(
svgPathBefore
);
Expand Down
2 changes: 1 addition & 1 deletion tests/pages/ViewportPageObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DataOverlayPageObject } from './DataOverlayPageObject';
import { DOMOverlayPageObject } from './DOMOverlayPageObject';
import { MagnifyGlassPageObject } from './MagnifyGlassPageObject';

type SvgInnerElement = 'circle' | 'path' | 'd' | 'line' | 'g';
export type SvgInnerElement = 'circle' | 'path' | 'line' | 'g';

type NormalizedDragParams = {
start: { x: number; y: number };
Expand Down
Loading