diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx index 7e871308e..fd98c50c8 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTable.tsx @@ -44,6 +44,7 @@ export const SegmentationTableRoot = (props: SegmentationTableProps) => { children, showConfig: externalShowConfig, selectedSegmentationIdForType, + segmentationRepresentationTypes, ...contextProps } = props; @@ -95,6 +96,10 @@ export const SegmentationTableRoot = (props: SegmentationTableProps) => { } }; + const dataCyTypeSuffix = segmentationRepresentationTypes + ? `-${segmentationRepresentationTypes[0]}` + : ''; + return ( { activeSegmentation, activeRepresentation, selectedSegmentationIdForType, + segmentationRepresentationTypes, ...contextProps, setShowConfig: toggleShowConfig, }} @@ -120,7 +126,10 @@ export const SegmentationTableRoot = (props: SegmentationTableProps) => { {t(title)} {hasConfigComponent && ( -
+
{ diff --git a/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx index af1f0609f..08aebbbf9 100644 --- a/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx +++ b/platform/ui-next/src/components/SegmentationTable/SegmentationTableConfig.tsx @@ -1,11 +1,10 @@ import React from 'react'; import { useTranslation } from 'react-i18next'; import { Tabs, TabsList, TabsTrigger } from '../Tabs'; -import { Slider } from '../Slider'; import { Icons } from '../Icons'; import { Switch } from '../Switch'; import { Label } from '../Label'; -import { Input } from '../Input'; +import Numeric from '../Numeric'; import { useSegmentationTableContext } from './contexts'; export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = ({ children }) => { @@ -33,6 +32,10 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = return null; } + const dataCyTypeSuffix = segmentationRepresentationTypes + ? `-${segmentationRepresentationTypes[0]}` + : ''; + return (
@@ -87,49 +90,52 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = - - setFillAlpha({ type: segmentationRepresentationTypes?.[0] }, value) - } - max={1} - min={0} - step={0.1} - /> - - setFillAlpha({ type: segmentationRepresentationTypes?.[0] }, Number(e.target.value)) - } - /> + data-cy={`segmentation-config-opacity${dataCyTypeSuffix}`} + > + + setFillAlpha({ type: segmentationRepresentationTypes?.[0] }, value as number) + } + > + + +
- - setOutlineWidth({ type: segmentationRepresentationTypes?.[0] }, value) - } - max={10} - min={0} - step={0.1} +
- - setOutlineWidth( - { type: segmentationRepresentationTypes?.[0] }, - Number(e.target.value) - ) - } - className="mx-1 w-10 flex-none text-center" - /> + data-cy={`segmentation-config-border${dataCyTypeSuffix}`} + > + + setOutlineWidth({ type: segmentationRepresentationTypes?.[0] }, value as number) + } + > + + +
@@ -149,19 +155,24 @@ export const SegmentationTableConfig: React.FC<{ children?: React.ReactNode }> = - setFillAlphaInactive({}, value)} - max={1} - min={0} - step={0.1} - /> - setFillAlphaInactive({}, Number(e.target.value))} - /> + data-cy={`segmentation-config-opacity-inactive${dataCyTypeSuffix}`} + > + setFillAlphaInactive({}, value as number)} + > + + +
)} {children} diff --git a/tests/SegmentationPanel.spec.ts b/tests/SegmentationPanel.spec.ts index 8bb2f4b34..98ede3f90 100644 --- a/tests/SegmentationPanel.spec.ts +++ b/tests/SegmentationPanel.spec.ts @@ -1,5 +1,7 @@ import { expect, test, visitStudy } from './utils'; +const nonNumericError = 'Cannot type text into input[type=number]'; + test.beforeEach(async ({ page }) => { // Using same one as JumpToMeasurementMPR.spec.ts const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458'; @@ -76,3 +78,128 @@ test('checks saved segmentations loads and jumps to slices', async ({ await rightPanelPageObject.labelMapSegmentationPanel.panel.segmentByText('Pancreas').click(); await expect(viewportInfoBottomRight).toContainText('22/'); }); + +test.describe('Segmentation panel config input validation for labelmap', () => { + test.beforeEach(async ({ rightPanelPageObject }) => { + await rightPanelPageObject.labelMapSegmentationPanel.addSegmentationButton.click(); + + await rightPanelPageObject.labelMapSegmentationPanel.config.toggle.click(); + }); + + test.describe('opacity', () => { + test('should accept valid values', async ({ rightPanelPageObject }) => { + const { opacity } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await opacity.fill('0'); + await expect(opacity.input).toHaveValue('0'); + + await opacity.fill('0.5'); + await expect(opacity.input).toHaveValue('0.5'); + + await opacity.fill('1'); + await expect(opacity.input).toHaveValue('1'); + }); + + test('should clamp opacity to max (1) when a value above the maximum is entered', async ({ + rightPanelPageObject, + }) => { + const { opacity } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await opacity.fill('500'); + await expect(opacity.input).toHaveValue('1'); + }); + + test('should clamp opacity to min (0) when a value below the minimum is entered', async ({ + rightPanelPageObject, + }) => { + const { opacity } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await opacity.fill('-1'); + await expect(opacity.input).toHaveValue('0'); + }); + + test('should reject non-numeric opacity input', async ({ rightPanelPageObject }) => { + const { opacity } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await expect(opacity.fill('abc')).rejects.toThrow(nonNumericError); + }); + }); + + test.describe('border', () => { + test('should accept valid values', async ({ rightPanelPageObject }) => { + const { border } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await border.fill('0'); + await expect(border.input).toHaveValue('0'); + + await border.fill('5'); + await expect(border.input).toHaveValue('5'); + + await border.fill('10'); + await expect(border.input).toHaveValue('10'); + }); + + test('should clamp border to max (10) when a value above the maximum is entered', async ({ + rightPanelPageObject, + }) => { + const { border } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await border.fill('500'); + await expect(border.input).toHaveValue('10'); + }); + + test('should clamp border to min (0) when a value below the minimum is entered', async ({ + rightPanelPageObject, + }) => { + const { border } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await border.fill('-1'); + await expect(border.input).toHaveValue('0'); + }); + + test('should reject non-numeric border input', async ({ rightPanelPageObject }) => { + const { border } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await expect(border.fill('abc')).rejects.toThrow(nonNumericError); + }); + }); + + test.describe('opacity inactive', () => { + test('should accept valid values', async ({ rightPanelPageObject }) => { + const { opacityInactive } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await opacityInactive.fill('0'); + await expect(opacityInactive.input).toHaveValue('0'); + + await opacityInactive.fill('0.5'); + await expect(opacityInactive.input).toHaveValue('0.5'); + + await opacityInactive.fill('1'); + await expect(opacityInactive.input).toHaveValue('1'); + }); + + test('should clamp opacity inactive to max (1) when a value above the maximum is entered', async ({ + rightPanelPageObject, + }) => { + const { opacityInactive } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await opacityInactive.fill('500'); + await expect(opacityInactive.input).toHaveValue('1'); + }); + + test('should clamp opacity inactive to min (0) when a value below the minimum is entered', async ({ + rightPanelPageObject, + }) => { + const { opacityInactive } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await opacityInactive.fill('-1'); + await expect(opacityInactive.input).toHaveValue('0'); + }); + + test('should reject non-numeric opacity inactive input', async ({ rightPanelPageObject }) => { + const { opacityInactive } = rightPanelPageObject.labelMapSegmentationPanel.config; + + await expect(opacityInactive.fill('abc')).rejects.toThrow(nonNumericError); + }); + }); +}); diff --git a/tests/pages/RightPanelPageObject.ts b/tests/pages/RightPanelPageObject.ts index 5926b5413..6b0715716 100644 --- a/tests/pages/RightPanelPageObject.ts +++ b/tests/pages/RightPanelPageObject.ts @@ -201,6 +201,51 @@ export class RightPanelPageObject { }; }, }, + + get config() { + const configToggle = page.getByTestId('segmentation-config-toggle-Labelmap'); + return { + toggle: { + locator: configToggle, + click: async () => { + await configToggle.click(); + }, + }, + + get opacity() { + const container = page.getByTestId('segmentation-config-opacity-Labelmap'); + return { + input: container.locator('input'), + slider: container.getByRole('slider'), + fill: async (value: string) => { + await container.locator('input').fill(value); + }, + }; + }, + + get border() { + const container = page.getByTestId('segmentation-config-border-Labelmap'); + return { + input: container.locator('input'), + slider: container.getByRole('slider'), + fill: async (value: string) => { + await container.locator('input').fill(value); + }, + }; + }, + + get opacityInactive() { + const container = page.getByTestId('segmentation-config-opacity-inactive-Labelmap'); + return { + input: container.locator('input'), + slider: container.getByRole('slider'), + fill: async (value: string) => { + await container.locator('input').fill(value); + }, + }; + }, + }; + }, }; }