Skip to content

Commit c156ecd

Browse files
authored
Add some test coverage for some error cases (#25240)
1 parent 3613284 commit c156ecd

File tree

3 files changed

+213
-48
lines changed

3 files changed

+213
-48
lines changed

packages/react-client/src/ReactFlightClient.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,6 @@ export function parseModelString(
317317
} else {
318318
const id = parseInt(value.substring(1), 16);
319319
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-
}
325320
return readChunk(chunk);
326321
}
327322
}

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js

Lines changed: 174 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,23 @@ global.setImmediate = cb => cb();
1919

2020
let act;
2121
let clientExports;
22+
let clientModuleError;
2223
let webpackMap;
2324
let Stream;
2425
let React;
2526
let ReactDOMClient;
2627
let ReactServerDOMWriter;
2728
let ReactServerDOMReader;
2829
let Suspense;
30+
let ErrorBoundary;
2931

3032
describe('ReactFlightDOM', () => {
3133
beforeEach(() => {
3234
jest.resetModules();
3335
act = require('jest-react').act;
3436
const WebpackMock = require('./utils/WebpackMock');
3537
clientExports = WebpackMock.clientExports;
38+
clientModuleError = WebpackMock.clientModuleError;
3639
webpackMap = WebpackMock.webpackMap;
3740

3841
Stream = require('stream');
@@ -41,6 +44,22 @@ describe('ReactFlightDOM', () => {
4144
ReactDOMClient = require('react-dom/client');
4245
ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server');
4346
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+
};
4463
});
4564

4665
function getTestStream() {
@@ -319,22 +338,6 @@ describe('ReactFlightDOM', () => {
319338

320339
// Client Components
321340

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-
338341
function MyErrorBoundary({children}) {
339342
return (
340343
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
@@ -605,22 +608,6 @@ describe('ReactFlightDOM', () => {
605608
it('should be able to complete after aborting and throw the reason client-side', async () => {
606609
const reportedErrors = [];
607610

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-
624611
const {writable, readable} = getTestStream();
625612
const {pipe, abort} = ReactServerDOMWriter.renderToPipeableStream(
626613
<div>
@@ -661,4 +648,159 @@ describe('ReactFlightDOM', () => {
661648

662649
expect(reportedErrors).toEqual(['for reasons']);
663650
});
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+
});
664806
});

packages/react-server-dom-webpack/src/__tests__/utils/WebpackMock.js

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ const Module = require('module');
1212

1313
let webpackModuleIdx = 0;
1414
const webpackModules = {};
15+
const webpackErroredModules = {};
1516
const webpackMap = {};
1617
global.__webpack_require__ = function(id) {
18+
if (webpackErroredModules[id]) {
19+
throw webpackErroredModules[id];
20+
}
1721
return webpackModules[id];
1822
};
1923

@@ -36,6 +40,27 @@ Module._extensions['.client.js'] = previousLoader;
3640
exports.webpackMap = webpackMap;
3741
exports.webpackModules = webpackModules;
3842

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+
3964
exports.clientExports = function clientExports(moduleExports) {
4065
const idx = '' + webpackModuleIdx++;
4166
webpackModules[idx] = moduleExports;
@@ -53,17 +78,20 @@ exports.clientExports = function clientExports(moduleExports) {
5378
},
5479
};
5580
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+
);
6795
}
6896
for (const name in moduleExports) {
6997
webpackMap[path] = {

0 commit comments

Comments
 (0)