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';