9
9
10
10
'use strict' ;
11
11
12
+ const ReactDOMServerIntegrationUtils = require ( './utils/ReactDOMServerIntegrationTestUtils' ) ;
13
+
14
+ const TEXT_NODE_TYPE = 3 ;
15
+
12
16
let PropTypes ;
13
17
let React ;
14
18
let ReactDOM ;
15
19
let ReactDOMServer ;
16
20
let ReactTestUtils ;
17
21
18
- const stream = require ( 'stream' ) ;
19
-
20
- const TEXT_NODE_TYPE = 3 ;
21
-
22
- // Helper functions for rendering tests
23
- // ====================================
24
-
25
- // promisified version of ReactDOM.render()
26
- function asyncReactDOMRender ( reactElement , domElement , forceHydrate ) {
27
- return new Promise ( resolve => {
28
- if ( forceHydrate ) {
29
- ReactDOM . hydrate ( reactElement , domElement ) ;
30
- } else {
31
- ReactDOM . render ( reactElement , domElement ) ;
32
- }
33
- // We can't use the callback for resolution because that will not catch
34
- // errors. They're thrown.
35
- resolve ( ) ;
36
- } ) ;
37
- }
38
- // performs fn asynchronously and expects count errors logged to console.error.
39
- // will fail the test if the count of errors logged is not equal to count.
40
- async function expectErrors ( fn , count ) {
41
- if ( console . error . calls && console . error . calls . reset ) {
42
- console . error . calls . reset ( ) ;
43
- } else {
44
- spyOnDev ( console , 'error' ) ;
45
- }
46
-
47
- const result = await fn ( ) ;
48
- if (
49
- console . error . calls &&
50
- console . error . calls . count ( ) !== count &&
51
- console . error . calls . count ( ) !== 0
52
- ) {
53
- console . log (
54
- `We expected ${
55
- count
56
- } warning(s), but saw ${ console . error . calls . count ( ) } warning(s).`,
57
- ) ;
58
- if ( console . error . calls . count ( ) > 0 ) {
59
- console . log ( `We saw these warnings:` ) ;
60
- for ( var i = 0 ; i < console . error . calls . count ( ) ; i ++ ) {
61
- console . log ( console . error . calls . argsFor ( i ) [ 0 ] ) ;
62
- }
63
- }
64
- }
65
- if ( __DEV__ ) {
66
- expect ( console . error . calls . count ( ) ) . toBe ( count ) ;
67
- }
68
- return result ;
69
- }
70
-
71
- // renders the reactElement into domElement, and expects a certain number of errors.
72
- // returns a Promise that resolves when the render is complete.
73
- function renderIntoDom ( reactElement , domElement , forceHydrate , errorCount = 0 ) {
74
- return expectErrors ( async ( ) => {
75
- await asyncReactDOMRender ( reactElement , domElement , forceHydrate ) ;
76
- return domElement . firstChild ;
77
- } , errorCount ) ;
78
- }
79
-
80
- async function renderIntoString ( reactElement , errorCount = 0 ) {
81
- return await expectErrors (
82
- ( ) =>
83
- new Promise ( resolve =>
84
- resolve ( ReactDOMServer . renderToString ( reactElement ) ) ,
85
- ) ,
86
- errorCount ,
87
- ) ;
88
- }
89
-
90
- // Renders text using SSR and then stuffs it into a DOM node; returns the DOM
91
- // element that corresponds with the reactElement.
92
- // Does not render on client or perform client-side revival.
93
- async function serverRender ( reactElement , errorCount = 0 ) {
94
- const markup = await renderIntoString ( reactElement , errorCount ) ;
95
- var domElement = document . createElement ( 'div' ) ;
96
- domElement . innerHTML = markup ;
97
- return domElement . firstChild ;
98
- }
99
-
100
- // this just drains a readable piped into it to a string, which can be accessed
101
- // via .buffer.
102
- class DrainWritable extends stream . Writable {
103
- constructor ( options ) {
104
- super ( options ) ;
105
- this . buffer = '' ;
106
- }
107
-
108
- _write ( chunk , encoding , cb ) {
109
- this . buffer += chunk ;
110
- cb ( ) ;
111
- }
112
- }
113
-
114
- async function renderIntoStream ( reactElement , errorCount = 0 ) {
115
- return await expectErrors (
116
- ( ) =>
117
- new Promise ( resolve => {
118
- let writable = new DrainWritable ( ) ;
119
- ReactDOMServer . renderToNodeStream ( reactElement ) . pipe ( writable ) ;
120
- writable . on ( 'finish' , ( ) => resolve ( writable . buffer ) ) ;
121
- } ) ,
122
- errorCount ,
123
- ) ;
124
- }
125
-
126
- // Renders text using node stream SSR and then stuffs it into a DOM node;
127
- // returns the DOM element that corresponds with the reactElement.
128
- // Does not render on client or perform client-side revival.
129
- async function streamRender ( reactElement , errorCount = 0 ) {
130
- const markup = await renderIntoStream ( reactElement , errorCount ) ;
131
- var domElement = document . createElement ( 'div' ) ;
132
- domElement . innerHTML = markup ;
133
- return domElement . firstChild ;
134
- }
135
-
136
- const clientCleanRender = ( element , errorCount = 0 ) => {
137
- const div = document . createElement ( 'div' ) ;
138
- return renderIntoDom ( element , div , false , errorCount ) ;
139
- } ;
140
-
141
- const clientRenderOnServerString = async ( element , errorCount = 0 ) => {
142
- const markup = await renderIntoString ( element , errorCount ) ;
143
- resetModules ( ) ;
144
-
145
- var domElement = document . createElement ( 'div' ) ;
146
- domElement . innerHTML = markup ;
147
- let serverNode = domElement . firstChild ;
148
-
149
- const firstClientNode = await renderIntoDom (
150
- element ,
151
- domElement ,
152
- true ,
153
- errorCount ,
154
- ) ;
155
- let clientNode = firstClientNode ;
156
-
157
- // Make sure all top level nodes match up
158
- while ( serverNode || clientNode ) {
159
- expect ( serverNode != null ) . toBe ( true ) ;
160
- expect ( clientNode != null ) . toBe ( true ) ;
161
- expect ( clientNode . nodeType ) . toBe ( serverNode . nodeType ) ;
162
- // Assert that the DOM element hasn't been replaced.
163
- // Note that we cannot use expect(serverNode).toBe(clientNode) because
164
- // of jest bug #1772.
165
- expect ( serverNode === clientNode ) . toBe ( true ) ;
166
- serverNode = serverNode . nextSibling ;
167
- clientNode = clientNode . nextSibling ;
168
- }
169
- return firstClientNode ;
170
- } ;
171
-
172
- function BadMarkupExpected ( ) { }
173
-
174
- const clientRenderOnBadMarkup = async ( element , errorCount = 0 ) => {
175
- // First we render the top of bad mark up.
176
- var domElement = document . createElement ( 'div' ) ;
177
- domElement . innerHTML =
178
- '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>' ;
179
- await renderIntoDom ( element , domElement , true , errorCount + 1 ) ;
180
-
181
- // This gives us the resulting text content.
182
- var hydratedTextContent = domElement . textContent ;
183
-
184
- // Next we render the element into a clean DOM node client side.
185
- const cleanDomElement = document . createElement ( 'div' ) ;
186
- await asyncReactDOMRender ( element , cleanDomElement , true ) ;
187
- // This gives us the expected text content.
188
- const cleanTextContent = cleanDomElement . textContent ;
189
-
190
- // The only guarantee is that text content has been patched up if needed.
191
- expect ( hydratedTextContent ) . toBe ( cleanTextContent ) ;
192
-
193
- // Abort any further expects. All bets are off at this point.
194
- throw new BadMarkupExpected ( ) ;
195
- } ;
196
-
197
- // runs a DOM rendering test as four different tests, with four different rendering
198
- // scenarios:
199
- // -- render to string on server
200
- // -- render on client without any server markup "clean client render"
201
- // -- render on client on top of good server-generated string markup
202
- // -- render on client on top of bad server-generated markup
203
- //
204
- // testFn is a test that has one arg, which is a render function. the render
205
- // function takes in a ReactElement and an optional expected error count and
206
- // returns a promise of a DOM Element.
207
- //
208
- // You should only perform tests that examine the DOM of the results of
209
- // render; you should not depend on the interactivity of the returned DOM element,
210
- // as that will not work in the server string scenario.
211
- function itRenders ( desc , testFn ) {
212
- it ( `renders ${ desc } with server string render` , ( ) => testFn ( serverRender ) ) ;
213
- it ( `renders ${ desc } with server stream render` , ( ) => testFn ( streamRender ) ) ;
214
- itClientRenders ( desc , testFn ) ;
215
- }
216
-
217
- // run testFn in three different rendering scenarios:
218
- // -- render on client without any server markup "clean client render"
219
- // -- render on client on top of good server-generated string markup
220
- // -- render on client on top of bad server-generated markup
221
- //
222
- // testFn is a test that has one arg, which is a render function. the render
223
- // function takes in a ReactElement and an optional expected error count and
224
- // returns a promise of a DOM Element.
225
- //
226
- // Since all of the renders in this function are on the client, you can test interactivity,
227
- // unlike with itRenders.
228
- function itClientRenders ( desc , testFn ) {
229
- it ( `renders ${ desc } with clean client render` , ( ) =>
230
- testFn ( clientCleanRender ) ) ;
231
- it ( `renders ${ desc } with client render on top of good server markup` , ( ) =>
232
- testFn ( clientRenderOnServerString ) ) ;
233
- it ( `renders ${
234
- desc
235
- } with client render on top of bad server markup`, async ( ) => {
236
- try {
237
- await testFn ( clientRenderOnBadMarkup ) ;
238
- } catch ( x ) {
239
- // We expect this to trigger the BadMarkupExpected rejection.
240
- if ( ! ( x instanceof BadMarkupExpected ) ) {
241
- // If not, rethrow.
242
- throw x ;
243
- }
244
- }
245
- } ) ;
246
- }
247
-
248
- function itThrows ( desc , testFn , partialMessage ) {
249
- it ( `throws ${ desc } ` , ( ) => {
250
- return testFn ( ) . then (
251
- ( ) => expect ( false ) . toBe ( 'The promise resolved and should not have.' ) ,
252
- err => {
253
- expect ( err ) . toBeInstanceOf ( Error ) ;
254
- expect ( err . message ) . toContain ( partialMessage ) ;
255
- } ,
256
- ) ;
257
- } ) ;
258
- }
259
-
260
- function itThrowsWhenRendering ( desc , testFn , partialMessage ) {
261
- itThrows (
262
- `when rendering ${ desc } with server string render` ,
263
- ( ) => testFn ( serverRender ) ,
264
- partialMessage ,
265
- ) ;
266
- itThrows (
267
- `when rendering ${ desc } with clean client render` ,
268
- ( ) => testFn ( clientCleanRender ) ,
269
- partialMessage ,
270
- ) ;
271
-
272
- // we subtract one from the warning count here because the throw means that it won't
273
- // get the usual markup mismatch warning.
274
- itThrows (
275
- `when rendering ${ desc } with client render on top of bad server markup` ,
276
- ( ) =>
277
- testFn ( ( element , warningCount = 0 ) =>
278
- clientRenderOnBadMarkup ( element , warningCount - 1 ) ,
279
- ) ,
280
- partialMessage ,
281
- ) ;
282
- }
283
-
284
- // renders serverElement to a string, sticks it into a DOM element, and then
285
- // tries to render clientElement on top of it. shouldMatch is a boolean
286
- // telling whether we should expect the markup to match or not.
287
- async function testMarkupMatch ( serverElement , clientElement , shouldMatch ) {
288
- const domElement = await serverRender ( serverElement ) ;
289
- resetModules ( ) ;
290
- return renderIntoDom (
291
- clientElement ,
292
- domElement . parentNode ,
293
- true ,
294
- shouldMatch ? 0 : 1 ,
295
- ) ;
296
- }
297
-
298
- // expects that rendering clientElement on top of a server-rendered
299
- // serverElement does NOT raise a markup mismatch warning.
300
- function expectMarkupMatch ( serverElement , clientElement ) {
301
- return testMarkupMatch ( serverElement , clientElement , true ) ;
302
- }
303
-
304
- // expects that rendering clientElement on top of a server-rendered
305
- // serverElement DOES raise a markup mismatch warning.
306
- function expectMarkupMismatch ( serverElement , clientElement ) {
307
- return testMarkupMatch ( serverElement , clientElement , false ) ;
308
- }
309
-
310
22
// When there is a test that renders on server and then on client and expects a logged
311
23
// error, you want to see the error show up both on server and client. Unfortunately,
312
24
// React refuses to issue the same error twice to avoid clogging up the console.
313
25
// To get around this, we must reload React modules in between server and client render.
314
- function resetModules ( ) {
26
+ function initModules ( ) {
315
27
// First, reset the modules to load the client renderer.
316
28
jest . resetModuleRegistry ( ) ;
317
29
318
30
require ( 'shared/ReactFeatureFlags' ) . enableReactFragment = true ;
319
-
320
31
PropTypes = require ( 'prop-types' ) ;
321
32
React = require ( 'react' ) ;
322
33
ReactDOM = require ( 'react-dom' ) ;
@@ -328,8 +39,30 @@ function resetModules() {
328
39
jest . resetModuleRegistry ( ) ;
329
40
require ( 'shared/ReactFeatureFlags' ) . enableReactFragment = true ;
330
41
ReactDOMServer = require ( 'react-dom/server' ) ;
42
+
43
+ // Make them available to the helpers.
44
+ return {
45
+ React,
46
+ ReactDOM,
47
+ ReactTestUtils,
48
+ ReactDOMServer,
49
+ } ;
331
50
}
332
51
52
+ const {
53
+ resetModules,
54
+ expectMarkupMismatch,
55
+ expectMarkupMatch,
56
+ itRenders,
57
+ itClientRenders,
58
+ itThrowsWhenRendering,
59
+ asyncReactDOMRender,
60
+ serverRender,
61
+ clientRenderOnServerString,
62
+ renderIntoDom,
63
+ streamRender,
64
+ } = ReactDOMServerIntegrationUtils ( initModules ) ;
65
+
333
66
describe ( 'ReactDOMServerIntegration' , ( ) => {
334
67
beforeEach ( ( ) => {
335
68
resetModules ( ) ;
0 commit comments