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