5
5
* LICENSE file in the root directory of this source tree.
6
6
*
7
7
* @emails react-core
8
- * @jest -environment node
9
8
*/
10
9
11
10
'use strict' ;
@@ -15,18 +14,47 @@ global.ReadableStream = require('@mattiasbuelens/web-streams-polyfill/ponyfill/e
15
14
global . TextEncoder = require ( 'util' ) . TextEncoder ;
16
15
global . TextDecoder = require ( 'util' ) . TextDecoder ;
17
16
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 ;
18
25
let React ;
26
+ let ReactDOM ;
19
27
let ReactServerDOMWriter ;
20
28
let ReactServerDOMReader ;
21
29
22
30
describe ( 'ReactFlightDOMBrowser' , ( ) => {
23
31
beforeEach ( ( ) => {
24
32
jest . resetModules ( ) ;
33
+ webpackModules = { } ;
34
+ webpackMap = { } ;
35
+ act = require ( 'jest-react' ) . act ;
25
36
React = require ( 'react' ) ;
37
+ ReactDOM = require ( 'react-dom' ) ;
26
38
ReactServerDOMWriter = require ( 'react-server-dom-webpack/writer.browser.server' ) ;
27
39
ReactServerDOMReader = require ( 'react-server-dom-webpack' ) ;
28
40
} ) ;
29
41
42
+ function moduleReference ( moduleExport ) {
43
+ const idx = webpackModuleIdx ++ ;
44
+ webpackModules [ idx ] = {
45
+ d : moduleExport ,
46
+ } ;
47
+ webpackMap [ 'path/' + idx ] = {
48
+ default : {
49
+ id : '' + idx ,
50
+ chunks : [ ] ,
51
+ name : 'd' ,
52
+ } ,
53
+ } ;
54
+ const MODULE_TAG = Symbol . for ( 'react.module.reference' ) ;
55
+ return { $$typeof : MODULE_TAG , filepath : 'path/' + idx , name : 'default' } ;
56
+ }
57
+
30
58
async function waitForSuspense ( fn ) {
31
59
while ( true ) {
32
60
try {
@@ -75,4 +103,241 @@ describe('ReactFlightDOMBrowser', () => {
75
103
} ) ;
76
104
} ) ;
77
105
} ) ;
106
+
107
+ it ( 'should resolve HTML using W3C streams' , async ( ) => {
108
+ function Text ( { children} ) {
109
+ return < span > { children } </ span > ;
110
+ }
111
+ function HTML ( ) {
112
+ return (
113
+ < div >
114
+ < Text > hello</ Text >
115
+ < Text > world</ Text >
116
+ </ div >
117
+ ) ;
118
+ }
119
+
120
+ function App ( ) {
121
+ const model = {
122
+ html : < HTML /> ,
123
+ } ;
124
+ return model ;
125
+ }
126
+
127
+ const stream = ReactServerDOMWriter . renderToReadableStream ( < App /> ) ;
128
+ const response = ReactServerDOMReader . createFromReadableStream ( stream ) ;
129
+ await waitForSuspense ( ( ) => {
130
+ const model = response . readRoot ( ) ;
131
+ expect ( model ) . toEqual ( {
132
+ html : (
133
+ < div >
134
+ < span > hello</ span >
135
+ < span > world</ span >
136
+ </ div >
137
+ ) ,
138
+ } ) ;
139
+ } ) ;
140
+ } ) ;
141
+
142
+ it ( 'should progressively reveal server components' , async ( ) => {
143
+ let reportedErrors = [ ] ;
144
+ const { Suspense} = React ;
145
+
146
+ // Client Components
147
+
148
+ class ErrorBoundary extends React . Component {
149
+ state = { hasError : false , error : null } ;
150
+ static getDerivedStateFromError ( error ) {
151
+ return {
152
+ hasError : true ,
153
+ error,
154
+ } ;
155
+ }
156
+ render ( ) {
157
+ if ( this . state . hasError ) {
158
+ return this . props . fallback ( this . state . error ) ;
159
+ }
160
+ return this . props . children ;
161
+ }
162
+ }
163
+
164
+ function MyErrorBoundary ( { children} ) {
165
+ return (
166
+ < ErrorBoundary fallback = { e => < p > { e . message } </ p > } >
167
+ { children }
168
+ </ ErrorBoundary >
169
+ ) ;
170
+ }
171
+
172
+ // Model
173
+ function Text ( { children} ) {
174
+ return children ;
175
+ }
176
+
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 ( ) ;
207
+
208
+ // View
209
+ function ProfileDetails ( { avatar} ) {
210
+ return (
211
+ < div >
212
+ < Name > :name:</ Name >
213
+ { avatar }
214
+ </ div >
215
+ ) ;
216
+ }
217
+ function ProfileSidebar ( { friends} ) {
218
+ return (
219
+ < div >
220
+ < Photos > :photos:</ Photos >
221
+ { friends }
222
+ </ div >
223
+ ) ;
224
+ }
225
+ function ProfilePosts ( { posts} ) {
226
+ return < div > { posts } </ div > ;
227
+ }
228
+ function ProfileGames ( { games} ) {
229
+ return < div > { games } </ div > ;
230
+ }
231
+
232
+ const MyErrorBoundaryClient = moduleReference ( MyErrorBoundary ) ;
233
+
234
+ function ProfileContent ( ) {
235
+ return (
236
+ < >
237
+ < ProfileDetails avatar = { < Text > :avatar:</ Text > } />
238
+ < Suspense fallback = { < p > (loading sidebar)</ p > } >
239
+ < ProfileSidebar friends = { < Friends > :friends:</ Friends > } />
240
+ </ Suspense >
241
+ < Suspense fallback = { < p > (loading posts)</ p > } >
242
+ < ProfilePosts posts = { < Posts > :posts:</ Posts > } />
243
+ </ Suspense >
244
+ < MyErrorBoundaryClient >
245
+ < Suspense fallback = { < p > (loading games)</ p > } >
246
+ < ProfileGames games = { < Games > :games:</ Games > } />
247
+ </ Suspense >
248
+ </ MyErrorBoundaryClient >
249
+ </ >
250
+ ) ;
251
+ }
252
+
253
+ const model = {
254
+ rootContent : < ProfileContent /> ,
255
+ } ;
256
+
257
+ function ProfilePage ( { response} ) {
258
+ return response . readRoot ( ) . rootContent ;
259
+ }
260
+
261
+ const stream = ReactServerDOMWriter . renderToReadableStream (
262
+ model ,
263
+ webpackMap ,
264
+ {
265
+ onError ( x ) {
266
+ reportedErrors . push ( x ) ;
267
+ } ,
268
+ } ,
269
+ ) ;
270
+ const response = ReactServerDOMReader . createFromReadableStream ( stream ) ;
271
+
272
+ const container = document . createElement ( 'div' ) ;
273
+ const root = ReactDOM . createRoot ( container ) ;
274
+ await act ( async ( ) => {
275
+ root . render (
276
+ < Suspense fallback = { < p > (loading)</ p > } >
277
+ < ProfilePage response = { response } />
278
+ </ Suspense > ,
279
+ ) ;
280
+ } ) ;
281
+ expect ( container . innerHTML ) . toBe ( '<p>(loading)</p>' ) ;
282
+
283
+ // This isn't enough to show anything.
284
+ await act ( async ( ) => {
285
+ resolveFriends ( ) ;
286
+ } ) ;
287
+ expect ( container . innerHTML ) . toBe ( '<p>(loading)</p>' ) ;
288
+
289
+ // We can now show the details. Sidebar and posts are still loading.
290
+ await act ( async ( ) => {
291
+ resolveName ( ) ;
292
+ } ) ;
293
+ // Advance time enough to trigger a nested fallback.
294
+ jest . advanceTimersByTime ( 500 ) ;
295
+ expect ( container . innerHTML ) . toBe (
296
+ '<div>:name::avatar:</div>' +
297
+ '<p>(loading sidebar)</p>' +
298
+ '<p>(loading posts)</p>' +
299
+ '<p>(loading games)</p>' ,
300
+ ) ;
301
+
302
+ expect ( reportedErrors ) . toEqual ( [ ] ) ;
303
+
304
+ const theError = new Error ( 'Game over' ) ;
305
+ // Let's *fail* loading games.
306
+ await act ( async ( ) => {
307
+ rejectGames ( theError ) ;
308
+ } ) ;
309
+ expect ( container . innerHTML ) . toBe (
310
+ '<div>:name::avatar:</div>' +
311
+ '<p>(loading sidebar)</p>' +
312
+ '<p>(loading posts)</p>' +
313
+ '<p>Game over</p>' , // TODO: should not have message in prod.
314
+ ) ;
315
+
316
+ expect ( reportedErrors ) . toEqual ( [ theError ] ) ;
317
+ reportedErrors = [ ] ;
318
+
319
+ // We can now show the sidebar.
320
+ await act ( async ( ) => {
321
+ resolvePhotos ( ) ;
322
+ } ) ;
323
+ expect ( container . innerHTML ) . toBe (
324
+ '<div>:name::avatar:</div>' +
325
+ '<div>:photos::friends:</div>' +
326
+ '<p>(loading posts)</p>' +
327
+ '<p>Game over</p>' , // TODO: should not have message in prod.
328
+ ) ;
329
+
330
+ // Show everything.
331
+ await act ( async ( ) => {
332
+ resolvePosts ( ) ;
333
+ } ) ;
334
+ expect ( container . innerHTML ) . toBe (
335
+ '<div>:name::avatar:</div>' +
336
+ '<div>:photos::friends:</div>' +
337
+ '<div>:posts:</div>' +
338
+ '<p>Game over</p>' , // TODO: should not have message in prod.
339
+ ) ;
340
+
341
+ expect ( reportedErrors ) . toEqual ( [ ] ) ;
342
+ } ) ;
78
343
} ) ;
0 commit comments