Skip to content

Commit ff525b6

Browse files
committed
Add regression test
This test doesn't actually error due to the streams polyfill not behaving like Chrome but rather according to spec.
1 parent 4c24dff commit ff525b6

File tree

1 file changed

+264
-1
lines changed

1 file changed

+264
-1
lines changed

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

Lines changed: 264 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*
77
* @emails react-core
8-
* @jest-environment node
98
*/
109

1110
'use strict';
@@ -15,18 +14,45 @@ global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/e
1514
global.TextEncoder = require('util').TextEncoder;
1615
global.TextDecoder = require('util').TextDecoder;
1716

17+
let webpackModuleIdx = 0;
18+
let webpackModules = {};
19+
let webpackMap = {};
20+
global.__webpack_require__ = function(id) {
21+
return webpackModules[id];
22+
};
23+
24+
let act;
1825
let React;
26+
let ReactDOM;
1927
let ReactServerDOMWriter;
2028
let ReactServerDOMReader;
2129

2230
describe('ReactFlightDOMBrowser', () => {
2331
beforeEach(() => {
2432
jest.resetModules();
33+
act = require('jest-react').act;
2534
React = require('react');
35+
ReactDOM = require('react-dom');
2636
ReactServerDOMWriter = require('react-server-dom-webpack/writer.browser.server');
2737
ReactServerDOMReader = require('react-server-dom-webpack');
2838
});
2939

40+
function moduleReference(moduleExport) {
41+
const idx = webpackModuleIdx++;
42+
webpackModules[idx] = {
43+
d: moduleExport,
44+
};
45+
webpackMap['path/' + idx] = {
46+
default: {
47+
id: '' + idx,
48+
chunks: [],
49+
name: 'd',
50+
},
51+
};
52+
const MODULE_TAG = Symbol.for('react.module.reference');
53+
return {$$typeof: MODULE_TAG, filepath: 'path/' + idx, name: 'default'};
54+
}
55+
3056
async function waitForSuspense(fn) {
3157
while (true) {
3258
try {
@@ -75,4 +101,241 @@ describe('ReactFlightDOMBrowser', () => {
75101
});
76102
});
77103
});
104+
105+
it('should resolve HTML using W3C streams', async () => {
106+
function Text({children}) {
107+
return <span>{children}</span>;
108+
}
109+
function HTML() {
110+
return (
111+
<div>
112+
<Text>hello</Text>
113+
<Text>world</Text>
114+
</div>
115+
);
116+
}
117+
118+
function App() {
119+
const model = {
120+
html: <HTML />,
121+
};
122+
return model;
123+
}
124+
125+
const stream = ReactServerDOMWriter.renderToReadableStream(<App />);
126+
const response = ReactServerDOMReader.createFromReadableStream(stream);
127+
await waitForSuspense(() => {
128+
const model = response.readRoot();
129+
expect(model).toEqual({
130+
html: (
131+
<div>
132+
<span>hello</span>
133+
<span>world</span>
134+
</div>
135+
),
136+
});
137+
});
138+
});
139+
140+
it('should progressively reveal server components', async () => {
141+
let reportedErrors = [];
142+
const {Suspense} = React;
143+
144+
// Client Components
145+
146+
class ErrorBoundary extends React.Component {
147+
state = {hasError: false, error: null};
148+
static getDerivedStateFromError(error) {
149+
return {
150+
hasError: true,
151+
error,
152+
};
153+
}
154+
render() {
155+
if (this.state.hasError) {
156+
return this.props.fallback(this.state.error);
157+
}
158+
return this.props.children;
159+
}
160+
}
161+
162+
function MyErrorBoundary({children}) {
163+
return (
164+
<ErrorBoundary fallback={e => <p>{e.message}</p>}>
165+
{children}
166+
</ErrorBoundary>
167+
);
168+
}
169+
170+
// Model
171+
function Text({children}) {
172+
return children;
173+
}
174+
175+
function makeDelayedText() {
176+
let error, _resolve, _reject;
177+
let promise = new Promise((resolve, reject) => {
178+
_resolve = () => {
179+
promise = null;
180+
resolve();
181+
};
182+
_reject = e => {
183+
error = e;
184+
promise = null;
185+
reject(e);
186+
};
187+
});
188+
function DelayedText({children}, data) {
189+
if (promise) {
190+
throw promise;
191+
}
192+
if (error) {
193+
throw error;
194+
}
195+
return <Text>{children}</Text>;
196+
}
197+
return [DelayedText, _resolve, _reject];
198+
}
199+
200+
const [Friends, resolveFriends] = makeDelayedText();
201+
const [Name, resolveName] = makeDelayedText();
202+
const [Posts, resolvePosts] = makeDelayedText();
203+
const [Photos, resolvePhotos] = makeDelayedText();
204+
const [Games, , rejectGames] = makeDelayedText();
205+
206+
// View
207+
function ProfileDetails({avatar}) {
208+
return (
209+
<div>
210+
<Name>:name:</Name>
211+
{avatar}
212+
</div>
213+
);
214+
}
215+
function ProfileSidebar({friends}) {
216+
return (
217+
<div>
218+
<Photos>:photos:</Photos>
219+
{friends}
220+
</div>
221+
);
222+
}
223+
function ProfilePosts({posts}) {
224+
return <div>{posts}</div>;
225+
}
226+
function ProfileGames({games}) {
227+
return <div>{games}</div>;
228+
}
229+
230+
const MyErrorBoundaryClient = moduleReference(MyErrorBoundary);
231+
232+
function ProfileContent() {
233+
return (
234+
<>
235+
<ProfileDetails avatar={<Text>:avatar:</Text>} />
236+
<Suspense fallback={<p>(loading sidebar)</p>}>
237+
<ProfileSidebar friends={<Friends>:friends:</Friends>} />
238+
</Suspense>
239+
<Suspense fallback={<p>(loading posts)</p>}>
240+
<ProfilePosts posts={<Posts>:posts:</Posts>} />
241+
</Suspense>
242+
<MyErrorBoundaryClient>
243+
<Suspense fallback={<p>(loading games)</p>}>
244+
<ProfileGames games={<Games>:games:</Games>} />
245+
</Suspense>
246+
</MyErrorBoundaryClient>
247+
</>
248+
);
249+
}
250+
251+
const model = {
252+
rootContent: <ProfileContent />,
253+
};
254+
255+
function ProfilePage({response}) {
256+
return response.readRoot().rootContent;
257+
}
258+
259+
const stream = ReactServerDOMWriter.renderToReadableStream(
260+
model,
261+
webpackMap,
262+
{
263+
onError(x) {
264+
reportedErrors.push(x);
265+
},
266+
},
267+
);
268+
const response = ReactServerDOMReader.createFromReadableStream(stream);
269+
270+
const container = document.createElement('div');
271+
const root = ReactDOM.createRoot(container);
272+
await act(async () => {
273+
root.render(
274+
<Suspense fallback={<p>(loading)</p>}>
275+
<ProfilePage response={response} />
276+
</Suspense>,
277+
);
278+
});
279+
expect(container.innerHTML).toBe('<p>(loading)</p>');
280+
281+
// This isn't enough to show anything.
282+
await act(async () => {
283+
resolveFriends();
284+
});
285+
expect(container.innerHTML).toBe('<p>(loading)</p>');
286+
287+
// We can now show the details. Sidebar and posts are still loading.
288+
await act(async () => {
289+
resolveName();
290+
});
291+
// Advance time enough to trigger a nested fallback.
292+
jest.advanceTimersByTime(500);
293+
expect(container.innerHTML).toBe(
294+
'<div>:name::avatar:</div>' +
295+
'<p>(loading sidebar)</p>' +
296+
'<p>(loading posts)</p>' +
297+
'<p>(loading games)</p>',
298+
);
299+
300+
expect(reportedErrors).toEqual([]);
301+
302+
const theError = new Error('Game over');
303+
// Let's *fail* loading games.
304+
await act(async () => {
305+
rejectGames(theError);
306+
});
307+
expect(container.innerHTML).toBe(
308+
'<div>:name::avatar:</div>' +
309+
'<p>(loading sidebar)</p>' +
310+
'<p>(loading posts)</p>' +
311+
'<p>Game over</p>', // TODO: should not have message in prod.
312+
);
313+
314+
expect(reportedErrors).toEqual([theError]);
315+
reportedErrors = [];
316+
317+
// We can now show the sidebar.
318+
await act(async () => {
319+
resolvePhotos();
320+
});
321+
expect(container.innerHTML).toBe(
322+
'<div>:name::avatar:</div>' +
323+
'<div>:photos::friends:</div>' +
324+
'<p>(loading posts)</p>' +
325+
'<p>Game over</p>', // TODO: should not have message in prod.
326+
);
327+
328+
// Show everything.
329+
await act(async () => {
330+
resolvePosts();
331+
});
332+
expect(container.innerHTML).toBe(
333+
'<div>:name::avatar:</div>' +
334+
'<div>:photos::friends:</div>' +
335+
'<div>:posts:</div>' +
336+
'<p>Game over</p>', // TODO: should not have message in prod.
337+
);
338+
339+
expect(reportedErrors).toEqual([]);
340+
});
78341
});

0 commit comments

Comments
 (0)