ohif-viewer/tests/utils/waitForViewportsRendered.ts
Alireza 6dd150d401
fix: ohif tests to run with cornerstone 3d 5.0 (#6043)
* chore(tests): Update multiple screenshot test images for various specs

* feat(screenshot-reviewer): Add screenshot review tool and update package.json scripts

* fix(DICOMSRDisplayTool): Improve actor presence check in viewport

* chore(tests): Update multiple screenshot assets for various specs

* chore(tests): Integrate waitForPaintToSettle and waitForViewportsRendered in multiple specs for improved rendering stability

* chore(tests): Update screenshot assets for SEGHydration and SEGNoHydration specs

* test: update progressive loading screenshots

* jest 30 test fixes for compatibility with pnpm cs3d

* Use correct setDisplaySets instead of setDataId

* fix: Naming change for LegacyVolumeViewport3D

* Update to allow tolerance for contour tests

* update

* fix

* refactor: Replace instanceof checks with utility functions for viewport type validation

* fix: Update createSegmentationForViewport to handle undefined displaySetInstanceUID gracefully

* bun lock

* fix: Install cs3d with pnpm instead of bun

* Update node version for playwright

* Update to v5.0.0 of cs3d

* fix: Build dependency

* audit

* Change to a web await retry assert

* Fix timing related test failures

* fix: Freehand close

---------

Co-authored-by: Bill Wallace <wayfarer3130@gmail.com>
2026-06-09 20:25:14 -04:00

218 lines
6.1 KiB
TypeScript

import type { Page } from '@playwright/test';
type WaitForAnyViewportNeedsRenderOptions = {
timeout?: number;
};
type WaitForViewportsRenderedOptions = {
timeout?: number;
/**
* If true (default), also waits for any volume actors referenced by the
* viewports to report loaded.
*/
waitVolumeLoad?: boolean;
/**
* If true (default), inserts a post-rendered "settle" step that waits two
* animation frames and a short idle window so the GPU has presented the
* final paint before screenshots are taken.
*/
settle?: boolean;
};
type WaitForRenderCycleToCompleteOptions = {
/**
* Timeout for waiting until at least one viewport reaches `needsRender`.
*/
needsRenderTimeout?: number;
/**
* Timeout for waiting until all viewports are `rendered`
* (and optionally volume-loaded).
*/
renderedTimeout?: number;
/**
* If true (default), also waits for any volume actors referenced by the
* viewports to report loaded during the rendered phase.
*/
waitVolumeLoad?: boolean;
/**
* If true (default), inserts a post-rendered "settle" step before resolving.
*/
settle?: boolean;
};
/**
* Waits for a full render cycle:
* 1) any viewport requests render (`needsRender`)
* 2) all viewports finish rendering (`rendered`)
*/
const waitForViewportRenderCycle = async (
page: Page,
options: WaitForRenderCycleToCompleteOptions = {}
) => {
const {
needsRenderTimeout = 5000,
renderedTimeout = 15000,
waitVolumeLoad = true,
settle = true,
} = options;
await waitForAnyViewportNeedsRender(page, { timeout: needsRenderTimeout });
await waitForViewportsRendered(page, { timeout: renderedTimeout, waitVolumeLoad, settle });
};
/**
* Waits until at least one viewport enters the 'needsRender' state, indicating
* that a render has been requested but not yet started.
*/
const waitForAnyViewportNeedsRender = async (
page: Page,
options: WaitForAnyViewportNeedsRenderOptions = {}
) => {
const { timeout = 5000 } = options;
await page.waitForFunction(
() => {
const cornerstone = (window as any).cornerstone;
if (!cornerstone?.getRenderingEngines) {
return false;
}
const renderingEngines = cornerstone.getRenderingEngines();
const viewports = renderingEngines.flatMap(engine =>
engine.getViewports ? engine.getViewports() : []
);
if (!viewports.length) {
return false;
}
const needsRender = viewports.some(viewport => viewport?.viewportStatus === 'needsRender');
return needsRender;
},
{},
{ timeout }
);
};
/**
* Stabilize tests by waiting for a short tick, network idle, then viewport render completion.
* To use this method safely, you may need to make changes to OHIF and/or CS3D
* methods handling clicks (SHOULD be commands modules only). These should set the
* state to needs render synchronously so that this method can safely wait for the render to complete.
* Examples such as changing the hanging protocol currently don't set such a state
* and thus can't be rendered without a delay.
*
* If options.waitVolumeLoad is not false, then this method will wait for all volumes
* associated with viewports to be loaded.
*/
const waitForViewportsRendered = async (
page: Page,
options: WaitForViewportsRenderedOptions = {}
) => {
const { timeout = 15000, waitVolumeLoad = true, settle = true } = options;
await page.waitForFunction(
({ waitVolumeLoad }) => {
const cornerstone = (window as any).cornerstone;
if (!cornerstone?.getRenderingEngines) {
return false;
}
const renderingEngines = cornerstone.getRenderingEngines();
const viewports = renderingEngines.flatMap(engine =>
engine.getViewports ? engine.getViewports() : []
);
if (!viewports.length) {
return false;
}
const allRendered = viewports.every(viewport => viewport?.viewportStatus === 'rendered');
if (!allRendered) {
return false;
}
if (!waitVolumeLoad) {
return true;
}
const cache = cornerstone.cache;
if (!cache?.getVolume) {
return true;
}
const actorEntries = viewports.flatMap(viewport =>
viewport?.getActors ? viewport.getActors() : []
);
for (const actorEntry of actorEntries) {
const id = actorEntry?.referencedId || actorEntry?.uid;
if (!id) {
continue;
}
let volume: any;
try {
volume = cache.getVolume(id);
} catch {
continue;
}
const loaded =
volume?.loadStatus && typeof volume.loadStatus.loaded === 'boolean'
? volume.loadStatus.loaded
: true;
if (!loaded) {
return false;
}
}
return true;
},
{ waitVolumeLoad },
{ timeout }
);
if (settle) {
await waitForPaintToSettle(page);
}
};
/**
* After the render-cycle predicate has resolved, give the browser two animation
* frames (RAF fires *before* the paint commits, so we wait for two consecutive
* frames plus a microtask) followed by a short idle window. This drains the
* tail of any progressive texture uploads that landed in the same tick the
* `loadStatus.loaded` flag was flipped, so screenshots capture the final paint
* rather than a mid-stream frame. Mirrors the `scheduleAfterPaint` deferral in
* BaseStreamingImageVolume on the cs3d side.
*/
const waitForPaintToSettle = async (page: Page) => {
await page.evaluate(
() =>
new Promise<void>(resolve => {
const raf = (cb: FrameRequestCallback) =>
typeof requestAnimationFrame === 'function'
? requestAnimationFrame(cb)
: (setTimeout(cb, 16) as unknown as number);
raf(() =>
raf(() => {
Promise.resolve().then(resolve);
})
);
})
);
// Final guard against stragglers (e.g. label-outline edge detection that
// queues an extra render via requestAnimationFrame from inside an event
// handler).
await page.waitForTimeout(150);
};
export {
waitForViewportsRendered,
waitForAnyViewportNeedsRender,
waitForViewportRenderCycle,
waitForPaintToSettle,
};