chore(testing): Setup new self-hosted test runner (#6067)

This commit is contained in:
Joe Boccanfuso authored and GitHub committed 2026-06-18 11:21:41 -04:00
1 parent 6e2d53f280
commit 0dd6571505
8 files changed
+139 -49

No files matched your search

+7 -4
View File
@@ -24,10 +24,13 @@ test.describe('3D primary Test', async () => {
await attemptAction(() => reduce3DViewportSize(page), 10, 100);
await waitForViewportsRendered(page);
await checkForScreenshot(
await checkForScreenshot({
page,
viewportPageObject.grid,
screenShotPaths.threeDPrimary.threeDPrimaryDisplayedCorrectly
);
locator: viewportPageObject.grid,
// Volume-3D ray-cast output is GPU/driver-noisy run-to-run; match the
// tolerance already used by the sibling 3DOnly test.
maxDiffPixelRatio: 0.03,
screenshotPath: screenShotPaths.threeDPrimary.threeDPrimaryDisplayedCorrectly,
});
});
});
+78 -5
View File
@@ -1,11 +1,52 @@
import {
checkForScreenshot,
expect,
getAnnotationStats,
screenShotPaths,
test,
visitStudy,
waitForViewportRenderCycle,
} from './utils';
/**
* Asserts the arrow annotation text across every surface that should reflect it:
* - the tracked measurements side panel row title,
* - the DOM SVG linked text box rendered in the viewport, and
* - the source-of-truth cornerstone annotation state (`data.label`).
*
* This avoids relying on the screenshot alone (whose font rendering differs
* between systems) to assert the text is correct, mirroring the DOM/state
* assertions in the SCOORD rectangle test.
*/
async function expectArrowText({
page,
activeViewport,
rightPanelPageObject,
annotationUID,
expectedText,
}) {
// Side panel: the measurement row title reflects the arrow text.
await expect(rightPanelPageObject.measurementsPanel.panel.nthMeasurement(0).title).toHaveText(
expectedText
);
// DOM SVG: the arrow's linked text box renders the arrow text, independent of
// any system font differences.
const svgTextLines = activeViewport.getSvgAnnotationStatTextLines(annotationUID);
await expect(svgTextLines).toHaveCount(1);
await expect(svgTextLines.nth(0)).toHaveText(expectedText);
// Source-of-truth annotation state. ArrowAnnotate stores its text on
// `data.label` rather than computed `cachedStats`, so read with requireStats: false.
const arrows = await getAnnotationStats(page, {
toolName: 'ArrowAnnotate',
requireStats: false,
});
const arrow = arrows.find(a => a.annotationUID === annotationUID);
expect(arrow).toBeDefined();
expect(arrow.label).toBe(expectedText);
}
test.beforeEach(async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5';
const mode = 'viewer';
@@ -41,11 +82,28 @@ test('should display the arrow tool and allow free-form text to be entered', asy
await checkForScreenshot({
page,
maxDiffPixelRatio: 0.0075,
screenshotPath: screenShotPaths.arrowAnnotate.arrowAnnotateDisplayedCorrectly0,
});
// Now edit the arrow text and the label should not change.
// Resolve the arrow annotation UID once; it remains stable across subsequent edits.
const arrows = await getAnnotationStats(page, {
toolName: 'ArrowAnnotate',
requireStats: false,
});
expect(arrows.length).toBeGreaterThan(0);
const annotationUID = arrows[0].annotationUID;
await expectArrowText({
page,
activeViewport,
rightPanelPageObject,
annotationUID,
expectedText: 'Ringo Starr was the drummer for The Beatles',
});
// Double-clicking the arrow re-opens the text dialog. ArrowAnnotate stores its
// text on `data.label`, so the new text replaces it across the viewport SVG,
// the side panel and the annotation state.
await activeViewport.doubleClickAt({ x: 164, y: 234 });
@@ -53,11 +111,19 @@ test('should display the arrow tool and allow free-form text to be entered', asy
await checkForScreenshot({
page,
maxDiffPixelRatio: 0.0075,
screenshotPath: screenShotPaths.arrowAnnotate.arrowAnnotateDisplayedCorrectly1,
});
// Now edit the label and the text should not change.
await expectArrowText({
page,
activeViewport,
rightPanelPageObject,
annotationUID,
expectedText: 'Neil Peart was the drummer for Rush',
});
// Renaming from the side panel updates the same `data.label`, so the new text
// is reflected everywhere as well.
await rightPanelPageObject.measurementsPanel.panel
.nthMeasurement(0)
@@ -65,7 +131,14 @@ test('should display the arrow tool and allow free-form text to be entered', asy
await checkForScreenshot({
page,
maxDiffPixelRatio: 0.0075,
screenshotPath: screenShotPaths.arrowAnnotate.arrowAnnotateDisplayedCorrectly2,
});
await expectArrowText({
page,
activeViewport,
rightPanelPageObject,
annotationUID,
expectedText: 'Drummer annotation arrow',
});
});
@@ -24,7 +24,6 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
const dataOverlayPageObject = (await viewportPageObject.getById('default')).overlayMenu
.dataOverlay;
await dataOverlayPageObject.toggle();
const viewportRenderCycle = waitForViewportRenderCycle(page);
await dataOverlayPageObject.addSegmentation('ARIA RadOnc Structure Sets');
// Adding an overlay should not show the LOAD button.
@@ -32,8 +31,7 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
// Hide the overlay menu.
await dataOverlayPageObject.toggle();
await viewportRenderCycle;
await waitForViewportsRendered(page);
await checkForScreenshot(
page,
@@ -43,8 +41,8 @@ test('should launch MPR with unhydrated RTSTRUCT chosen from the data overlay me
await mainToolbarPageObject.layoutSelection.MPR.click();
await waitForViewportsRendered(page, { timeout: 40000 });
// Waiting for the render via waitForViewportRenderCycle appears to wait 'forever', so instead
// we use the baked in wait for screenshot comparisons.
await checkForScreenshot(
page,
viewportPageObject.grid,
+6 -13
View File
@@ -5,7 +5,6 @@ import {
screenShotPaths,
test,
visitStudy,
waitForPaintToSettle,
waitForViewportsRendered,
waitForViewportRenderCycle,
expect,
@@ -44,9 +43,6 @@ test.describe('3D four up SEG hydration', async () => {
await expect(DOMOverlayPageObject.viewport.segmentationHydration.locator).toBeVisible({
timeout: 60000,
});
await waitForViewportsRendered(page, { timeout: 60000 });
await page.waitForTimeout(3000);
await waitForPaintToSettle(page);
await checkForScreenshot(
page,
@@ -58,18 +54,12 @@ test.describe('3D four up SEG hydration', async () => {
// High rendered timeout needed: layout has 4 viewports (3D volume + MPR planes + SEG overlays),
// which can take significantly longer time to fully render
viewportRenderCycle = waitForViewportRenderCycle(page, { renderedTimeout: 180000 });
viewportRenderCycle = waitForViewportRenderCycle(page, { renderedTimeout: 240000 });
await DOMOverlayPageObject.viewport.segmentationHydration.yes.click();
// Wait until all viewports have finished rendering
await viewportRenderCycle;
// 3D volume rendering keeps streaming refined geometry after the load
// status flips; give the GPU a window to present the final frame before
// screenshotting.
await waitForViewportsRendered(page, { timeout: 180000 });
await page.waitForTimeout(3000);
await waitForPaintToSettle(page);
await checkForScreenshot({
page,
@@ -92,12 +82,15 @@ test.describe('3D four up to 3x2 layout SEG hydration', () => {
mainToolbarPageObject,
viewportPageObject,
}) => {
let viewportRenderCycle = waitForViewportRenderCycle(page, { renderedTimeout: 240000 });
await mainToolbarPageObject.layoutSelection.threeDFourUp.click();
await viewportRenderCycle;
let viewportRenderCycle = waitForViewportRenderCycle(page);
// Switch to a manual 3x2 grid layout
await mainToolbarPageObject.layoutSelection.grid(3, 2).click();
await viewportRenderCycle;
// Wait for the 3x2 grid to be rendered.
await expect(viewportPageObject.getNthLocator(5)).toBeVisible();
// Activate the 3rd viewport (index 2) then load the SEG into it
await viewportPageObject.getNthLocator(2).click();
Binary file not shown.

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 243 KiB

+18 -5
View File
@@ -5,6 +5,8 @@ type AnnotationStats = {
toolName: string;
referencedImageId?: string;
FrameOfReferenceUID?: string;
label?: string;
text?: string;
cachedStats: Record<string, TargetStats>;
targetIds: string[];
firstTargetStats: TargetStats | undefined;
@@ -14,10 +16,17 @@ type Options = {
toolName?: string | string[];
attempts?: number;
intervalMs?: number;
/**
* When true (default), only resolves once at least one matching annotation has
* populated `cachedStats`. Set to false for tools whose source of truth is
* `data.label`/`data.text` rather than computed stats (e.g. `ArrowAnnotate`),
* in which case any matching annotation is returned.
*/
requireStats?: boolean;
};
const getAnnotationStats = async (page, options: Options = {}): Promise<AnnotationStats[]> => {
const { toolName, attempts = 20, intervalMs = 500 } = options;
const { toolName, attempts = 20, intervalMs = 500, requireStats = true } = options;
const toolNames =
toolName === undefined ? undefined : Array.isArray(toolName) ? toolName : [toolName];
@@ -38,6 +47,8 @@ const getAnnotationStats = async (page, options: Options = {}): Promise<Annotati
toolName: a.metadata?.toolName,
referencedImageId: a.metadata?.referencedImageId,
FrameOfReferenceUID: a.metadata?.FrameOfReferenceUID,
label: a?.data?.label,
text: a?.data?.text,
cachedStats,
targetIds,
firstTargetStats: targetIds.length ? cachedStats[targetIds[0]] : undefined,
@@ -47,16 +58,18 @@ const getAnnotationStats = async (page, options: Options = {}): Promise<Annotati
{ toolNames }
);
const populated = results.filter(r => r.targetIds.length > 0);
if (populated.length > 0) {
return populated;
const matching = requireStats ? results.filter(r => r.targetIds.length > 0) : results;
if (matching.length > 0) {
return matching;
}
await new Promise(resolve => setTimeout(resolve, intervalMs));
}
throw new Error(
`No annotations${toolNames ? ` for tool(s) ${toolNames.join(',')}` : ''} with populated cachedStats found`
`No annotations${toolNames ? ` for tool(s) ${toolNames.join(',')}` : ''}${
requireStats ? ' with populated cachedStats' : ''
} found`
);
};