Skip to content

Commit 5619560

Browse files
jplhomersebmarkbage
authored andcommitted
tests: add failing test to demonstrate bug in ReadableStream implementation
1 parent 1760b27 commit 5619560

File tree

1 file changed

+143
-30
lines changed

1 file changed

+143
-30
lines changed

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

Lines changed: 143 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,31 @@ describe('ReactFlightDOMBrowser', () => {
6969
}
7070
}
7171

72+
function makeDelayedText(Model) {
73+
let error, _resolve, _reject;
74+
let promise = new Promise((resolve, reject) => {
75+
_resolve = () => {
76+
promise = null;
77+
resolve();
78+
};
79+
_reject = e => {
80+
error = e;
81+
promise = null;
82+
reject(e);
83+
};
84+
});
85+
function DelayedText({children}, data) {
86+
if (promise) {
87+
throw promise;
88+
}
89+
if (error) {
90+
throw error;
91+
}
92+
return <Model>{children}</Model>;
93+
}
94+
return [DelayedText, _resolve, _reject];
95+
}
96+
7297
it('should resolve HTML using W3C streams', async () => {
7398
function Text({children}) {
7499
return <span>{children}</span>;
@@ -174,36 +199,11 @@ describe('ReactFlightDOMBrowser', () => {
174199
return children;
175200
}
176201

177-
function makeDelayedText() {
178-
let error, _resolve, _reject;
179-
let promise = new Promise((resolve, reject) => {
180-
_resolve = () => {
181-
promise = null;
182-
resolve();
183-
};
184-
_reject = e => {
185-
error = e;
186-
promise = null;
187-
reject(e);
188-
};
189-
});
190-
function DelayedText({children}, data) {
191-
if (promise) {
192-
throw promise;
193-
}
194-
if (error) {
195-
throw error;
196-
}
197-
return <Text>{children}</Text>;
198-
}
199-
return [DelayedText, _resolve, _reject];
200-
}
201-
202-
const [Friends, resolveFriends] = makeDelayedText();
203-
const [Name, resolveName] = makeDelayedText();
204-
const [Posts, resolvePosts] = makeDelayedText();
205-
const [Photos, resolvePhotos] = makeDelayedText();
206-
const [Games, , rejectGames] = makeDelayedText();
202+
const [Friends, resolveFriends] = makeDelayedText(Text);
203+
const [Name, resolveName] = makeDelayedText(Text);
204+
const [Posts, resolvePosts] = makeDelayedText(Text);
205+
const [Photos, resolvePhotos] = makeDelayedText(Text);
206+
const [Games, , rejectGames] = makeDelayedText(Text);
207207

208208
// View
209209
function ProfileDetails({avatar}) {
@@ -340,4 +340,117 @@ describe('ReactFlightDOMBrowser', () => {
340340

341341
expect(reportedErrors).toEqual([]);
342342
});
343+
344+
it('should close the stream upon completion when rendering to W3C streams', async () => {
345+
const {Suspense} = React;
346+
347+
// Model
348+
function Text({children}) {
349+
return children;
350+
}
351+
352+
const [Friends, resolveFriends] = makeDelayedText(Text);
353+
const [Name, resolveName] = makeDelayedText(Text);
354+
const [Posts, resolvePosts] = makeDelayedText(Text);
355+
const [Photos, resolvePhotos] = makeDelayedText(Text);
356+
357+
// View
358+
function ProfileDetails({avatar}) {
359+
return (
360+
<div>
361+
<Name>:name:</Name>
362+
{avatar}
363+
</div>
364+
);
365+
}
366+
function ProfileSidebar({friends}) {
367+
return (
368+
<div>
369+
<Photos>:photos:</Photos>
370+
{friends}
371+
</div>
372+
);
373+
}
374+
function ProfilePosts({posts}) {
375+
return <div>{posts}</div>;
376+
}
377+
378+
function ProfileContent() {
379+
return (
380+
<Suspense fallback="(loading everything)">
381+
<ProfileDetails avatar={<Text>:avatar:</Text>} />
382+
<Suspense fallback={<p>(loading sidebar)</p>}>
383+
<ProfileSidebar friends={<Friends>:friends:</Friends>} />
384+
</Suspense>
385+
<Suspense fallback={<p>(loading posts)</p>}>
386+
<ProfilePosts posts={<Posts>:posts:</Posts>} />
387+
</Suspense>
388+
</Suspense>
389+
);
390+
}
391+
392+
const model = {
393+
rootContent: <ProfileContent />,
394+
};
395+
396+
const stream = ReactServerDOMWriter.renderToReadableStream(
397+
model,
398+
webpackMap,
399+
);
400+
401+
const reader = stream.getReader();
402+
const decoder = new TextDecoder();
403+
404+
let flightResponse = '';
405+
let isDone = false;
406+
407+
reader.read().then(function progress({done, value}) {
408+
if (done) {
409+
isDone = true;
410+
return;
411+
}
412+
413+
flightResponse += decoder.decode(value);
414+
415+
return reader.read().then(progress);
416+
});
417+
418+
// Advance time enough to trigger a nested fallback.
419+
jest.advanceTimersByTime(500);
420+
421+
await act(async () => {});
422+
423+
expect(flightResponse).toContain('(loading everything)');
424+
expect(flightResponse).toContain('(loading sidebar)');
425+
expect(flightResponse).toContain('(loading posts)');
426+
expect(flightResponse).not.toContain(':friends:');
427+
expect(flightResponse).not.toContain(':name:');
428+
429+
await act(async () => {
430+
resolveFriends();
431+
});
432+
433+
expect(flightResponse).toContain(':friends:');
434+
435+
await act(async () => {
436+
resolveName();
437+
});
438+
439+
expect(flightResponse).toContain(':name:');
440+
441+
await act(async () => {
442+
resolvePhotos();
443+
});
444+
445+
expect(flightResponse).toContain(':photos:');
446+
447+
await act(async () => {
448+
resolvePosts();
449+
});
450+
451+
expect(flightResponse).toContain(':posts:');
452+
453+
// Final pending chunk is written; stream should be closed.
454+
expect(isDone).toBeTruthy();
455+
});
343456
});

0 commit comments

Comments
 (0)