Skip to content

Commit 9083637

Browse files
xfournetclaude
andcommitted
fix(front): opt the PDF viewers out of the React-PDF 11 Suspense flow
React-PDF 11 suspends while loading and rethrows load failures by default, so `Document` no longer honours the `loading` and `onLoadError` props the two viewers are built on. `PdfViewer` would hand its wait to the nearest route fallback, and `StixCoreObjectContent` would send a password-protected file to the page Error Boundary instead of rendering its own placeholder. Both viewers now pass `suspense={false}`, which is the documented way to keep the previous behaviour. `Page` inherits the flag through the document context, so the pages inside need no change. The new tests cover both regressions: they pass on 10.5.0, fail on 11.0.0 without this change, and pass again with it. They need a DOMMatrix stub, which PDF.js reads at import time and jsdom does not provide. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 1cd468d commit 9083637

4 files changed

Lines changed: 96 additions & 0 deletions

File tree

opencti-platform/opencti-front/setup-vitest.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,21 @@ if (!Element.prototype.releasePointerCapture) {
4747
if (!Element.prototype.scrollIntoView) {
4848
Element.prototype.scrollIntoView = () => {};
4949
}
50+
51+
// jsdom ships no DOMMatrix, and PDF.js reads it at import time in its canvas
52+
// module. Without this stub any test importing react-pdf fails on
53+
// `ReferenceError: DOMMatrix is not defined` before a component is even rendered.
54+
if (!('DOMMatrix' in globalThis)) {
55+
class DOMMatrixStub {
56+
translate() {
57+
return new DOMMatrixStub();
58+
}
59+
60+
scale() {
61+
return new DOMMatrixStub();
62+
}
63+
}
64+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
65+
// @ts-ignore
66+
globalThis.DOMMatrix = DOMMatrixStub;
67+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React, { Component, ReactNode, Suspense } from 'react';
2+
import { render, screen, waitFor } from '@testing-library/react';
3+
import { describe, expect, it, vi } from 'vitest';
4+
import { Document } from 'react-pdf';
5+
import PdfViewer from './PdfViewer';
6+
7+
// The PDF viewers rely on `Document` resolving through its own props rather than
8+
// through React's Suspense and Error Boundary machinery: `PdfViewer` passes a
9+
// `loading` element, and `StixCoreObjectContent` additionally relies on
10+
// `onLoadError` to swap in its "password protected" placeholder. React-PDF 11
11+
// suspends and rethrows by default, which silently defeats both, so these tests
12+
// pin the behaviour the viewers are written against.
13+
14+
const brokenPdf = () => new File(
15+
[new Uint8Array([1, 2, 3, 4])],
16+
'broken.pdf',
17+
{ type: 'application/pdf' },
18+
);
19+
20+
class TestErrorBoundary extends Component<{ children: ReactNode }, { failed: boolean }> {
21+
constructor(props: { children: ReactNode }) {
22+
super(props);
23+
this.state = { failed: false };
24+
}
25+
26+
static getDerivedStateFromError() {
27+
return { failed: true };
28+
}
29+
30+
render() {
31+
if (this.state.failed) return <div>BOUNDARY</div>;
32+
return this.props.children;
33+
}
34+
}
35+
36+
describe('PdfViewer', () => {
37+
it('renders its own loader instead of suspending to the surrounding fallback', () => {
38+
render(
39+
<Suspense fallback={<div>ROUTE_FALLBACK</div>}>
40+
<PdfViewer pdf={brokenPdf()} />
41+
</Suspense>,
42+
);
43+
44+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
45+
expect(screen.queryByText('ROUTE_FALLBACK')).not.toBeInTheDocument();
46+
});
47+
});
48+
49+
// Mirrors how StixCoreObjectContent renders a PDF: a load failure has to reach
50+
// `onLoadError` so the component can render its own placeholder, and must not
51+
// escape to the Error Boundary wrapping the page.
52+
describe('Document load failure', () => {
53+
it('reaches onLoadError without escaping to the Error Boundary', async () => {
54+
const onLoadError = vi.fn();
55+
56+
render(
57+
<TestErrorBoundary>
58+
<Document
59+
file={brokenPdf()}
60+
suspense={false}
61+
loading={<div>DOCUMENT_LOADING</div>}
62+
onLoadError={onLoadError}
63+
/>
64+
</TestErrorBoundary>,
65+
);
66+
67+
expect(screen.getByText('DOCUMENT_LOADING')).toBeInTheDocument();
68+
await waitFor(() => expect(onLoadError).toHaveBeenCalled());
69+
expect(screen.queryByText('BOUNDARY')).not.toBeInTheDocument();
70+
});
71+
});

opencti-platform/opencti-front/src/components/PdfViewer.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ const PdfViewer = ({ pdf }: PdfViewerProps) => {
2121
>
2222
<Document
2323
file={pdf}
24+
// React-PDF 11 suspends by default, which ignores `loading` and hands the
25+
// wait to the nearest route-level fallback.
26+
suspense={false}
2427
loading={<Loader />}
2528
onLoadSuccess={({ numPages }) => setNbPages(numPages)}
2629
>

opencti-platform/opencti-front/src/private/components/common/stix_core_objects/StixCoreObjectContent.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,10 @@ class StixCoreObjectContentComponent extends Component {
726726
}
727727
>
728728
<Document
729+
// React-PDF 11 suspends and rethrows by default, which ignores
730+
// `loading` and sends a password-protected file to the page Error
731+
// Boundary instead of the placeholder handlePdfLoadError renders.
732+
suspense={false}
729733
onLoadSuccess={this.onDocumentLoadSuccess.bind(this)}
730734
onLoadError={this.handlePdfLoadError.bind(this)}
731735
onPassword={this.handlePdfPasswordRequest.bind(this)}

0 commit comments

Comments
 (0)