Test: Playwright tests for tmtv (#4331)

Co-authored-by: Ibrahim <93064150+IbrahimCSAE@users.noreply.github.com>
Co-authored-by: Ibrahim <ibrahim.mdev@gmail.com>
This commit is contained in:
dxlin 2024-08-15 21:57:04 -04:00 committed by GitHub
parent 4068488bb2
commit b2c0faf29d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 294 additions and 13 deletions

View File

@ -128,8 +128,8 @@ declare global {
displaySetsCount: number;
maxNumPrefetchRequests: number;
order: 'closest' | 'downward' | 'upward';
}
}
};
}
export interface Test {
services?: Services;
@ -144,4 +144,6 @@ declare global {
AppTypes.Managers & {
[key: string]: any;
};
export type withTestTypes<T = object> = T & AppTypes.Test;
}

View File

@ -0,0 +1,54 @@
import { test, expect } from '@playwright/test';
import { visitStudy, scrollVolumeViewport } from './utils';
test('PT should show slice closest to CT', async ({ page }) => {
const studyInstanceUID = '1.2.840.113619.2.290.3.3767434740.226.1600859119.501';
const mode = 'Total Metabolic Tumor Volume';
await visitStudy(page, studyInstanceUID, mode);
const vp = page.getByTestId('viewport-pane');
// Sagittal
await expect(vp.nth(1)).toContainText('257/512', { useInnerText: true }); // Should default i 257
await expect.soft(vp.nth(4)).toContainText('97/192');
await scrollVolumeViewport(page, 'ctSAGITTAL', -1); // CT i 256
await expect(vp.nth(1)).toContainText('256/512');
await expect.soft(vp.nth(4)).toContainText('96/192');
await scrollVolumeViewport(page, 'ctSAGITTAL', -1); // CT i 255
await expect(vp.nth(1)).toContainText('255/512');
await expect.soft(vp.nth(4)).toContainText('95/192');
await scrollVolumeViewport(page, 'ctSAGITTAL', -1); // CT i 254
await expect(vp.nth(1)).toContainText('254/512');
await expect.soft(vp.nth(4)).toContainText('95/192');
await scrollVolumeViewport(page, 'ctSAGITTAL', -1); // CT i 253
await expect(vp.nth(1)).toContainText('253/512');
await expect.soft(vp.nth(4)).toContainText('94/192');
await scrollVolumeViewport(page, 'ctSAGITTAL', -1); // CT i 252
await expect(vp.nth(1)).toContainText('252/512');
await expect.soft(vp.nth(4)).toContainText('94/192');
await scrollVolumeViewport(page, 'ctSAGITTAL', -1); // CT i 251
await expect(vp.nth(1)).toContainText('251/512');
await expect.soft(vp.nth(4)).toContainText('93/192');
// Coronal
await expect(vp.nth(2)).toContainText('256/512'); // Should default i 256
await expect.soft(vp.nth(5)).toContainText('96/192');
await scrollVolumeViewport(page, 'ctCORONAL', -1); // CT i 255
await expect(vp.nth(2)).toContainText('255/512');
await expect.soft(vp.nth(5)).toContainText('96/192');
await scrollVolumeViewport(page, 'ctCORONAL', -1); // CT i 254
await expect(vp.nth(2)).toContainText('254/512');
await expect.soft(vp.nth(5)).toContainText('95/192');
await scrollVolumeViewport(page, 'ctCORONAL', -1); // CT i 253
await expect(vp.nth(2)).toContainText('253/512');
await expect.soft(vp.nth(5)).toContainText('95/192');
await scrollVolumeViewport(page, 'ctCORONAL', -1); // CT i 252
await expect(vp.nth(2)).toContainText('252/512');
await expect.soft(vp.nth(5)).toContainText('94/192');
await scrollVolumeViewport(page, 'ctCORONAL', -1); // CT i 251
await expect(vp.nth(2)).toContainText('251/512');
await expect.soft(vp.nth(5)).toContainText('94/192');
await scrollVolumeViewport(page, 'ctCORONAL', -1); // CT i 250
await expect(vp.nth(2)).toContainText('250/512');
await expect.soft(vp.nth(5)).toContainText('93/192');
});

View File

@ -0,0 +1,49 @@
import { test, expect } from '@playwright/test';
import {
visitStudy,
simulateClicksOnElement,
getTMTVModalityUnit,
clearAllAnnotations,
} from './utils/index';
test('pets where SUV cannot be calculated should show same unit in TMTV as in Basic Viewer.', async ({
page,
}) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339';
const mode = 'Total Metabolic Tumor Volume';
await visitStudy(page, studyInstanceUID, mode, 10000);
// Change to image where SUV cannot be calculated
await page.getByTestId('side-panel-header-left').click();
await page
.getByRole('button', { name: 'S: 2 311 PET NAC' })
.dragTo(page.getByTestId('viewport-grid').locator('canvas').nth(3));
// Wait for the new series to load
await page.waitForLoadState('networkidle');
// Add ROI annotation
await page.getByTestId('MeasurementTools-split-button-secondary').click();
await page.getByTestId('EllipticalROI').click();
const locator = page.getByTestId('viewport-pane').locator('canvas').first();
await clearAllAnnotations(page);
await simulateClicksOnElement({
locator,
points: [
{
x: 100,
y: 100,
},
{
x: 150,
y: 150,
},
],
});
const modalityUnit = await getTMTVModalityUnit(page);
// in basic viewer, when you convert to volume, the unit is raw not PROPCNT (tmtv starts as a volume)
expect(modalityUnit).toEqual('raw');
});

