99
1010'use strict' ;
1111
12+ const ReactDOMServerIntegrationUtils = require ( './utils/ReactDOMServerIntegrationTestUtils' ) ;
13+
14+ const TEXT_NODE_TYPE = 3 ;
15+
1216let PropTypes ;
1317let React ;
1418let ReactDOM ;
1519let ReactDOMServer ;
1620let ReactTestUtils ;
1721
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-
31022// When there is a test that renders on server and then on client and expects a logged
31123// error, you want to see the error show up both on server and client. Unfortunately,
31224// React refuses to issue the same error twice to avoid clogging up the console.
31325// To get around this, we must reload React modules in between server and client render.
314- function resetModules ( ) {
26+ function initModules ( ) {
31527 // First, reset the modules to load the client renderer.
31628 jest . resetModuleRegistry ( ) ;
31729
31830 require ( 'shared/ReactFeatureFlags' ) . enableReactFragment = true ;
319-
32031 PropTypes = require ( 'prop-types' ) ;
32132 React = require ( 'react' ) ;
32233 ReactDOM = require ( 'react-dom' ) ;
@@ -328,8 +39,30 @@ function resetModules() {
32839 jest . resetModuleRegistry ( ) ;
32940 require ( 'shared/ReactFeatureFlags' ) . enableReactFragment = true ;
33041 ReactDOMServer = require ( 'react-dom/server' ) ;
42+
43+ // Make them available to the helpers.
44+ return {
45+ React,
46+ ReactDOM,
47+ ReactTestUtils,
48+ ReactDOMServer,
49+ } ;
33150}
33251
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+
33366describe ( 'ReactDOMServerIntegration' , ( ) => {
33467 beforeEach ( ( ) => {
33568 resetModules ( ) ;
0 commit comments