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,45 @@ 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
+ act = require ( 'jest-react' ) . act ;
25
34
React = require ( 'react' ) ;
35
+ ReactDOM = require ( 'react-dom' ) ;
26
36
ReactServerDOMWriter = require ( 'react-server-dom-webpack/writer.browser.server' ) ;
27
37
ReactServerDOMReader = require ( 'react-server-dom-webpack' ) ;
28
38
} ) ;
29
39
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
+
30
56
async function waitForSuspense ( fn ) {
31
57
while ( true ) {
32
58
try {
@@ -75,4 +101,241 @@ describe('ReactFlightDOMBrowser', () => {
75
101
} ) ;
76
102
} ) ;
77
103
} ) ;
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
+ } ) ;
78
341
} ) ;
0 commit comments