Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Flight] Add some test coverage for some error cases #25240

Merged
merged 1 commit into from
Sep 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions packages/react-client/src/ReactFlightClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,11 +317,6 @@ export function parseModelString(
} else {
const id = parseInt(value.substring(1), 16);
const chunk = getChunk(response, id);
if (chunk._status === PENDING) {
throw new Error(
"We didn't expect to see a forward reference. This is a bug in the React Server.",
);
}
return readChunk(chunk);
}
}
Expand Down
206 changes: 174 additions & 32 deletions packages/react-server-dom-webpack/src/__tests__/ReactFlightDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,23 @@ global.setImmediate = cb => cb();

let act;
let clientExports;
let clientModuleError;
let webpackMap;
let Stream;
let React;
let ReactDOMClient;
let ReactServerDOMWriter;
let ReactServerDOMReader;
let Suspense;
let ErrorBoundary;

describe('ReactFlightDOM', () => {
beforeEach(() => {
jest.resetModules();
act = require('jest-react').act;
const WebpackMock = require('./utils/WebpackMock');
clientExports = WebpackMock.clientExports;
clientModuleError = WebpackMock.clientModuleError;
webpackMap = WebpackMock.webpackMap;

Stream = require('stream');
Expand All @@ -41,6 +44,22 @@ describe('ReactFlightDOM', () => {
ReactDOMClient = require('react-dom/client');
ReactServerDOMWriter = require('react-server-dom-webpack/writer.node.server');
ReactServerDOMReader = require('react-server-dom-webpack');

ErrorBoundary = class extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
};
});

function getTestStream() {
Expand Down Expand Up @@ -319,22 +338,6 @@ describe('ReactFlightDOM', () => {

// Client Components

class ErrorBoundary extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}

function MyErrorBoundary({children}) {
return (
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
Expand Down Expand Up @@ -605,22 +608,6 @@ describe('ReactFlightDOM', () => {
it('should be able to complete after aborting and throw the reason client-side', async () => {
const reportedErrors = [];

class ErrorBoundary extends React.Component {
state = {hasError: false, error: null};
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback(this.state.error);
}
return this.props.children;
}
}

const {writable, readable} = getTestStream();
const {pipe, abort} = ReactServerDOMWriter.renderToPipeableStream(
<div>
Expand Down Expand Up @@ -661,4 +648,159 @@ describe('ReactFlightDOM', () => {

expect(reportedErrors).toEqual(['for reasons']);
});

it('should be able to recover from a direct reference erroring client-side', async () => {
const reportedErrors = [];

const ClientComponent = clientExports(function({prop}) {
return 'This should never render';
});

const ClientReference = clientModuleError(new Error('module init error'));

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<div>
<ClientComponent prop={ClientReference} />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
},
},
);
pipe(writable);
const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

function App({res}) {
return res.readRoot();
}

await act(async () => {
root.render(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>module init error</p>');

expect(reportedErrors).toEqual([]);
});

it('should be able to recover from a direct reference erroring client-side async', async () => {
const reportedErrors = [];

const ClientComponent = clientExports(function({prop}) {
return 'This should never render';
});

let rejectPromise;
const ClientReference = await clientExports(
new Promise((resolve, reject) => {
rejectPromise = reject;
}),
);

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<div>
<ClientComponent prop={ClientReference} />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
},
},
);
pipe(writable);
const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

function App({res}) {
return res.readRoot();
}

await act(async () => {
root.render(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});

expect(container.innerHTML).toBe('<p>(loading)</p>');

await act(async () => {
rejectPromise(new Error('async module init error'));
});

expect(container.innerHTML).toBe('<p>async module init error</p>');

expect(reportedErrors).toEqual([]);
});

it('should be able to recover from a direct reference erroring server-side', async () => {
const reportedErrors = [];

const ClientComponent = clientExports(function({prop}) {
return 'This should never render';
});

// We simulate a bug in the Webpack bundler which causes an error on the server.
for (const id in webpackMap) {
Object.defineProperty(webpackMap, id, {
get: () => {
throw new Error('bug in the bundler');
},
});
}

const {writable, readable} = getTestStream();
const {pipe} = ReactServerDOMWriter.renderToPipeableStream(
<div>
<ClientComponent />
</div>,
webpackMap,
{
onError(x) {
reportedErrors.push(x);
},
},
);
pipe(writable);

const response = ReactServerDOMReader.createFromReadableStream(readable);

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);

function App({res}) {
return res.readRoot();
}

await act(async () => {
root.render(
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
<Suspense fallback={<p>(loading)</p>}>
<App res={response} />
</Suspense>
</ErrorBoundary>,
);
});
expect(container.innerHTML).toBe('<p>bug in the bundler</p>');

expect(reportedErrors).toEqual([]);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ const Module = require('module');

let webpackModuleIdx = 0;
const webpackModules = {};
const webpackErroredModules = {};
const webpackMap = {};
global.__webpack_require__ = function(id) {
if (webpackErroredModules[id]) {
throw webpackErroredModules[id];
}
return webpackModules[id];
};

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

exports.clientModuleError = function clientModuleError(moduleError) {
const idx = '' + webpackModuleIdx++;
webpackErroredModules[idx] = moduleError;
const path = url.pathToFileURL(idx).href;
webpackMap[path] = {
'': {
id: idx,
chunks: [],
name: '',
},
'*': {
id: idx,
chunks: [],
name: '*',
},
};
const mod = {exports: {}};
nodeLoader(mod, idx);
return mod.exports;
};

exports.clientExports = function clientExports(moduleExports) {
const idx = '' + webpackModuleIdx++;
webpackModules[idx] = moduleExports;
Expand All @@ -53,17 +78,20 @@ exports.clientExports = function clientExports(moduleExports) {
},
};
if (typeof moduleExports.then === 'function') {
moduleExports.then(asyncModuleExports => {
for (const name in asyncModuleExports) {
webpackMap[path] = {
[name]: {
id: idx,
chunks: [],
name: name,
},
};
}
});
moduleExports.then(
asyncModuleExports => {
for (const name in asyncModuleExports) {
webpackMap[path] = {
[name]: {
id: idx,
chunks: [],
name: name,
},
};
}
},
() => {},
);
}
for (const name in moduleExports) {
webpackMap[path] = {
Expand Down