Skip to content

Commit cc809ed

Browse files
committed
Split up ReactDOMServerIntegration into test suite and utilities
This enables us to further split it down. Good both for parallelization and extracting public parts.
1 parent 0a90485 commit cc809ed

File tree

2 files changed

+357
-294
lines changed

2 files changed

+357
-294
lines changed

packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.internal.js

Lines changed: 27 additions & 294 deletions
Original file line numberDiff line numberDiff line change
@@ -9,314 +9,25 @@
99

1010
'use strict';
1111

12+
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13+
14+
const TEXT_NODE_TYPE = 3;
15+
1216
let PropTypes;
1317
let React;
1418
let ReactDOM;
1519
let ReactDOMServer;
1620
let 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+
33366
describe('ReactDOMServerIntegration', () => {
33467
beforeEach(() => {
33568
resetModules();

0 commit comments

Comments
 (0)