From 7f9e59ee3074e3511d22c7f0075856a7bd2cc32e Mon Sep 17 00:00:00 2001 From: Ghadeer Albattarni <165973963+GhadeerAlbattarni@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:51:44 -0500 Subject: [PATCH] fix(app): display study not found message for all modes (#5752) * fix(app): move study validation to ModeRoute component - Moved validation from defaultRouteInit.ts to Mode.tsx - Added dedicated useEffect hook for study validation * test: add Playwright test for study not found error page across multiple modes --- .../Panels/StudyBrowser/PanelStudyBrowser.tsx | 5 -- platform/app/src/routes/Mode/Mode.tsx | 35 ++++++++ platform/app/src/routes/index.tsx | 16 +++- tests/StudyValidation.spec.ts | 83 +++++++++++++++++++ tests/pages/NotFoundStudyPageObject.ts | 21 +++++ tests/pages/index.ts | 2 + tests/utils/fixture.ts | 5 ++ 7 files changed, 159 insertions(+), 8 deletions(-) create mode 100644 tests/StudyValidation.spec.ts create mode 100644 tests/pages/NotFoundStudyPageObject.ts diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index 052d0fe28..db2a32075 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -122,11 +122,6 @@ function PanelStudyBrowser({ studyInstanceUid: StudyInstanceUID, }); - if (!qidoForStudyUID?.length) { - navigate('/notfoundstudy', '_self'); - throw new Error('Invalid study URL'); - } - let qidoStudiesForPatient = qidoForStudyUID; // try to fetch the prior studies based on the patientID if the diff --git a/platform/app/src/routes/Mode/Mode.tsx b/platform/app/src/routes/Mode/Mode.tsx index 8633906a1..8ea46a8f6 100644 --- a/platform/app/src/routes/Mode/Mode.tsx +++ b/platform/app/src/routes/Mode/Mode.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState, useRef } from 'react'; import { useParams, useLocation } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import PropTypes from 'prop-types'; import { utils } from '@ohif/core'; import { ImageViewerProvider, DragAndDropProvider } from '@ohif/ui-next'; @@ -31,6 +32,8 @@ export default function ModeRoute({ // The URL's query search parameters where the keys casing is maintained const query = useSearchParams(); + const navigate = useNavigate(); + mode?.onModeInit?.({ servicesManager, extensionManager, @@ -130,6 +133,38 @@ export default function ModeRoute({ }; }, [location, ExtensionDependenciesLoaded]); + /** + * Validates study existence before loading the viewer. + * Moved from PanelStudyBrowser.tsx to ensure validation runs in all modes + */ + useEffect(() => { + if (!ExtensionDependenciesLoaded || !studyInstanceUIDs?.length || !dataSource) { + return; + } + + const validateStudies = async () => { + for (const studyInstanceUID of studyInstanceUIDs) { + try { + const qidoForStudyUID = await dataSource.query.studies.search({ + studyInstanceUid: studyInstanceUID, + }); + + if (!qidoForStudyUID?.length) { + console.warn('Study not found:', studyInstanceUID); + navigate('/notfoundstudy'); + return; + } + } catch (error) { + console.error('Error validating study:', studyInstanceUID, error); + navigate('/notfoundstudy'); + return; + } + } + }; + + validateStudies(); + }, [studyInstanceUIDs, ExtensionDependenciesLoaded, dataSource, navigate]); + useEffect(() => { if (!ExtensionDependenciesLoaded || !studyInstanceUIDs?.length) { return; diff --git a/platform/app/src/routes/index.tsx b/platform/app/src/routes/index.tsx index 6084ba52e..b0c73b808 100644 --- a/platform/app/src/routes/index.tsx +++ b/platform/app/src/routes/index.tsx @@ -38,12 +38,22 @@ const NotFoundStudy = () => { return (
-

+

One or more of the requested studies are not available at this time.

{showStudyList && ( -

- Return to the study list to select a different study to view. +

+ Return to the{' '} + + study list + {' '} + to select a different study to view.

)}
diff --git a/tests/StudyValidation.spec.ts b/tests/StudyValidation.spec.ts new file mode 100644 index 000000000..bfb132551 --- /dev/null +++ b/tests/StudyValidation.spec.ts @@ -0,0 +1,83 @@ +import { expect, test } from './utils'; + +test.describe('Study Validation', () => { + const invalidStudyUID = '9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9.9'; + const validStudyUID = '1.3.6.1.4.1.25403.345050719074.3824.20170125095438.5'; + + const modes = ['viewer', 'segmentation', 'microscopy', 'tmtv']; + + // Test 1: Single invalid study + modes.forEach(mode => { + test(`should display error page when study UID is not found in ${mode} mode`, async ({ + page, + notFoundStudyPageObject, + }) => { + await page.goto(`/${mode}/ohif?StudyInstanceUIDs=${invalidStudyUID}`); + + await page.waitForURL('**/notfoundstudy', { timeout: 15000 }); + + await expect(notFoundStudyPageObject.errorMessage).toBeVisible(); + await expect(notFoundStudyPageObject.errorMessage).toHaveText( + 'One or more of the requested studies are not available at this time.' + ); + + await expect(notFoundStudyPageObject.returnMessage).toBeVisible(); + await expect(notFoundStudyPageObject.returnMessage).toContainText( + 'Return to the study list to select a different study to view.' + ); + + await expect(notFoundStudyPageObject.studyListLink).toBeVisible(); + await expect(notFoundStudyPageObject.studyListLink).toHaveAttribute('href', '/'); + }); + }); + + // Test 2: Multiple studies - valid first, then invalid + modes.forEach(mode => { + test(`should display error page when second study UID is invalid in ${mode} mode`, async ({ + page, + notFoundStudyPageObject, + }) => { + await page.goto(`/${mode}/ohif?StudyInstanceUIDs=${validStudyUID},${invalidStudyUID}`); + + await page.waitForURL('**/notfoundstudy', { timeout: 15000 }); + + await expect(notFoundStudyPageObject.errorMessage).toBeVisible(); + await expect(notFoundStudyPageObject.errorMessage).toHaveText( + 'One or more of the requested studies are not available at this time.' + ); + + await expect(notFoundStudyPageObject.returnMessage).toBeVisible(); + await expect(notFoundStudyPageObject.returnMessage).toContainText( + 'Return to the study list to select a different study to view.' + ); + + await expect(notFoundStudyPageObject.studyListLink).toBeVisible(); + await expect(notFoundStudyPageObject.studyListLink).toHaveAttribute('href', '/'); + }); + }); + + // Test 3: Multiple studies - invalid first, then valid + modes.forEach(mode => { + test(`should display error page when first study UID is invalid in ${mode} mode`, async ({ + page, + notFoundStudyPageObject, + }) => { + await page.goto(`/${mode}/ohif?StudyInstanceUIDs=${invalidStudyUID},${validStudyUID}`); + + await page.waitForURL('**/notfoundstudy', { timeout: 15000 }); + + await expect(notFoundStudyPageObject.errorMessage).toBeVisible(); + await expect(notFoundStudyPageObject.errorMessage).toHaveText( + 'One or more of the requested studies are not available at this time.' + ); + + await expect(notFoundStudyPageObject.returnMessage).toBeVisible(); + await expect(notFoundStudyPageObject.returnMessage).toContainText( + 'Return to the study list to select a different study to view.' + ); + + await expect(notFoundStudyPageObject.studyListLink).toBeVisible(); + await expect(notFoundStudyPageObject.studyListLink).toHaveAttribute('href', '/'); + }); + }); +}); diff --git a/tests/pages/NotFoundStudyPageObject.ts b/tests/pages/NotFoundStudyPageObject.ts new file mode 100644 index 000000000..3a9084b85 --- /dev/null +++ b/tests/pages/NotFoundStudyPageObject.ts @@ -0,0 +1,21 @@ +import { Locator, Page } from '@playwright/test'; + +export class NotFoundStudyPageObject { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + get errorMessage(): Locator { + return this.page.locator('[data-cy="study-not-found-message"]'); + } + + get returnMessage(): Locator { + return this.page.locator('[data-cy="return-to-study-list-message"]'); + } + + get studyListLink(): Locator { + return this.page.getByRole('link', { name: 'study list' }); + } +} diff --git a/tests/pages/index.ts b/tests/pages/index.ts index 8f441d1c8..a05f1ca3f 100644 --- a/tests/pages/index.ts +++ b/tests/pages/index.ts @@ -3,6 +3,7 @@ import { MainToolbarPageObject } from './MainToolbarPageObject'; import { LeftPanelPageObject } from './LeftPanelPageObject'; import { RightPanelPageObject } from './RightPanelPageObject'; import { ViewportPageObject } from './ViewportPageObject'; +import { NotFoundStudyPageObject } from './NotFoundStudyPageObject'; export { DOMOverlayPageObject, @@ -10,4 +11,5 @@ export { LeftPanelPageObject, RightPanelPageObject, ViewportPageObject, + NotFoundStudyPageObject, }; diff --git a/tests/utils/fixture.ts b/tests/utils/fixture.ts index 116856a43..007768deb 100644 --- a/tests/utils/fixture.ts +++ b/tests/utils/fixture.ts @@ -5,6 +5,7 @@ import { LeftPanelPageObject, RightPanelPageObject, ViewportPageObject, + NotFoundStudyPageObject, } from '../pages'; type PageObjects = { @@ -13,6 +14,7 @@ type PageObjects = { leftPanelPageObject: LeftPanelPageObject; rightPanelPageObject: RightPanelPageObject; viewportPageObject: ViewportPageObject; + notFoundStudyPageObject: NotFoundStudyPageObject; }; export const test = base.extend({ @@ -31,6 +33,9 @@ export const test = base.extend({ viewportPageObject: async ({ page }, use) => { await use(new ViewportPageObject(page)); }, + notFoundStudyPageObject: async ({ page }, use) => { + await use(new NotFoundStudyPageObject(page)); + }, }); export { expect } from 'playwright-test-coverage';