diff --git a/__tests__/src/components/ImageFailureMessage.test.jsx b/__tests__/src/components/ImageFailureMessage.test.jsx
index 4c7db194a..f231dc9a3 100644
--- a/__tests__/src/components/ImageFailureMessage.test.jsx
+++ b/__tests__/src/components/ImageFailureMessage.test.jsx
@@ -7,24 +7,23 @@ describe('ImageFailureMessage', () => {
it('does not render when no images have failed', () => {
const { container } = render(
-
+
,
);
expect(container).toBeEmptyDOMElement();
});
- it('renders default translated message when images have failed', () => {
- // Mock the context with hasFailed: true
+ it('renders message when a specific image has failed', () => {
const mockContext = {
+ failedImages: new Set(['https://example.com/image1.jpg']),
fallbackImage: 'data:image/svg+xml,...',
- hasFailed: true,
notifyFailure: vi.fn(),
};
render(
-
+
,
);
@@ -33,16 +32,44 @@ describe('ImageFailureMessage', () => {
expect(screen.getByText(/See console for details/i)).toBeInTheDocument();
});
+
+ it('isolates failures per window', () => {
+ const mockContext = {
+ failedImages: new Set(['https://example.com/manifest-a/canvas-1/image.jpg']),
+ fallbackImage: 'data:image/svg+xml,...',
+ notifyFailure: vi.fn(),
+ };
+
+ // Render Window A with its failed image
+ const { container: containerA } = render(
+
+
+ ,
+ );
+
+ expect(containerA).not.toBeEmptyDOMElement();
+ expect(screen.getByText(/Problem loading image/i)).toBeInTheDocument();
+
+ // Render Window B with non-failed image
+ const { container: containerB } = render(
+
+
+ ,
+ );
+
+ expect(containerB).toBeEmptyDOMElement();
+ });
+
it('has proper accessibility attributes', () => {
const mockContext = {
+ failedImages: new Set(['https://example.com/image1.jpg']),
fallbackImage: 'data:image/svg+xml,...',
- hasFailed: true,
notifyFailure: vi.fn(),
};
render(
-
+
,
);
diff --git a/__tests__/src/components/OpenSeadragonViewer.test.js b/__tests__/src/components/OpenSeadragonViewer.test.js
index 89e744016..4006b10ab 100644
--- a/__tests__/src/components/OpenSeadragonViewer.test.js
+++ b/__tests__/src/components/OpenSeadragonViewer.test.js
@@ -7,6 +7,10 @@ import { OpenSeadragonViewer } from '../../../src/components/OpenSeadragonViewer
import fixture from '../../fixtures/version-2/019.json';
import { OSDReferences } from '../../../src/plugins/OSDReferences';
+vi.mock('../../../src/components/ImageFailureMessage', () => ({
+ ImageFailureMessage: vi.fn(() => null),
+}));
+
const canvases = Utils.parseManifest(fixture).getSequences()[0].getCanvases();
/**
@@ -115,4 +119,29 @@ describe('OpenSeadragonViewer', () => {
vi.useRealTimers();
});
});
+
+ describe('ImageFailureMessage', () => {
+ it('passes imageUrls from infoResponses and nonTiledImages', async () => {
+ const { ImageFailureMessage } = await import('../../../src/components/ImageFailureMessage');
+
+ const infoResponses = [
+ { id: 'http://example.com/image1', json: {} },
+ { id: 'http://example.com/image2', json: {} },
+ ];
+ const nonTiledImages = [
+ { id: 'http://example.com/image3', getProperty: () => 'Image' },
+ ];
+
+ createWrapper({ infoResponses, nonTiledImages });
+
+ // lastCall[0] returns props object
+ expect(ImageFailureMessage.mock.lastCall[0]).toMatchObject({
+ imageUrls: [
+ 'http://example.com/image1',
+ 'http://example.com/image2',
+ 'http://example.com/image3',
+ ],
+ });
+ });
+ });
});
diff --git a/__tests__/src/contexts/FailedImageProvider.test.jsx b/__tests__/src/contexts/FailedImageProvider.test.jsx
index 15aa20f5e..b9fd66c81 100644
--- a/__tests__/src/contexts/FailedImageProvider.test.jsx
+++ b/__tests__/src/contexts/FailedImageProvider.test.jsx
@@ -39,23 +39,18 @@ describe('FailedImageProvider', () => {
expect(result.current.fallbackImage).toContain('%231967d2'); // Blue color
});
- it('provides hasFailed as false initially', () => {
+ it('provides failedImages as empty Set initially and can add to it', () => {
const wrapper = ({ children }) => {children};
const { result } = renderHook(() => useContext(FailedImageContext), { wrapper });
- expect(result.current.hasFailed).toBe(false);
- });
-
- it('sets hasFailed to true after notifyFailure is called', () => {
- const wrapper = ({ children }) => {children};
- const { result } = renderHook(() => useContext(FailedImageContext), { wrapper });
-
- expect(result.current.hasFailed).toBe(false);
+ expect(result.current.failedImages).toBeInstanceOf(Set);
+ expect(result.current.failedImages.size).toBe(0);
act(() => {
- result.current.notifyFailure();
+ result.current.notifyFailure('https://example.com/image1.jpg');
});
- expect(result.current.hasFailed).toBe(true);
+ expect(result.current.failedImages.size).toBe(1);
+ expect(result.current.failedImages.has('https://example.com/image1.jpg')).toBe(true);
});
});
diff --git a/src/components/IIIFThumbnail.jsx b/src/components/IIIFThumbnail.jsx
index 75cf653db..4395560ba 100644
--- a/src/components/IIIFThumbnail.jsx
+++ b/src/components/IIIFThumbnail.jsx
@@ -130,7 +130,7 @@ const LazyLoadedImage = ({
style={imageStyles}
onError={() => {
setFailed(true);
- notifyFailure();
+ if (finalSrc) notifyFailure(finalSrc);
}}
{...props}
/>
diff --git a/src/components/ImageFailureMessage.jsx b/src/components/ImageFailureMessage.jsx
index 35bb8f05d..ae209641a 100644
--- a/src/components/ImageFailureMessage.jsx
+++ b/src/components/ImageFailureMessage.jsx
@@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';
import { styled } from '@mui/material/styles';
import Typography from '@mui/material/Typography';
import Box from '@mui/material/Box';
+import PropTypes from 'prop-types';
import FailedImageContext from '../contexts/FailedImageContext';
const MessageContainer = styled(Box)(({ theme }) => ({
@@ -21,11 +22,13 @@ const MessageContainer = styled(Box)(({ theme }) => ({
/**
* Displays an accessible message when images fail to load in the OSD viewer
*/
-export function ImageFailureMessage() {
- const { hasFailed } = useContext(FailedImageContext);
+export function ImageFailureMessage({ imageUrls = [] }) {
+ const { failedImages } = useContext(FailedImageContext);
const { t } = useTranslation();
- if (!hasFailed) return null;
+ const hasFailedImage = imageUrls.some(url => failedImages.has(url));
+
+ if (!hasFailedImage) return null;
return (
@@ -35,3 +38,7 @@ export function ImageFailureMessage() {
);
}
+
+ImageFailureMessage.propTypes = {
+ imageUrls: PropTypes.arrayOf(PropTypes.string),
+};
diff --git a/src/components/OpenSeadragonTileSource.jsx b/src/components/OpenSeadragonTileSource.jsx
index f8879cb33..e8b17371a 100644
--- a/src/components/OpenSeadragonTileSource.jsx
+++ b/src/components/OpenSeadragonTileSource.jsx
@@ -62,7 +62,8 @@ export default function OpenSeadragonTileSource({
success: (event) => resolve(event),
error: (event) => {
- notifyFailure();
+ const imageUrl = url || (typeof tileSource === 'string' ? tileSource : tileSource?.['@id']);
+ if (imageUrl) notifyFailure(imageUrl);
loadFallback();
reject(event);
diff --git a/src/components/OpenSeadragonViewer.jsx b/src/components/OpenSeadragonViewer.jsx
index 851cc31bd..41bb31742 100644
--- a/src/components/OpenSeadragonViewer.jsx
+++ b/src/components/OpenSeadragonViewer.jsx
@@ -1,5 +1,5 @@
import {
- useRef, Children, cloneElement, useCallback, useState, useEffect,
+ useRef, Children, cloneElement, useCallback, useState, useEffect, useMemo,
} from 'react';
import PropTypes from 'prop-types';
import { styled } from '@mui/material/styles';
@@ -85,6 +85,11 @@ export function OpenSeadragonViewer({
...rest,
};
+ const imageUrls = useMemo(() => [
+ ...infoResponses.map(info => info.id),
+ ...nonTiledImages.map(img => img.id),
+ ], [infoResponses, nonTiledImages]);
+
return (
}
{ enhancedChildren }
-
+
);
diff --git a/src/contexts/FailedImageProvider.jsx b/src/contexts/FailedImageProvider.jsx
index 33369e1e3..c81ee58b1 100644
--- a/src/contexts/FailedImageProvider.jsx
+++ b/src/contexts/FailedImageProvider.jsx
@@ -9,16 +9,16 @@ const defaultFallback = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000
export default function FailedImageProvider({ children }) {
const fallbackImage = config.fallbackImage || defaultFallback;
- const [hasFailed, setHasFailed] = useState(false);
+ const [failedImages, setFailedImages] = useState(new Set());
- const notifyFailure = useCallback(() => {
- setHasFailed(true);
+ const notifyFailure = useCallback((imageId) => {
+ setFailedImages((prev) => new Set(prev).add(imageId));
}, []);
return (