@samitouri / QOS-React-2 / commits / c156ecd483

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] = {