diff --git a/extensions/default/src/utils/colorPickerDialog.tsx b/extensions/default/src/utils/colorPickerDialog.tsx
index 006a72050..93df339c0 100644
--- a/extensions/default/src/utils/colorPickerDialog.tsx
+++ b/extensions/default/src/utils/colorPickerDialog.tsx
@@ -12,7 +12,7 @@ function ColorPickerDialog({ value, hide, onSave }) {
};
return (
-
+
- Cancel
+
+ Cancel
+
{
hide();
onSave(color);
diff --git a/platform/ui-next/src/components/DataRow/DataRow.tsx b/platform/ui-next/src/components/DataRow/DataRow.tsx
index 1f4fa00c5..b99384b6f 100644
--- a/platform/ui-next/src/components/DataRow/DataRow.tsx
+++ b/platform/ui-next/src/components/DataRow/DataRow.tsx
@@ -275,6 +275,7 @@ const DataRowComponent = React.forwardRef(
)}
diff --git a/tests/ContourSegmentColorChange.spec.ts b/tests/ContourSegmentColorChange.spec.ts
new file mode 100644
index 000000000..168c1dd6c
--- /dev/null
+++ b/tests/ContourSegmentColorChange.spec.ts
@@ -0,0 +1,177 @@
+import {
+ expect,
+ test,
+ visitStudy,
+ waitForViewportsRendered,
+ getSvgAttribute,
+} from './utils';
+
+const NEW_HEX = '#FF00FF';
+const NEW_HEX_CSS_RGB = 'rgb(255, 0, 255)';
+const NEW_HEX_CSS_RGBA = 'rgba(255, 0, 255, 1)';
+
+// Default colors baked into the RTSTRUCT in the canonical contour study.
+const THRESHOLD_CONTOUR_DEFAULT_HEX = '#00EBEB';
+const THRESHOLD_CONTOUR_DEFAULT_CSS_RGBA = 'rgba(0, 235, 235, 1)';
+const THRESHOLD_CONTOUR_DEFAULT_CSS_RGB = 'rgb(0, 235, 235)';
+
+const STUDY_UID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
+
+test.beforeEach(async ({ page, leftPanelPageObject, DOMOverlayPageObject }) => {
+ await visitStudy(page, STUDY_UID, 'segmentation', 2000);
+
+ await leftPanelPageObject.loadSeriesByModality('RTSTRUCT');
+ await waitForViewportsRendered(page);
+ await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible();
+ await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
+});
+
+test('opens the color edit popup when "Change Color" is clicked', async ({
+ rightPanelPageObject,
+ DOMOverlayPageObject,
+}) => {
+ const segment = rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(0);
+
+ await segment.actions.openChangeColor();
+
+ await expect(DOMOverlayPageObject.dialog.colorPicker.locator).toBeVisible();
+ await expect(DOMOverlayPageObject.dialog.title).toHaveText('Segment Color');
+ await expect(DOMOverlayPageObject.dialog.colorPicker.saveButton).toBeVisible();
+ await expect(DOMOverlayPageObject.dialog.colorPicker.cancelButton).toBeVisible();
+
+ await expect(DOMOverlayPageObject.dialog.colorPicker.hexInput).toHaveValue(
+ THRESHOLD_CONTOUR_DEFAULT_HEX
+ );
+});
+
+test('changes the contour color when the user saves the edits', async ({
+ viewportPageObject,
+ rightPanelPageObject,
+ DOMOverlayPageObject,
+}) => {
+ await rightPanelPageObject.contourSegmentationPanel.segmentsVisibilityToggle.click();
+ const segment = rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(0);
+ await segment.toggleVisibility();
+ await segment.click();
+
+ await expect(segment.rowDataColorHex).toHaveCSS('background-color',THRESHOLD_CONTOUR_DEFAULT_CSS_RGB);
+
+ const svgStrokeAttributeBeforeColorChange = await getSvgAttribute({
+ viewportPageObject,
+ svgInnerElement: 'path',
+ attributeName: 'stroke',
+ });
+
+ expect(svgStrokeAttributeBeforeColorChange, 'Expected SVG stroke attribute to be original threshold color').toBe(
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGBA
+ );
+
+ // change color
+ await segment.actions.changeColor(NEW_HEX);
+
+ await expect(DOMOverlayPageObject.dialog.colorPicker.locator).toBeHidden();
+ await expect(segment.rowDataColorHex).toHaveCSS('background-color', NEW_HEX_CSS_RGB);
+
+ //check svg path stroke attribute is updated with new color
+ const svgStrokeAttributeAfterColorChange = await getSvgAttribute({
+ viewportPageObject,
+ svgInnerElement: 'path',
+ attributeName: 'stroke',
+ });
+
+ expect(svgStrokeAttributeAfterColorChange, 'Expected SVG stroke attribute to be updated with new color').toBe(
+ NEW_HEX_CSS_RGBA
+ );
+});
+
+test('does not change the contour color when the user cancels', async ({
+ rightPanelPageObject,
+ DOMOverlayPageObject,
+ viewportPageObject
+}) => {
+ await rightPanelPageObject.contourSegmentationPanel.segmentsVisibilityToggle.click();
+ const segment = rightPanelPageObject.contourSegmentationPanel.panel.nthSegment(0);
+ await segment.toggleVisibility();
+ await segment.click();
+
+ await expect(segment.rowDataColorHex).toHaveCSS(
+ 'background-color',
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGB
+ );
+
+ const svgStrokeAttributeBeforeColorChange = await getSvgAttribute({
+ viewportPageObject,
+ svgInnerElement: 'path',
+ attributeName: 'stroke',
+ });
+
+ expect(svgStrokeAttributeBeforeColorChange, 'Expected SVG stroke attribute to be original threshold color').toBe(
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGBA
+ );
+
+ await segment.actions.cancelChangeColor(NEW_HEX);
+
+ await expect(DOMOverlayPageObject.dialog.colorPicker.locator).toBeHidden();
+ await expect(segment.rowDataColorHex).toHaveCSS('background-color', THRESHOLD_CONTOUR_DEFAULT_CSS_RGB);
+
+ //check svg path stroke attribute is updated with new color
+ const svgStrokeAttributeAfterColorChange = await getSvgAttribute({
+ viewportPageObject,
+ svgInnerElement: 'path',
+ attributeName: 'stroke',
+ });
+
+ expect(svgStrokeAttributeAfterColorChange, 'Expected SVG stroke attribute to remain the original color').toBe(
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGBA
+ );
+});
+
+test('duplicated contour will be generated with a new color', async ({
+ viewportPageObject,
+ rightPanelPageObject,
+}) => {
+ await rightPanelPageObject.contourSegmentationPanel.segmentsVisibilityToggle.click();
+ const panel = rightPanelPageObject.contourSegmentationPanel.panel;
+ const segment0 = panel.nthSegment(0);
+ await segment0.toggleVisibility();
+ await segment0.click();
+
+ await expect(segment0.rowDataColorHex).toHaveCSS(
+ 'background-color',
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGB
+ );
+
+ const svgStrokeAttributeBeforeColorChange = await getSvgAttribute({
+ viewportPageObject,
+ svgInnerElement: 'path',
+ attributeName: 'stroke',
+ });
+
+ expect(svgStrokeAttributeBeforeColorChange, 'Expected SVG stroke attribute to be original threshold color').toBe(
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGBA
+ );
+
+ // duplicate the segment
+ const initialCount = await panel.getSegmentCount();
+ expect(initialCount, 'Expected to start with 4 segments').toBe(4);
+ await segment0.actions.duplicate();
+ await segment0.toggleVisibility(); // toggle original segment 0 visibility off to be able to grab the duplicated segment's path
+
+ const duplicateSegment = panel.nthSegment(4);
+ await duplicateSegment.click();
+
+ const duplicatedSegmentSvgStrokeAttribute = await getSvgAttribute({
+ viewportPageObject,
+ svgInnerElement: 'path',
+ attributeName: 'stroke',
+ });
+
+ await expect(duplicateSegment.rowDataColorHex).not.toHaveCSS(
+ 'background-color',
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGB
+ );
+
+ expect(duplicatedSegmentSvgStrokeAttribute, 'Expected duplicated segment to have a different stroke color').not.toBe(
+ THRESHOLD_CONTOUR_DEFAULT_CSS_RGBA
+ );
+});
diff --git a/tests/pages/DOMOverlayPageObject.ts b/tests/pages/DOMOverlayPageObject.ts
index cd92c4443..d7234c790 100644
--- a/tests/pages/DOMOverlayPageObject.ts
+++ b/tests/pages/DOMOverlayPageObject.ts
@@ -60,6 +60,39 @@ export class DOMOverlayPageObject {
return new DicomTagBrowserPageObject(page);
},
+ get colorPicker() {
+ const locator = page.getByTestId('color-picker-dialog');
+ const hexInput = locator.getByLabel('hex');
+ const saveButton = locator.getByTestId('color-picker-save-btn');
+ const cancelButton = locator.getByTestId('color-picker-cancel-btn');
+ return {
+ locator,
+ hexInput,
+ saveButton,
+ cancelButton,
+ fillHex: async (hex: string) => {
+ await hexInput.fill(hex);
+ await hexInput.press('Enter');
+ },
+ save: async () => {
+ await saveButton.click();
+ },
+ cancel: async () => {
+ await cancelButton.click();
+ },
+ fillHexAndSave: async (hex: string) => {
+ await hexInput.fill(hex);
+ await hexInput.press('Enter');
+ await saveButton.click();
+ },
+ fillHexAndCancel: async (hex: string) => {
+ await hexInput.fill(hex);
+ await hexInput.press('Enter');
+ await cancelButton.click();
+ },
+ };
+ },
+
title: page.locator('[role="dialog"] h2'),
};
}
diff --git a/tests/pages/RightPanelPageObject.ts b/tests/pages/RightPanelPageObject.ts
index 04c479676..3362ca65b 100644
--- a/tests/pages/RightPanelPageObject.ts
+++ b/tests/pages/RightPanelPageObject.ts
@@ -73,6 +73,26 @@ export class RightPanelPageObject {
await actionsButton.click();
await this.page.getByTestId('Duplicate').click();
},
+ openChangeColor: async () => {
+ await actionsButton.click();
+ await this.page.getByTestId('Change Color').click();
+ },
+ changeColor: async (hex: string) => {
+ await actionsButton.click();
+ await this.page.getByTestId('Change Color').click();
+ await this.DOMOverlayPageObject.dialog.colorPicker.fillHexAndSave(hex);
+ },
+ // This function assumes the user opens the change color dialog,
+ // but then cancels out of it instead of saving a new color.
+ cancelChangeColor: async (hex?: string) => {
+ await actionsButton.click();
+ await this.page.getByTestId('Change Color').click();
+ if (hex) {
+ await this.DOMOverlayPageObject.dialog.colorPicker.fillHexAndCancel(hex);
+ } else {
+ await this.DOMOverlayPageObject.dialog.colorPicker.cancel();
+ }
+ },
};
}
@@ -86,6 +106,9 @@ export class RightPanelPageObject {
get title() {
return row.getByTestId('data-row-title');
},
+ get rowDataColorHex() {
+ return row.getByTestId('data-row-colorhex');
+ },
click: async () => {
await row.getByTestId('data-row-title').click();
},