View File

@ -0,0 +1,76 @@
import { expect, test } from '@playwright/test';
import { visitStudy, simulateClicksOnElement, getSUV, clearAllAnnotations } from './utils/index';
test('should update SUV values correctly.', async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339';
const mode = 'Total Metabolic Tumor Volume';
await visitStudy(page, studyInstanceUID, mode, 10000);
// Create ROI
await page.getByTestId('petSUV-btn').click();
await page.getByTestId('MeasurementTools-split-button-secondary').click();
await page.getByTestId('EllipticalROI').click();
const locator = page.getByTestId('viewport-pane').locator('canvas').first();
await clearAllAnnotations(page);
await simulateClicksOnElement({
locator,
points: [
{
x: 100,
y: 100,
},
{
x: 150,
y: 150,
},
],
});
// Get current SUV text
let oldSUV = await getSUV(page);
// Change PT Weight
await page.getByTestId('input-weight-input').fill('31');
await page.getByText('Reload Data').click();
await page.waitForLoadState('networkidle');
// Get new SUV text
let newSUV = await getSUV(page);
// Compare then store new SUV
expect.soft(newSUV).not.toEqual(oldSUV);
oldSUV = newSUV;
// Change total dose
await page
.getByText('Patient SexWeight kgTotal')
.locator('div')
.filter({ hasText: /^Total Dose bq$/ })
.getByTestId('input-undefined')
.fill('1888020304');
await page.getByText('Reload Data').click();
await page.waitForLoadState('networkidle');
// Get new SUV
newSUV = await getSUV(page);
// Compare then store new
expect.soft(newSUV).not.toEqual(oldSUV);
oldSUV = newSUV;
// Change injection time
await page
.getByText('Patient SexWeight kgTotal')
.locator('div')
.filter({ hasText: /^Injection Time s$/ })
.getByTestId('input-undefined')
.fill('060000');
await page.getByText('Reload Data').click();
await page.waitForLoadState('networkidle');
// Get new SUV
newSUV = await getSUV(page);
// Compare SUV
expect.soft(newSUV).not.toEqual(oldSUV);
});

View File

