fix(seg hydration): auto-hydrate RT struct on second load with disableConfirmationPrompts (#5875)

This commit is contained in:
Ghadeer Albattarni 2026-03-09 09:08:21 -04:00 committed by GitHub
parent 0f822aaf48
commit 6f773f9972
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 166 additions and 17 deletions

View File

@ -94,22 +94,30 @@ function promptHydrationDialog({
if (type === HydrationType.SEG) {
// SEG needs setTimeout
window.setTimeout(async () => {
const isHydrated = await hydrateCallback({
segDisplaySet: displaySet,
viewportId,
});
resolve(isHydrated);
try {
const isHydrated = await hydrateCallback({
segDisplaySet: displaySet,
viewportId,
});
resolve(isHydrated);
} catch (error) {
reject(error);
}
}, 0);
} else if (type === HydrationType.RTSTRUCT) {
// RT hydration
const isHydrated = await hydrateCallback({
rtDisplaySet: displaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
window.setTimeout(async () => {
try {
const isHydrated = await hydrateCallback({
rtDisplaySet: displaySet,
viewportId,
servicesManager,
});
resolve(isHydrated);
} catch (error) {
reject(error);
}
}, 0);
} else if (type === HydrationType.SR) {
// SR has a different result structure
const hydrationResult = await hydrateCallback(displaySet);

View File

@ -32,12 +32,19 @@ const SegmentationCollapsedHeader = ({ children }: { children: React.ReactNode }
// Dropdown menu component - specifically for dropdown menu content
const SegmentationCollapsedDropdownMenu = ({ children }: { children: React.ReactNode }) => {
const { segmentationRepresentationTypes } = useSegmentationTableContext(
'SegmentationCollapsedDropdownMenu'
);
const dataCyTypeSuffix = segmentationRepresentationTypes?.[0]
? `-${segmentationRepresentationTypes[0]}`
: '';
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="icon"
data-cy={`segmentation-collapsed-more-btn${dataCyTypeSuffix}`}
>
<Icons.More className="h-6 w-6" />
</Button>

View File

@ -0,0 +1,81 @@
import {
checkForScreenshot,
expect,
screenShotPaths,
test,
visitStudy,
addOHIFConfiguration,
} from './utils';
import { press } from './utils/keyboardUtils';
test.beforeEach(async ({ page }) => {
await addOHIFConfiguration(page, {
disableConfirmationPrompts: true,
});
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
const mode = 'viewer';
await visitStudy(page, studyInstanceUID, mode, 2000);
});
test('should auto hydrate RT STRUCT on the second load and keep viewport stable after deleting segmentations', async ({
page,
DOMOverlayPageObject,
leftPanelPageObject,
rightPanelPageObject,
viewportPageObject,
}) => {
// First load
await leftPanelPageObject.loadSeriesByModality('RTSTRUCT');
await page.waitForTimeout(5000);
const loadBadgeCountAfterFirstLoad =
await DOMOverlayPageObject.viewport.getModalityLoadBadgeCount();
expect(loadBadgeCountAfterFirstLoad).toBe(0);
await press({ page, key: 'ArrowDown', nTimes: 12 });
await checkForScreenshot(
page,
viewportPageObject.active.pane,
screenShotPaths.rtHydrationDisableConfirmation.firstLoadPostHydration
);
await rightPanelPageObject.toggle();
await rightPanelPageObject.noToolsSegmentationPanel.panel.moreMenu.delete();
await page.waitForTimeout(2000);
await checkForScreenshot(
page,
viewportPageObject.active.pane,
screenShotPaths.rtHydrationDisableConfirmation.viewportAfterFirstDelete
);
// Second load
await leftPanelPageObject.loadSeriesByModality('RTSTRUCT');
await page.waitForTimeout(5000);
const loadBadgeCountAfterSecondLoad =
await DOMOverlayPageObject.viewport.getModalityLoadBadgeCount();
expect(loadBadgeCountAfterSecondLoad).toBe(0);
await press({ page, key: 'ArrowDown', nTimes: 12 });
await checkForScreenshot(
page,
viewportPageObject.active.pane,
screenShotPaths.rtHydrationDisableConfirmation.secondLoadPostHydration
);
await rightPanelPageObject.noToolsSegmentationPanel.panel.moreMenu.delete();
await page.waitForTimeout(2000);
await checkForScreenshot(
page,
viewportPageObject.active.pane,
screenShotPaths.rtHydrationDisableConfirmation.viewportAfterSecondDelete
);
});

View File

@ -11,6 +11,30 @@ export class RightPanelPageObject {
this.DOMOverlayPageObject = new DOMOverlayPageObject(page);
}
private getCollapsedMoreMenu(typeSuffix?: string) {
const page = this.page;
const testId = typeSuffix
? `segmentation-collapsed-more-btn-${typeSuffix}`
: 'segmentation-collapsed-more-btn';
const button = page.getByTestId(testId);
return {
button,
click: async () => {
await button.click();
},
delete: async () => {
await button.click();
await page.getByRole('menuitem', { name: 'Delete' }).click();
},
rename: async (text: string) => {
await button.click();
await page.getByRole('menuitem', { name: 'Rename' }).click();
await this.DOMOverlayPageObject.dialog.input.fillAndSave(text);
},
};
}
private getActionsMenu(row: Locator) {
const actionsButton = row.getByTestId('actionsMenuTrigger');
@ -112,12 +136,14 @@ export class RightPanelPageObject {
};
}
private get segmentationPanel() {
private getSegmentationPanel(typeSuffix?: string) {
const page = this.page;
const getSegmentByIdx = (index: number) => this.getPanelRowByIdx(index);
const getSegmentByText = (text: string) => this.getPanelRowByText(text);
const moreMenu = this.getCollapsedMoreMenu(typeSuffix);
return {
moreMenu,
getSegmentCount: async () => {
return await page.getByTestId('data-row').count();
},
@ -135,7 +161,7 @@ export class RightPanelPageObject {
get contourSegmentationPanel() {
const page = this.page;
const addSegmentationButton = this.addSegmentationButton;
const panel = this.segmentationPanel;
const panel = this.getSegmentationPanel('Contour');
const menuButton = page.getByTestId('panelSegmentationWithToolsContour-btn');
return {
@ -150,7 +176,7 @@ export class RightPanelPageObject {
get labelMapSegmentationPanel() {
const page = this.page;
const addSegmentationButton = this.addSegmentationButton;
const panel = this.segmentationPanel;
const panel = this.getSegmentationPanel('Labelmap');
const menuButton = page.getByTestId('panelSegmentationWithToolsLabelMap-btn');
return {
@ -264,7 +290,7 @@ export class RightPanelPageObject {
get noToolsSegmentationPanel() {
const page = this.page;
const panel = this.segmentationPanel;
const panel = this.getSegmentationPanel();
const menuButton = page.getByTestId(/^panelSegmentation.*-btn$/).first();
return {

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,19 @@
import { Page } from 'playwright-test-coverage';
export async function addOHIFConfiguration(page: Page, configToAdd: Record<string, unknown>) {
await page.addInitScript(config => {
let _config;
Object.defineProperty(window, 'config', {
get() {
return _config;
},
set(value) {
_config = {
...value,
...config,
};
},
configurable: true,
});
}, configToAdd);
}

View File

@ -1,4 +1,5 @@
import { visitStudy } from './visitStudy';
import { addOHIFConfiguration } from './OHIFConfiguration';
import { checkForScreenshot } from './checkForScreenshot';
import { screenShotPaths } from './screenShotPaths';
import {
@ -23,6 +24,7 @@ import { subscribeToMeasurementAdded } from './subscribeToMeasurement';
export {
visitStudy,
addOHIFConfiguration,
checkForScreenshot,
screenShotPaths,
simulateClicksOnElement,

View File

@ -177,6 +177,12 @@ const screenShotPaths = {
rtPostHydration: 'rtPostHydration.png',
rtPreHydration: 'rtPreHydration.png',
},
rtHydrationDisableConfirmation: {
firstLoadPostHydration: 'firstLoadPostHydration.png',
viewportAfterFirstDelete: 'viewportAfterFirstDelete.png',
secondLoadPostHydration: 'secondLoadPostHydration.png',
viewportAfterSecondDelete: 'viewportAfterSecondDelete.png',
},
crosshairs: {
crosshairsRendered: 'crosshairsRendered.png',
crosshairsRotated: 'crosshairsRotated.png',