fix(segmentation): Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. (#5503)
* fix(segmentation): Lock all rehydrated segmentation segments when panelSegmentation.disableEditing is true. * PR feedback. * PR feedback. * Added tests for disableEditing and utilities for simulating drag operations. * Update bun.lock file after merging master.
This commit is contained in:
1 parent
3e1d50c8d2
commit
170e860aa1
23 files changed
+625
-242
No files matched your search
@@ -0,0 +1,144 @@
|
||||
import { test, expect } from 'playwright-test-coverage';
|
||||
import { visitStudy } from './utils';
|
||||
import { viewportSVGPathLocator } from './utils/locators';
|
||||
import { simulateNormalizedDragOnElement } from './utils/simulateDragOnElement';
|
||||
|
||||
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
|
||||
|
||||
test('should not allow contours to be edited in basic viewer mode', async ({ page }) => {
|
||||
const mode = 'viewer';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
|
||||
await page.getByTestId('side-panel-header-right').click();
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
|
||||
// Wait for the segmentation to hydrate.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const svgPathLocatorPreEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
|
||||
|
||||
expect(
|
||||
await svgPathLocatorPreEdit.count(),
|
||||
'Expected exactly 1 path element in the viewport'
|
||||
).toBe(1);
|
||||
|
||||
const expectedPathCommands = await svgPathLocatorPreEdit.getAttribute('d');
|
||||
|
||||
// Try to drag one of the edges of the rectangular contour.
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: svgPathLocatorPreEdit,
|
||||
start: { x: 0.1, y: 0 },
|
||||
end: { x: 0.1, y: -0.2 },
|
||||
});
|
||||
|
||||
const svgPathLocatorPostEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
|
||||
|
||||
expect(
|
||||
await svgPathLocatorPostEdit.getAttribute('d'),
|
||||
'Expected the path commands to be the same as the pre-edit path commands'
|
||||
).toBe(expectedPathCommands);
|
||||
});
|
||||
|
||||
test('should not allow contours to be edited when panelSegmentation.disableEditing is true', async ({
|
||||
page,
|
||||
}) => {
|
||||
const mode = 'segmentation';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
|
||||
await page.getByTestId('side-panel-header-right').click();
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// disable editing of segmentations via the customization service
|
||||
await page.evaluate(() => {
|
||||
window.services.customizationService.setGlobalCustomization(
|
||||
'panelSegmentation.disableEditing',
|
||||
{
|
||||
$set: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
|
||||
// Wait for the segmentation to hydrate.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const svgPathLocatorPreEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
|
||||
|
||||
expect(
|
||||
await svgPathLocatorPreEdit.count(),
|
||||
'Expected exactly 1 path element in the viewport'
|
||||
).toBe(1);
|
||||
|
||||
const expectedPathCommands = await svgPathLocatorPreEdit.getAttribute('d');
|
||||
|
||||
// Try to drag one of the edges of the rectangular contour.
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: svgPathLocatorPreEdit,
|
||||
start: { x: 0.1, y: 0 },
|
||||
end: { x: 0.1, y: -0.2 },
|
||||
});
|
||||
|
||||
const svgPathLocatorPostEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
|
||||
|
||||
expect(
|
||||
await svgPathLocatorPostEdit.getAttribute('d'),
|
||||
'Expected the path commands to be the same as the pre-edit path commands'
|
||||
).toBe(expectedPathCommands);
|
||||
});
|
||||
|
||||
test('should allow contours to be edited when panelSegmentation.disableEditing is false', async ({
|
||||
page,
|
||||
}) => {
|
||||
const mode = 'segmentation';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
|
||||
await page.getByTestId('side-panel-header-right').click();
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// disable editing of segmentations via the customization service
|
||||
await page.evaluate(() => {
|
||||
window.services.customizationService.setGlobalCustomization(
|
||||
'panelSegmentation.disableEditing',
|
||||
{
|
||||
$set: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
|
||||
// Wait for the segmentation to hydrate.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
const svgPathLocatorPreEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
|
||||
|
||||
expect(
|
||||
await svgPathLocatorPreEdit.count(),
|
||||
'Expected exactly 1 path element in the viewport'
|
||||
).toBe(1);
|
||||
|
||||
const preEditPathCommands = await svgPathLocatorPreEdit.getAttribute('d');
|
||||
|
||||
// Try to drag one of the edges of the rectangular contour.
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: svgPathLocatorPreEdit,
|
||||
start: { x: 0.1, y: 0 },
|
||||
end: { x: 0.1, y: -0.2 },
|
||||
});
|
||||
|
||||
const svgPathLocatorPostEdit = viewportSVGPathLocator({ page, viewportId: 'default' });
|
||||
|
||||
expect(
|
||||
await svgPathLocatorPostEdit.getAttribute('d'),
|
||||
'Not expecting the path commands to be the same as the pre-edit path commands'
|
||||
).not.toBe(preEditPathCommands);
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import { test } from 'playwright-test-coverage';
|
||||
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils';
|
||||
import { press } from './utils/keyboardUtils';
|
||||
import { simulateNormalizedDragOnElement } from './utils/simulateDragOnElement';
|
||||
import { viewportLocator } from './utils/locators';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.256467663913010332776401703474716742458';
|
||||
const mode = 'segmentation';
|
||||
await visitStudy(page, studyInstanceUID, mode, 2000);
|
||||
});
|
||||
|
||||
test('should prevent editing of label map segmentations when panelSegmentation.disableEditing is true', async ({
|
||||
page,
|
||||
}) => {
|
||||
// disable editing of segmentations via the customization service
|
||||
await page.evaluate(() => {
|
||||
window.services.customizationService.setGlobalCustomization(
|
||||
'panelSegmentation.disableEditing',
|
||||
{
|
||||
$set: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
|
||||
// Wait for the segmentation to hydrate.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// navigate to the 12th image and ensure the correct overlay is displayed
|
||||
await press({ page, key: 'ArrowDown', nTimes: 11 });
|
||||
|
||||
await checkForScreenshot(page, page, screenShotPaths.labelMapSegLocking.globalLockedSegPreEdit);
|
||||
|
||||
// Attempt to erase the segmentations.
|
||||
await page.getByTestId('Eraser-btn').click();
|
||||
|
||||
// Use the largest eraser radius to help ensure the entire image is erased.
|
||||
await page.locator(`css=div[data-cy="eraser-radius"] input`).fill('1000');
|
||||
|
||||
// Attempt to erase the segmentations by dragging the eraser tool across the image several times.
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: viewportLocator({ page, viewportId: 'default' }),
|
||||
start: { x: 0.01, y: 0.25 },
|
||||
end: { x: 1.0, y: 0.25 },
|
||||
});
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: viewportLocator({ page, viewportId: 'default' }),
|
||||
start: { x: 0.01, y: 0.5 },
|
||||
end: { x: 1.0, y: 0.5 },
|
||||
});
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: viewportLocator({ page, viewportId: 'default' }),
|
||||
start: { x: 0.01, y: 0.75 },
|
||||
end: { x: 1.0, y: 0.75 },
|
||||
});
|
||||
|
||||
await checkForScreenshot(page, page, screenShotPaths.labelMapSegLocking.globalLockedSegPostEdit);
|
||||
});
|
||||
|
||||
test('should allow editing of label map segmentations when panelSegmentation.disableEditing is false', async ({
|
||||
page,
|
||||
}) => {
|
||||
// disable editing of segmentations via the customization service
|
||||
await page.evaluate(() => {
|
||||
window.services.customizationService.setGlobalCustomization(
|
||||
'panelSegmentation.disableEditing',
|
||||
{
|
||||
$set: false,
|
||||
}
|
||||
);
|
||||
});
|
||||
await page.getByTestId('study-browser-thumbnail-no-image').dblclick();
|
||||
// Wait for the segmentation to be loaded.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
await page.getByTestId('yes-hydrate-btn').click();
|
||||
// Wait for the segmentation to hydrate.
|
||||
await page.waitForTimeout(5000);
|
||||
|
||||
// navigate to the 12th image and ensure the correct overlay is displayed
|
||||
await press({ page, key: 'ArrowDown', nTimes: 11 });
|
||||
|
||||
await checkForScreenshot(page, page, screenShotPaths.labelMapSegLocking.globalUnlockedSegPreEdit);
|
||||
|
||||
// Attempt to erase the segmentations.
|
||||
await page.getByTestId('Eraser-btn').click();
|
||||
|
||||
// Use the largest eraser radius to help ensure the eraser passes over the entire image.
|
||||
await page.locator(`css=div[data-cy="eraser-radius"] input`).fill('1000');
|
||||
|
||||
// Attempt to erase the segmentations by dragging the eraser tool across the image several times.
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: viewportLocator({ page, viewportId: 'default' }),
|
||||
start: { x: 0.01, y: 0.25 },
|
||||
end: { x: 1.0, y: 0.25 },
|
||||
});
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: viewportLocator({ page, viewportId: 'default' }),
|
||||
start: { x: 0.01, y: 0.5 },
|
||||
end: { x: 1.0, y: 0.5 },
|
||||
});
|
||||
await simulateNormalizedDragOnElement({
|
||||
locator: viewportLocator({ page, viewportId: 'default' }),
|
||||
start: { x: 0.01, y: 0.75 },
|
||||
end: { x: 1.0, y: 0.75 },
|
||||
});
|
||||
|
||||
await checkForScreenshot(
|
||||
page,
|
||||
page,
|
||||
screenShotPaths.labelMapSegLocking.globalUnlockedSegPostEdit
|
||||
);
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 304 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 297 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 282 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 299 KiB |
@@ -9,3 +9,23 @@ export const viewportLocator = ({
|
||||
}): Locator => {
|
||||
return page.locator(`css=div[data-viewportid="${viewportId}"]`);
|
||||
};
|
||||
|
||||
export const viewportSVGLocator = ({
|
||||
page,
|
||||
viewportId,
|
||||
}: {
|
||||
page: Page;
|
||||
viewportId: string;
|
||||
}): Locator => {
|
||||
return page.locator(`css=div[data-viewportid="${viewportId}"] svg`);
|
||||
};
|
||||
|
||||
export const viewportSVGPathLocator = ({
|
||||
page,
|
||||
viewportId,
|
||||
}: {
|
||||
page: Page;
|
||||
viewportId: string;
|
||||
}): Locator => {
|
||||
return page.locator(`css=div[data-viewportid="${viewportId}"] svg path`);
|
||||
};
|
||||
@@ -26,6 +26,12 @@ const screenShotPaths = {
|
||||
ellipse: {
|
||||
ellipseDisplayedCorrectly: 'ellipseDisplayedCorrectly.png',
|
||||
},
|
||||
labelMapSegLocking: {
|
||||
globalLockedSegPreEdit: 'lockedSegPreEdit.png',
|
||||
globalLockedSegPostEdit: 'lockedSegPostEdit.png',
|
||||
globalUnlockedSegPreEdit: 'unlockedSegPreEdit.png',
|
||||
globalUnlockedSegPostEdit: 'unlockedSegPostEdit.png',
|
||||
},
|
||||
length: {
|
||||
lengthDisplayedCorrectly: 'lengthDisplayedCorrectly.png',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Locator } from 'playwright';
|
||||
|
||||
/**
|
||||
* Simulates a drag operation on an element using normalized coordinates.
|
||||
*
|
||||
* @param locator - The locator to drag on.
|
||||
* @param start - The start point with x and y coordinates, normalized to the element's bounding box.
|
||||
* @param end - The end point with x and y coordinates, normalized to the element's bounding box.
|
||||
* @param button - The mouse button to use for the drag (default is 'left').
|
||||
* @param delay - Milliseconds between steps (default is 50).
|
||||
* @param steps - Number of intermediate positions for smooth dragging (default is 5).
|
||||
*/
|
||||
export async function simulateNormalizedDragOnElement({
|
||||
locator,
|
||||
start,
|
||||
end,
|
||||
button = 'left',
|
||||
delay = 50,
|
||||
steps = 10,
|
||||
}: {
|
||||
locator: Locator;
|
||||
start: { x: number; y: number };
|
||||
end: { x: number; y: number };
|
||||
button?: 'left' | 'right' | 'middle';
|
||||
delay?: number;
|
||||
steps?: number;
|
||||
}) {
|
||||
const bBox = await locator.boundingBox();
|
||||
if (!bBox) {
|
||||
throw new Error('Element bounding box not found');
|
||||
}
|
||||
|
||||
const startPosition = {
|
||||
x: bBox.x + start.x * bBox.width,
|
||||
y: bBox.y + start.y * bBox.height,
|
||||
};
|
||||
const endPosition = {
|
||||
x: bBox.x + end.x * bBox.width,
|
||||
y: bBox.y + end.y * bBox.height,
|
||||
};
|
||||
|
||||
// Get the page from the locator context
|
||||
const page = locator.page();
|
||||
|
||||
page.mouse.move(startPosition.x, startPosition.y);
|
||||
|
||||
// Start drag
|
||||
await page.mouse.down({ button });
|
||||
|
||||
// Calculate intermediate positions for smooth dragging
|
||||
const deltaX = (endPosition.x - startPosition.x) / steps;
|
||||
const deltaY = (endPosition.y - startPosition.y) / steps;
|
||||
|
||||
// Move through intermediate positions
|
||||
for (let i = 1; i <= steps; i++) {
|
||||
const currentX = startPosition.x + deltaX * i;
|
||||
const currentY = startPosition.y + deltaY * i;
|
||||
// Add delay if specified
|
||||
if (delay > 0) {
|
||||
await page.waitForTimeout(delay);
|
||||
}
|
||||
await page.mouse.move(currentX, currentY);
|
||||
}
|
||||
|
||||
// End drag
|
||||
await page.mouse.up({ button });
|
||||
}
|
||||
Reference in new issue
Block a user