Add some test coverage for some error cases (#25240)
Sebastian Markbåge committed
Sep 11, 2022 at 21:53 UTC
c156ecd483b4adc8e16254b8cab3ee735e893271
3 files changed
+213
-48
packages/react-client/src/ReactFlightClient.js
-5
@@ -317,11 +317,6 @@ export function parseModelString(
317
} else {
318
const id = parseInt(value.substring(1), 16);
319
const chunk = getChunk(response, id);
320
- if (chunk._status === PENDING) {
321
- throw new Error(
322
- "We didn't expect to see a forward reference. This is a bug in the React Server.",
323
- );
324
- }
320
return readChunk(chunk);
321
}
322
}
packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
+174
-32
@@ -19,6 +19,7 @@ global.setImmediate = cb => cb();
19
20
let act;
21
let clientExports;
22
+let clientModuleError;
23
let webpackMap;
24
let Stream;
25
let React;
@@ -26,6 +27,7 @@ let ReactDOMClient;
27
let ReactServerDOMWriter;
28
let ReactServerDOMReader;
29
let Suspense;
30
+let ErrorBoundary;
31
32
describe('ReactFlightDOM', () => {
33
beforeEach(() => {
@@ -33,6 +35,7 @@ describe('ReactFlightDOM', () => {
35
act = require('jest-react').act;
36
const WebpackMock = require('./utils/WebpackMock');
37
clientExports = WebpackMock.clientExports;
38
+ clientModuleError = WebpackMock.clientModuleError;
39
webpackMap = WebpackMock.webpackMap;
40
41
Stream = require('stream');
@@ -41,6 +44,22 @@ describe('ReactFlightDOM', () => {
44
ReactDOMClient = require('react-dom/client');
45
ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server');
46
ReactServerDOMReader = require('react-server-dom-webpack');
47
+
48
+ ErrorBoundary = class extends React.Component {
49
+ state = {hasError: false, error: null};
50
+ static getDerivedStateFromError(error) {
51
+ return {
52
+ hasError: true,
53
+ error,
54
+ };
55
+ }
56
+ render() {
57
+ if (this.state.hasError) {
58
+ return this.props.fallback(this.state.error);
59
+ }
60
+ return this.props.children;
61
+ }
62
+ };
63
});
64
65
function getTestStream() {
@@ -319,22 +338,6 @@ describe('ReactFlightDOM', () => {
338
339
// Client Components
340
322
- class ErrorBoundary extends React.Component {
323
- state = {hasError: false, error: null};
324
- static getDerivedStateFromError(error) {
325
- return {
326
- hasError: true,
327
- error,
328
- };
329
- }
330
- render() {
331
- if (this.state.hasError) {
332
- return this.props.fallback(this.state.error);
333
- }
334
- return this.props.children;
335
- }
336
- }
337
-
341
function MyErrorBoundary({children}) {
342
return (
343
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
@@ -605,22 +608,6 @@ describe('ReactFlightDOM', () => {
608
it('should be able to complete after aborting and throw the reason client-side', async () => {
609
const reportedErrors = [];
610
608
- class ErrorBoundary extends React.Component {
609
- state = {hasError: false, error: null};
610
- static getDerivedStateFromError(error) {
611
- return {
612
- hasError: true,
613
- error,
614
- };
615
- }
616
- render() {
617
- if (this.state.hasError) {
618
- return this.props.fallback(this.state.error);
619
- }
620
- return this.props.children;
621
- }
622
- }
623
-
611
const {writable, readable} = getTestStream();
612
const {pipe, abort} = ReactServerDOMWriter.renderToPipeableStream(
613
<div>
@@ -661,4 +648,159 @@ describe('ReactFlightDOM', () => {
648
649
expect(reportedErrors).toEqual(['for reasons']);
650
});
651
+
652
+ it('should be able to recover from a direct reference erroring client-side', async () => {
653
+ const reportedErrors = [];
654
+
655
+ const ClientComponent = clientExports(function({prop}) {
656
+ return 'This should never render';
657
+ });
658
+
659
+ const ClientReference = clientModuleError(new Error('module init error'));
660
+
661
+ const {writable, readable} = getTestStream();
662
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
663
+ <div>
664
+ <ClientComponent prop={ClientReference} />
665
+ </div>,
666
+ webpackMap,
667
+ {
668
+ onError(x) {
669
+ reportedErrors.push(x);
670
+ },
671
+ },
672
+ );
673
+ pipe(writable);
674
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
675
+
676
+ const container = document.createElement('div');
677
+ const root = ReactDOMClient.createRoot(container);
678
+
679
+ function App({res}) {
680
+ return res.readRoot();
681
+ }
682
+
683
+ await act(async () => {
684
+ root.render(
685
+ <ErrorBoundary fallback={e => <p>{e.message}</p>}>
686
+ <Suspense fallback={<p>(loading)</p>}>
687
+ <App res={response} />
688
+ </Suspense>
689
+ </ErrorBoundary>,
690
+ );
691
+ });
692
+ expect(container.innerHTML).toBe('<p>module init error</p>');
693
+
694
+ expect(reportedErrors).toEqual([]);
695
+ });
696
+
697
+ it('should be able to recover from a direct reference erroring client-side async', async () => {
698
+ const reportedErrors = [];
699
+
700
+ const ClientComponent = clientExports(function({prop}) {
701
+ return 'This should never render';
702
+ });
703
+
704
+ let rejectPromise;
705
+ const ClientReference = await clientExports(
706
+ new Promise((resolve, reject) => {
707
+ rejectPromise = reject;
708
+ }),
709
+ );
710
+
711
+ const {writable, readable} = getTestStream();
712
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
713
+ <div>
714
+ <ClientComponent prop={ClientReference} />
715
+ </div>,
716
+ webpackMap,
717
+ {
718
+ onError(x) {
719
+ reportedErrors.push(x);
720
+ },
721
+ },
722
+ );
723
+ pipe(writable);
724
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
725
+
726
+ const container = document.createElement('div');
727
+ const root = ReactDOMClient.createRoot(container);
728
+
729
+ function App({res}) {
730
+ return res.readRoot();
731
+ }
732
+
733
+ await act(async () => {
734
+ root.render(
735
+ <ErrorBoundary fallback={e => <p>{e.message}</p>}>
736
+ <Suspense fallback={<p>(loading)</p>}>
737
+ <App res={response} />
738
+ </Suspense>
739
+ </ErrorBoundary>,
740
+ );
741
+ });
742
+
743
+ expect(container.innerHTML).toBe('<p>(loading)</p>');
744
+
745
+ await act(async () => {
746
+ rejectPromise(new Error('async module init error'));
747
+ });
748
+
749
+ expect(container.innerHTML).toBe('<p>async module init error</p>');
750
+
751
+ expect(reportedErrors).toEqual([]);
752
+ });
753
+
754
+ it('should be able to recover from a direct reference erroring server-side', async () => {
755
+ const reportedErrors = [];
756
+
757
+ const ClientComponent = clientExports(function({prop}) {
758
+ return 'This should never render';
759
+ });
760
+
761
+ // We simulate a bug in the Webpack bundler which causes an error on the server.
762
+ for (const id in webpackMap) {
763
+ Object.defineProperty(webpackMap, id, {
764
+ get: () => {
765
+ throw new Error('bug in the bundler');
766
+ },
767
+ });
768
+ }
769
+
770
+ const {writable, readable} = getTestStream();
771
+ const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
772
+ <div>
773
+ <ClientComponent />
774
+ </div>,
775
+ webpackMap,
776
+ {
777
+ onError(x) {
778
+ reportedErrors.push(x);
779
+ },
780
+ },
781
+ );
782
+ pipe(writable);
783
+
784
+ const response = ReactServerDOMReader.createFromReadableStream(readable);
785
+
786
+ const container = document.createElement('div');
787
+ const root = ReactDOMClient.createRoot(container);
788
+
789
+ function App({res}) {
790
+ return res.readRoot();
791
+ }
792
+
793
+ await act(async () => {
794
+ root.render(
795
+ <ErrorBoundary fallback={e => <p>{e.message}</p>}>
796
+ <Suspense fallback={<p>(loading)</p>}>
797
+ <App res={response} />
798
+ </Suspense>
799
+ </ErrorBoundary>,
800
+ );
801
+ });
802
+ expect(container.innerHTML).toBe('<p>bug in the bundler</p>');
803
+
804
+ expect(reportedErrors).toEqual([]);
805
+ });
806
});
packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js
+39
-11
@@ -12,8 +12,12 @@ const Module = require('module');
12
13
let webpackModuleIdx = 0;
14
const webpackModules = {};
15
+const webpackErroredModules = {};
16
const webpackMap = {};
17
global.__webpack_require__ = function(id) {
18
+ if (webpackErroredModules[id]) {
19
+ throw webpackErroredModules[id];
20
+ }
21
return webpackModules[id];
22
};
23
@@ -36,6 +40,27 @@ Module._extensions['.client.js'] = previousLoader;
40
exports.webpackMap = webpackMap;
41
exports.webpackModules = webpackModules;
42
43
+exports.clientModuleError = function clientModuleError(moduleError) {
44
+ const idx = '' + webpackModuleIdx++;
45
+ webpackErroredModules[idx] = moduleError;
46
+ const path = url.pathToFileURL(idx).href;
47
+ webpackMap[path] = {
48
+ '': {
49
+ id: idx,
50
+ chunks: [],
51
+ name: '',
52
+ },
53
+ '*': {
54
+ id: idx,
55
+ chunks: [],
56
+ name: '*',
57
+ },
58
+ };
59
+ const mod = {exports: {}};
60
+ nodeLoader(mod, idx);
61
+ return mod.exports;
62
+};
63
+
64
exports.clientExports = function clientExports(moduleExports) {
65
const idx = '' + webpackModuleIdx++;
66
webpackModules[idx] = moduleExports;
@@ -53,17 +78,20 @@ exports.clientExports = function clientExports(moduleExports) {
78
},
79
};
80
if (typeof moduleExports.then === 'function') {
56
- moduleExports.then(asyncModuleExports => {
57
- for (const name in asyncModuleExports) {
58
- webpackMap[path] = {
59
- [name]: {
60
- id: idx,
61
- chunks: [],
62
- name: name,
63
- },
64
- };
65
- }
66
- });
81
+ moduleExports.then(
82
+ asyncModuleExports => {
83
+ for (const name in asyncModuleExports) {
84
+ webpackMap[path] = {
85
+ [name]: {
86
+ id: idx,
87
+ chunks: [],
88
+ name: name,
89
+ },
90
+ };
91
+ }
92
+ },
93
+ () => {},
94
+ );
95
}
96
for (const name in moduleExports) {
97
webpackMap[path] = {