diff --git a/platform/core/src/types/AppTypes.ts b/platform/core/src/types/AppTypes.ts index e14bce529..7d5d35f5e 100644 --- a/platform/core/src/types/AppTypes.ts +++ b/platform/core/src/types/AppTypes.ts @@ -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 & AppTypes.Test; } diff --git a/tests/TMTVAlignment.spec.ts b/tests/TMTVAlignment.spec.ts new file mode 100644 index 000000000..8d7c5250f --- /dev/null +++ b/tests/TMTVAlignment.spec.ts @@ -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'); +}); diff --git a/tests/TMTVModalityUnit.spec.ts b/tests/TMTVModalityUnit.spec.ts new file mode 100644 index 000000000..1015de129 --- /dev/null +++ b/tests/TMTVModalityUnit.spec.ts @@ -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'); +}); diff --git a/tests/TMTVRecalculate.spec.ts b/tests/TMTVRecalculate.spec.ts new file mode 100644 index 000000000..06f46c79b --- /dev/null +++ b/tests/TMTVRecalculate.spec.ts @@ -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); +}); diff --git a/tests/TMTVRendering.spec.ts b/tests/TMTVRendering.spec.ts new file mode 100644 index 000000000..ebacf620c --- /dev/null +++ b/tests/TMTVRendering.spec.ts @@ -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); +}); diff --git a/tests/screenshots/chromium/TMTVRendering.spec.ts/tmtvDisplayedCorrectly.png b/tests/screenshots/chromium/TMTVRendering.spec.ts/tmtvDisplayedCorrectly.png new file mode 100644 index 000000000..4e277c502 Binary files /dev/null and b/tests/screenshots/chromium/TMTVRendering.spec.ts/tmtvDisplayedCorrectly.png differ diff --git a/tests/utils/clearAllAnnotations.ts b/tests/utils/clearAllAnnotations.ts new file mode 100644 index 000000000..6b3208128 --- /dev/null +++ b/tests/utils/clearAllAnnotations.ts @@ -0,0 +1,10 @@ +const clearAllAnnotations = async page => { + await page.evaluate( + ({ cornerstoneTools }: AppTypes.Test) => { + cornerstoneTools.annotation.state.removeAllAnnotations(); + }, + await page.evaluateHandle('window') + ); +}; + +export { clearAllAnnotations }; diff --git a/tests/utils/getSUV.ts b/tests/utils/getSUV.ts new file mode 100644 index 000000000..44bc7bec3 --- /dev/null +++ b/tests/utils/getSUV.ts @@ -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 }; diff --git a/tests/utils/getTMTVModalityUnit.ts b/tests/utils/getTMTVModalityUnit.ts new file mode 100644 index 000000000..057db71f5 --- /dev/null +++ b/tests/utils/getTMTVModalityUnit.ts @@ -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 }; diff --git a/tests/utils/index.ts b/tests/utils/index.ts index a6e9aeb77..ff5acf510 100644 --- a/tests/utils/index.ts +++ b/tests/utils/index.ts @@ -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, +}; diff --git a/tests/utils/reduce3DviewportSize.ts b/tests/utils/reduce3DviewportSize.ts index 2ce072c5d..e7ad480db 100644 --- a/tests/utils/reduce3DviewportSize.ts +++ b/tests/utils/reduce3DviewportSize.ts @@ -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') + ); +}; diff --git a/tests/utils/screenShotPaths.ts b/tests/utils/screenShotPaths.ts index 063f9c851..c2d3efd72 100644 --- a/tests/utils/screenShotPaths.ts +++ b/tests/utils/screenShotPaths.ts @@ -86,7 +86,10 @@ const screenShotPaths = { crosshairsSlabThickness: 'crosshairsSlabThickness.png', crosshairsResetToolbar: 'crosshairsResetToolbar.png', crosshairsNewDisplayset: 'crosshairsNewDisplayset.png', - } + }, + tmtvRendering: { + tmtvDisplayedCorrectly: 'tmtvDisplayedCorrectly.png', + }, }; export { screenShotPaths }; diff --git a/tests/utils/scrollVolumeViewport.ts b/tests/utils/scrollVolumeViewport.ts new file mode 100644 index 000000000..49911a126 --- /dev/null +++ b/tests/utils/scrollVolumeViewport.ts @@ -0,0 +1,17 @@ +type scrollVolumeViewportType = { + viewportId: string; + delta: number; +}; + +const scrollVolumeViewport = async (page, viewportId, delta) => { + await page.evaluate( + ({ services, viewportId, delta }: withTestTypes) => { + const { cornerstoneViewportService } = services; + const viewport = cornerstoneViewportService.getCornerstoneViewport(viewportId) as any; + viewport.scroll(delta); + }, + { viewportId, delta, services: await page.evaluateHandle('window.services') } + ); +}; + +export { scrollVolumeViewport };