@ -0,0 +1,9 @@
import { test } from '@playwright/test';
import { visitStudy, checkForScreenshot, screenShotPaths } from './utils/index.js';
test('should render TMTV correctly.', async ({ page }) => {
const studyInstanceUID = '1.3.6.1.4.1.14519.5.2.1.7009.2403.871108593056125491804754960339';
const mode = 'Total Metabolic Tumor Volume';
await visitStudy(page, studyInstanceUID, mode, 10000);
await checkForScreenshot(page, page, screenShotPaths.tmtvRendering.tmtvDisplayedCorrectly, 100);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

View File

@ -0,0 +1,10 @@
const clearAllAnnotations = async page => {
await page.evaluate(
({ cornerstoneTools }: AppTypes.Test) => {
cornerstoneTools.annotation.state.removeAllAnnotations();
},
await page.evaluateHandle('window')
);
};
export { clearAllAnnotations };

15
tests/utils/getSUV.ts Normal file
View File

@ -0,0 +1,15 @@
const getSUV = async page => {
const SUV = await page.evaluate(
({ services }: AppTypes.Test) => {
const { measurementService } = services;
const measurements = measurementService.getMeasurements();
const displayText = measurements[0].displayText;
return displayText[2];
},
await page.evaluateHandle('window')
);
return SUV;
};
export { getSUV };

View File

@ -0,0 +1,27 @@
const getTMTVModalityUnit = async (page, attempts = 20) => {
for (let i = 0; i < attempts; i++) {
try {
const modalityUnit = await page.evaluate(
({ cornerstoneTools }: AppTypes.Test) => {
const annotations = cornerstoneTools.annotation.state.getAllAnnotations();
const stats = annotations[0].data.cachedStats;
const targetIds = Object.keys(stats);
const targetStats = stats[targetIds[1]];
return targetStats.modalityUnit;
},
await page.evaluateHandle('window')
);
if (modalityUnit) {
return modalityUnit;
}
} catch (error) {
console.error('Failed to get modalityUnit', error);
}
await new Promise(resolve => setTimeout(resolve, 1000));
}
throw new Error('Failed to get modalityUnit after multiple attempts');
};
export { getTMTVModalityUnit };

View File

@ -4,6 +4,21 @@ import { screenShotPaths } from './screenShotPaths';
import { simulateClicksOnElement } from './simulateClicksOnElement';
import { reduce3DViewportSize } from './reduce3DviewportSize';
import { getMousePosition, initilizeMousePositionTracker } from './mouseUtils';
import { getSUV } from './getSUV';
import { getTMTVModalityUnit } from './getTMTVModalityUnit';
import { clearAllAnnotations } from './clearAllAnnotations';
import { scrollVolumeViewport } from './scrollVolumeViewport';
export { visitStudy, checkForScreenshot, screenShotPaths, simulateClicksOnElement, reduce3DViewportSize, getMousePosition, initilizeMousePositionTracker };
export {
visitStudy,
checkForScreenshot,
screenShotPaths,
simulateClicksOnElement,
reduce3DViewportSize,
getMousePosition,
initilizeMousePositionTracker,
getSUV,
getTMTVModalityUnit,
clearAllAnnotations,
scrollVolumeViewport,
};

View File

@ -1,9 +1,13 @@
export const reduce3DViewportSize = async (page: any) => {
await page.evaluate(({ cornerstone }: AppTypes.Test) => {
const enabledElement = cornerstone.getEnabledElements().filter(element => element.viewport.type === 'volume3d')[0]
const { viewport } = enabledElement;
viewport.setZoom(0.5);
viewport.render()
}, await page.evaluateHandle('window'));
}
await page.evaluate(
({ cornerstone }: AppTypes.Test) => {
const enabledElement = cornerstone
.getEnabledElements()
.filter(element => element.viewport.type === 'volume3d')[0];
const { viewport } = enabledElement;
viewport.setZoom(0.5);
viewport.render();
},
await page.evaluateHandle('window')
);
};

View File

@ -86,7 +86,10 @@ const screenShotPaths = {
crosshairsSlabThickness: 'crosshairsSlabThickness.png',
crosshairsResetToolbar: 'crosshairsResetToolbar.png',
crosshairsNewDisplayset: 'crosshairsNewDisplayset.png',
}
},
tmtvRendering: {
tmtvDisplayedCorrectly: 'tmtvDisplayedCorrectly.png',
},
};
export { screenShotPaths };

View File

@ -0,0 +1,17 @@
type scrollVolumeViewportType = {
viewportId: string;
delta: number;
};
const scrollVolumeViewport = async (page, viewportId, delta) => {
await page.evaluate(
({ services, viewportId, delta }: withTestTypes<scrollVolumeViewportType>) => {
const { cornerstoneViewportService } = services;
const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId) as any;
viewport.scroll(delta);
},
{ viewportId, delta, services: await page.evaluateHandle('window.services') }
);
};
export { scrollVolumeViewport };