@samitouri / QOS-React / commits / fa7a97fc46

Run 90% of tests on compiled bundles (both development and production) (#11633)

* Extract Jest config into a separate file * Refactor Jest scripts directory structure Introduces a more consistent naming scheme. * Add yarn test-bundles and yarn test-prod-bundles Only files ending with -test.public.js are opted in (so far we don't have any). * Fix error decoding for production bundles GCC seems to remove `new` from `new Error()` which broke our proxy. * Build production version of react-noop-renderer This lets us test more bundles. * Switch to blacklist (exclude .private.js tests) * Rename tests that are currently broken against bundles to *-test.internal.js Some of these are using private APIs. Some have other issues. * Add bundle tests to CI * Split private and public ReactJSXElementValidator tests * Remove internal deps from ReactServerRendering-test and make it public * Only run tests directly in __tests__ This lets us share code between test files by placing them in __tests__/utils. * Remove ExecutionEnvironment dependency from DOMServerIntegrationTest It's not necessary since Stack. * Split up ReactDOMServerIntegration into test suite and utilities This enables us to further split it down. Good both for parallelization and extracting public parts. * Split Fragment tests from other DOMServerIntegration tests This enables them to opt other DOMServerIntegration tests into bundle testing. * Split ReactDOMServerIntegration into different test files It was way too slow to run all these in sequence. * Don't reset the cache twice in DOMServerIntegration tests We used to do this to simulate testing separate bundles. But now we actually *do* test bundles. So there is no need for this, as it makes tests slower. * Rename test-bundles* commands to test-build* Also add test-prod-build as alias for test-build-prod because I keep messing them up. * Use regenerator polyfill for react-noop This fixes other issues and finally lets us run ReactNoop tests against a prod bundle. * Run most Incremental tests against bundles Now that GCC generator issue is fixed, we can do this. I split ErrorLogging test separately because it does mocking. Other error handling tests don't need it. * Update sizes * Fix ReactMount test * Enable ReactDOMComponent test * Fix a warning issue uncovered by flat bundle testing With flat bundles, we couldn't produce a good warning for <div onclick={}> on SSR because it doesn't use the event system. However the issue was not visible in normal Jest runs because the event plugins have been injected by the time the test ran. To solve this, I am explicitly passing whether event system is available as an argument to the hook. This makes the behavior consistent between source and bundle tests. Then I change the tests to document the actual logic and _attempt_ to show a nice message (e.g. we know for sure `onclick` is a bad event but we don't know the right name for it on the server so we just say a generic message about camelCase naming convention).

Dan Abramov committed Nov 23, 2017 at 17:44 UTC fa7a97fc46935e1611d52da2fdb7d53f6ab9577d
65 files changed +5081 -3681
package.json
+6 -31
@@ -35,6 +35,7 @@
35 "babel-plugin-transform-es3-property-literals": "^6.5.0",
36 "babel-plugin-transform-object-rest-spread": "^6.6.5",
37 "babel-plugin-transform-react-jsx-source": "^6.8.0",
38 + "babel-plugin-transform-regenerator": "^6.26.0",
39 "babel-preset-react": "^6.5.0",
40 "babel-traverse": "^6.9.0",
41 "babylon": "6.15.0",
@@ -103,40 +104,14 @@
104 "linc": "node ./scripts/tasks/linc.js",
105 "lint": "node ./scripts/tasks/eslint.js",
106 "postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
106 - "test": "cross-env NODE_ENV=development jest",
107 - "test-prod": "cross-env NODE_ENV=production jest",
107 + "test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js",
108 + "test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
109 + "test-prod-build": "yarn test-build-prod",
110 + "test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
111 + "test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
112 "flow": "node ./scripts/tasks/flow.js",
113 "prettier": "node ./scripts/prettier/index.js write-changed",
114 "prettier-all": "node ./scripts/prettier/index.js write",
115 "version-check": "node ./scripts/tasks/version-check.js"
112 - },
113 - "jest": {
114 - "modulePathIgnorePatterns": [
115 - "<rootDir>/scripts/rollup/shims/",
116 - "<rootDir>/scripts/bench/"
117 - ],
118 - "transform": {
119 - ".*": "./scripts/jest/preprocessor.js"
120 - },
121 - "setupFiles": [
122 - "./scripts/jest/environment.js"
123 - ],
124 - "setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js",
125 - "testRegex": "/__tests__/.*(\\.js|\\.coffee|[^d]\\.ts)$",
126 - "moduleFileExtensions": [
127 - "js",
128 - "json",
129 - "node",
130 - "coffee",
131 - "ts"
132 - ],
133 - "roots": [
134 - "<rootDir>/packages",
135 - "<rootDir>/scripts"
136 - ],
137 - "collectCoverageFrom": [
138 - "packages/**/*.js"
139 - ],
140 - "timers": "fake"
116 }
117 }
packages/events/__tests__/EventPluginRegistry-test.internal.js renamed
packages/events/__tests__/ResponderEventPlugin-test.internal.js renamed
packages/events/__tests__/accumulateInto-test.internal.js renamed
packages/react-cs-renderer/src/__tests__/ReactNativeCS-test.internal.js renamed
packages/react-dom/src/__tests__/ReactBrowserEventEmitter-test.internal.js renamed
packages/react-dom/src/__tests__/ReactDOMComponent-test.js
+24 -9
@@ -1709,15 +1709,25 @@ describe('ReactDOMComponent', () => {
1709 it('should warn about incorrect casing on event handlers (ssr)', () => {
1710 spyOnDev(console, 'error');
1711 ReactDOMServer.renderToString(
1712 - React.createElement('input', {type: 'text', onclick: '1'}),
1712 + React.createElement('input', {type: 'text', oninput: '1'}),
1713 );
1714 ReactDOMServer.renderToString(
1715 React.createElement('input', {type: 'text', onKeydown: '1'}),
1716 );
1717 if (__DEV__) {
1718 - expect(console.error.calls.count()).toBe(2);
1719 - expect(console.error.calls.argsFor(0)[0]).toContain('onClick');
1720 - expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown');
1718 + expect(console.error.calls.count()).toBe(1);
1719 + expect(console.error.calls.argsFor(0)[0]).toContain(
1720 + 'Invalid event handler property `oninput`. ' +
1721 + 'React events use the camelCase naming convention, ' +
1722 + // Note: we don't know the right event name so we
1723 + // use a generic one (onClick) as a suggestion.
1724 + // This is because we don't bundle the event system
1725 + // on the server.
1726 + 'for example `onClick`.',
1727 + );
1728 + // We can't warn for `onKeydown` on the server because
1729 + // there is no way tell if this is a valid event or not
1730 + // without access to the event system (which we don't bundle).
1731 }
1732 });
1733
@@ -1735,14 +1745,14 @@ describe('ReactDOMComponent', () => {
1745 it('should warn about incorrect casing on event handlers', () => {
1746 spyOnDev(console, 'error');
1747 ReactTestUtils.renderIntoDocument(
1738 - React.createElement('input', {type: 'text', onclick: '1'}),
1748 + React.createElement('input', {type: 'text', oninput: '1'}),
1749 );
1750 ReactTestUtils.renderIntoDocument(
1751 React.createElement('input', {type: 'text', onKeydown: '1'}),
1752 );
1753 if (__DEV__) {
1754 expect(console.error.calls.count()).toBe(2);
1745 - expect(console.error.calls.argsFor(0)[0]).toContain('onClick');
1755 + expect(console.error.calls.argsFor(0)[0]).toContain('onInput');
1756 expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown');
1757 }
1758 });
@@ -1860,15 +1870,20 @@ describe('ReactDOMComponent', () => {
1870 it('gives source code refs for unknown prop warning (ssr)', () => {
1871 spyOnDev(console, 'error');
1872 ReactDOMServer.renderToString(<div class="paladin" />);
1863 - ReactDOMServer.renderToString(<input type="text" onclick="1" />);
1873 + ReactDOMServer.renderToString(<input type="text" oninput="1" />);
1874 if (__DEV__) {
1875 expect(console.error.calls.count()).toBe(2);
1876 expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
1877 'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)',
1878 );
1879 expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe(
1870 - 'Warning: Invalid event handler property `onclick`. Did you mean ' +
1871 - '`onClick`?\n in input (at **)',
1880 + 'Warning: Invalid event handler property `oninput`. ' +
1881 + // Note: we don't know the right event name so we
1882 + // use a generic one (onClick) as a suggestion.
1883 + // This is because we don't bundle the event system
1884 + // on the server.
1885 + 'React events use the camelCase naming convention, for example `onClick`.' +
1886 + '\n in input (at **)',
1887 );
1888 }
1889 });
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js renamed
packages/react-dom/src/__tests__/ReactDOMRoot-test.internal.js renamed
packages/react-dom/src/__tests__/ReactDOMSelection-test.internal.js renamed
packages/react-dom/src/__tests__/ReactDOMServerIntegration-test.js deleted
-3150
@@ -1,3150 +0,0 @@
1 -/**
2 - * Copyright (c) 2013-present, Facebook, Inc.
3 - *
4 - * This source code is licensed under the MIT license found in the
5 - * LICENSE file in the root directory of this source tree.
6 - *
7 - * @emails react-core
8 - */
9 -
10 -'use strict';
11 -
12 -let ExecutionEnvironment;
13 -let PropTypes;
14 -let React;
15 -let ReactDOM;
16 -let ReactDOMServer;
17 -let ReactTestUtils;
18 -
19 -const stream = require('stream');
20 -
21 -const TEXT_NODE_TYPE = 3;
22 -
23 -// Helper functions for rendering tests
24 -// ====================================
25 -
26 -// promisified version of ReactDOM.render()
27 -function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
28 - return new Promise(resolve => {
29 - if (forceHydrate) {
30 - ReactDOM.hydrate(reactElement, domElement);
31 - } else {
32 - ReactDOM.render(reactElement, domElement);
33 - }
34 - // We can't use the callback for resolution because that will not catch
35 - // errors. They're thrown.
36 - resolve();
37 - });
38 -}
39 -// performs fn asynchronously and expects count errors logged to console.error.
40 -// will fail the test if the count of errors logged is not equal to count.
41 -async function expectErrors(fn, count) {
42 - if (console.error.calls && console.error.calls.reset) {
43 - console.error.calls.reset();
44 - } else {
45 - spyOnDev(console, 'error');
46 - }
47 -
48 - const result = await fn();
49 - if (
50 - console.error.calls &&
51 - console.error.calls.count() !== count &&
52 - console.error.calls.count() !== 0
53 - ) {
54 - console.log(
55 - `We expected ${
56 - count
57 - } warning(s), but saw ${console.error.calls.count()} warning(s).`,
58 - );
59 - if (console.error.calls.count() > 0) {
60 - console.log(`We saw these warnings:`);
61 - for (var i = 0; i < console.error.calls.count(); i++) {
62 - console.log(console.error.calls.argsFor(i)[0]);
63 - }
64 - }
65 - }
66 - if (__DEV__) {
67 - expect(console.error.calls.count()).toBe(count);
68 - }
69 - return result;
70 -}
71 -
72 -// renders the reactElement into domElement, and expects a certain number of errors.
73 -// returns a Promise that resolves when the render is complete.
74 -function renderIntoDom(reactElement, domElement, forceHydrate, errorCount = 0) {
75 - return expectErrors(async () => {
76 - ExecutionEnvironment.canUseDOM = true;
77 - await asyncReactDOMRender(reactElement, domElement, forceHydrate);
78 - ExecutionEnvironment.canUseDOM = false;
79 - return domElement.firstChild;
80 - }, errorCount);
81 -}
82 -
83 -async function renderIntoString(reactElement, errorCount = 0) {
84 - return await expectErrors(
85 - () =>
86 - new Promise(resolve =>
87 - resolve(ReactDOMServer.renderToString(reactElement)),
88 - ),
89 - errorCount,
90 - );
91 -}
92 -
93 -// Renders text using SSR and then stuffs it into a DOM node; returns the DOM
94 -// element that corresponds with the reactElement.
95 -// Does not render on client or perform client-side revival.
96 -async function serverRender(reactElement, errorCount = 0) {
97 - const markup = await renderIntoString(reactElement, errorCount);
98 - var domElement = document.createElement('div');
99 - domElement.innerHTML = markup;
100 - return domElement.firstChild;
101 -}
102 -
103 -// this just drains a readable piped into it to a string, which can be accessed
104 -// via .buffer.
105 -class DrainWritable extends stream.Writable {
106 - constructor(options) {
107 - super(options);
108 - this.buffer = '';
109 - }
110 -
111 - _write(chunk, encoding, cb) {
112 - this.buffer += chunk;
113 - cb();
114 - }
115 -}
116 -
117 -async function renderIntoStream(reactElement, errorCount = 0) {
118 - return await expectErrors(
119 - () =>
120 - new Promise(resolve => {
121 - let writable = new DrainWritable();
122 - ReactDOMServer.renderToNodeStream(reactElement).pipe(writable);
123 - writable.on('finish', () => resolve(writable.buffer));
124 - }),
125 - errorCount,
126 - );
127 -}
128 -
129 -// Renders text using node stream SSR and then stuffs it into a DOM node;
130 -// returns the DOM element that corresponds with the reactElement.
131 -// Does not render on client or perform client-side revival.
132 -async function streamRender(reactElement, errorCount = 0) {
133 - const markup = await renderIntoStream(reactElement, errorCount);
134 - var domElement = document.createElement('div');
135 - domElement.innerHTML = markup;
136 - return domElement.firstChild;
137 -}
138 -
139 -const clientCleanRender = (element, errorCount = 0) => {
140 - const div = document.createElement('div');
141 - return renderIntoDom(element, div, false, errorCount);
142 -};
143 -
144 -const clientRenderOnServerString = async (element, errorCount = 0) => {
145 - const markup = await renderIntoString(element, errorCount);
146 - resetModules();
147 -
148 - var domElement = document.createElement('div');
149 - domElement.innerHTML = markup;
150 - let serverNode = domElement.firstChild;
151 -
152 - const firstClientNode = await renderIntoDom(
153 - element,
154 - domElement,
155 - true,
156 - errorCount,
157 - );
158 - let clientNode = firstClientNode;
159 -
160 - // Make sure all top level nodes match up
161 - while (serverNode || clientNode) {
162 - expect(serverNode != null).toBe(true);
163 - expect(clientNode != null).toBe(true);
164 - expect(clientNode.nodeType).toBe(serverNode.nodeType);
165 - // Assert that the DOM element hasn't been replaced.
166 - // Note that we cannot use expect(serverNode).toBe(clientNode) because
167 - // of jest bug #1772.
168 - expect(serverNode === clientNode).toBe(true);
169 - serverNode = serverNode.nextSibling;
170 - clientNode = clientNode.nextSibling;
171 - }
172 - return firstClientNode;
173 -};
174 -
175 -function BadMarkupExpected() {}
176 -
177 -const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
178 - // First we render the top of bad mark up.
179 - var domElement = document.createElement('div');
180 - domElement.innerHTML =
181 - '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
182 - await renderIntoDom(element, domElement, true, errorCount + 1);
183 -
184 - // This gives us the resulting text content.
185 - var hydratedTextContent = domElement.textContent;
186 -
187 - // Next we render the element into a clean DOM node client side.
188 - const cleanDomElement = document.createElement('div');
189 - ExecutionEnvironment.canUseDOM = true;
190 - await asyncReactDOMRender(element, cleanDomElement, true);
191 - ExecutionEnvironment.canUseDOM = false;
192 - // This gives us the expected text content.
193 - const cleanTextContent = cleanDomElement.textContent;
194 -
195 - // The only guarantee is that text content has been patched up if needed.
196 - expect(hydratedTextContent).toBe(cleanTextContent);
197 -
198 - // Abort any further expects. All bets are off at this point.
199 - throw new BadMarkupExpected();
200 -};
201 -
202 -// runs a DOM rendering test as four different tests, with four different rendering
203 -// scenarios:
204 -// -- render to string on server
205 -// -- render on client without any server markup "clean client render"
206 -// -- render on client on top of good server-generated string markup
207 -// -- render on client on top of bad server-generated markup
208 -//
209 -// testFn is a test that has one arg, which is a render function. the render
210 -// function takes in a ReactElement and an optional expected error count and
211 -// returns a promise of a DOM Element.
212 -//
213 -// You should only perform tests that examine the DOM of the results of
214 -// render; you should not depend on the interactivity of the returned DOM element,
215 -// as that will not work in the server string scenario.
216 -function itRenders(desc, testFn) {
217 - it(`renders ${desc} with server string render`, () => testFn(serverRender));
218 - it(`renders ${desc} with server stream render`, () => testFn(streamRender));
219 - itClientRenders(desc, testFn);
220 -}
221 -
222 -// run testFn in three different rendering scenarios:
223 -// -- render on client without any server markup "clean client render"
224 -// -- render on client on top of good server-generated string markup
225 -// -- render on client on top of bad server-generated markup
226 -//
227 -// testFn is a test that has one arg, which is a render function. the render
228 -// function takes in a ReactElement and an optional expected error count and
229 -// returns a promise of a DOM Element.
230 -//
231 -// Since all of the renders in this function are on the client, you can test interactivity,
232 -// unlike with itRenders.
233 -function itClientRenders(desc, testFn) {
234 - it(`renders ${desc} with clean client render`, () =>
235 - testFn(clientCleanRender));
236 - it(`renders ${desc} with client render on top of good server markup`, () =>
237 - testFn(clientRenderOnServerString));
238 - it(`renders ${
239 - desc
240 - } with client render on top of bad server markup`, async () => {
241 - try {
242 - await testFn(clientRenderOnBadMarkup);
243 - } catch (x) {
244 - // We expect this to trigger the BadMarkupExpected rejection.
245 - if (!(x instanceof BadMarkupExpected)) {
246 - // If not, rethrow.
247 - throw x;
248 - }
249 - }
250 - });
251 -}
252 -
253 -function itThrows(desc, testFn, partialMessage) {
254 - it(`throws ${desc}`, () => {
255 - return testFn().then(
256 - () => expect(false).toBe('The promise resolved and should not have.'),
257 - err => {
258 - expect(err).toBeInstanceOf(Error);
259 - expect(err.message).toContain(partialMessage);
260 - },
261 - );
262 - });
263 -}
264 -
265 -function itThrowsWhenRendering(desc, testFn, partialMessage) {
266 - itThrows(
267 - `when rendering ${desc} with server string render`,
268 - () => testFn(serverRender),
269 - partialMessage,
270 - );
271 - itThrows(
272 - `when rendering ${desc} with clean client render`,
273 - () => testFn(clientCleanRender),
274 - partialMessage,
275 - );
276 -
277 - // we subtract one from the warning count here because the throw means that it won't
278 - // get the usual markup mismatch warning.
279 - itThrows(
280 - `when rendering ${desc} with client render on top of bad server markup`,
281 - () =>
282 - testFn((element, warningCount = 0) =>
283 - clientRenderOnBadMarkup(element, warningCount - 1),
284 - ),
285 - partialMessage,
286 - );
287 -}
288 -
289 -// renders serverElement to a string, sticks it into a DOM element, and then
290 -// tries to render clientElement on top of it. shouldMatch is a boolean
291 -// telling whether we should expect the markup to match or not.
292 -async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
293 - const domElement = await serverRender(serverElement);
294 - resetModules();
295 - return renderIntoDom(
296 - clientElement,
297 - domElement.parentNode,
298 - true,
299 - shouldMatch ? 0 : 1,
300 - );
301 -}
302 -
303 -// expects that rendering clientElement on top of a server-rendered
304 -// serverElement does NOT raise a markup mismatch warning.
305 -function expectMarkupMatch(serverElement, clientElement) {
306 - return testMarkupMatch(serverElement, clientElement, true);
307 -}
308 -
309 -// expects that rendering clientElement on top of a server-rendered
310 -// serverElement DOES raise a markup mismatch warning.
311 -function expectMarkupMismatch(serverElement, clientElement) {
312 - return testMarkupMatch(serverElement, clientElement, false);
313 -}
314 -
315 -// When there is a test that renders on server and then on client and expects a logged
316 -// error, you want to see the error show up both on server and client. Unfortunately,
317 -// React refuses to issue the same error twice to avoid clogging up the console.
318 -// To get around this, we must reload React modules in between server and client render.
319 -function resetModules() {
320 - // First, reset the modules to load the client renderer.
321 - jest.resetModuleRegistry();
322 -
323 - // TODO: can we express this test with only public API?
324 - ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
325 - require('shared/ReactFeatureFlags').enableReactFragment = true;
326 -
327 - PropTypes = require('prop-types');
328 - React = require('react');
329 - ReactDOM = require('react-dom');
330 - ReactTestUtils = require('react-dom/test-utils');
331 -
332 - // Now we reset the modules again to load the server renderer.
333 - // Resetting is important because we want to avoid any shared state
334 - // influencing the tests.
335 - jest.resetModuleRegistry();
336 - require('shared/ReactFeatureFlags').enableReactFragment = true;
337 - ReactDOMServer = require('react-dom/server');
338 -}
339 -
340 -describe('ReactDOMServerIntegration', () => {
341 - beforeEach(() => {
342 - resetModules();
343 -
344 - ExecutionEnvironment.canUseDOM = false;
345 - });
346 -
347 - describe('basic rendering', function() {
348 - itRenders('a blank div', async render => {
349 - const e = await render(<div />);
350 - expect(e.tagName).toBe('DIV');
351 - });
352 -
353 - itRenders('a self-closing tag', async render => {
354 - const e = await render(<br />);
355 - expect(e.tagName).toBe('BR');
356 - });
357 -
358 - itRenders('a self-closing tag as a child', async render => {
359 - const e = await render(
360 - <div>
361 - <br />
362 - </div>,
363 - );
364 - expect(e.childNodes.length).toBe(1);
365 - expect(e.firstChild.tagName).toBe('BR');
366 - });
367 -
368 - itRenders('a string', async render => {
369 - let e = await render('Hello');
370 - expect(e.nodeType).toBe(3);
371 - expect(e.nodeValue).toMatch('Hello');
372 - });
373 -
374 - itRenders('a number', async render => {
375 - let e = await render(42);
376 - expect(e.nodeType).toBe(3);
377 - expect(e.nodeValue).toMatch('42');
378 - });
379 -
380 - itRenders('an array with one child', async render => {
381 - let e = await render([<div key={1}>text1</div>]);
382 - let parent = e.parentNode;
383 - expect(parent.childNodes[0].tagName).toBe('DIV');
384 - });
385 -
386 - itRenders('an array with several children', async render => {
387 - let Header = props => {
388 - return <p>header</p>;
389 - };
390 - let Footer = props => {
391 - return [<h2 key={1}>footer</h2>, <h3 key={2}>about</h3>];
392 - };
393 - let e = await render([
394 - <div key={1}>text1</div>,
395 - <span key={2}>text2</span>,
396 - <Header key={3} />,
397 - <Footer key={4} />,
398 - ]);
399 - let parent = e.parentNode;
400 - expect(parent.childNodes[0].tagName).toBe('DIV');
401 - expect(parent.childNodes[1].tagName).toBe('SPAN');
402 - expect(parent.childNodes[2].tagName).toBe('P');
403 - expect(parent.childNodes[3].tagName).toBe('H2');
404 - expect(parent.childNodes[4].tagName).toBe('H3');
405 - });
406 -
407 - itRenders('a nested array', async render => {
408 - let e = await render([
409 - [<div key={1}>text1</div>],
410 - <span key={1}>text2</span>,
411 - [[[null, <p key={1} />], false]],
412 - ]);
413 - let parent = e.parentNode;
414 - expect(parent.childNodes[0].tagName).toBe('DIV');
415 - expect(parent.childNodes[1].tagName).toBe('SPAN');
416 - expect(parent.childNodes[2].tagName).toBe('P');
417 - });
418 -
419 - itRenders('a fragment with one child', async render => {
420 - let e = await render(
421 - <React.Fragment>
422 - <div>text1</div>
423 - </React.Fragment>,
424 - );
425 - let parent = e.parentNode;
426 - expect(parent.childNodes[0].tagName).toBe('DIV');
427 - });
428 -
429 - itRenders('a fragment with several children', async render => {
430 - let Header = props => {
431 - return <p>header</p>;
432 - };
433 - let Footer = props => {
434 - return (
435 - <React.Fragment>
436 - <h2>footer</h2>
437 - <h3>about</h3>
438 - </React.Fragment>
439 - );
440 - };
441 - let e = await render(
442 - <React.Fragment>
443 - <div>text1</div>
444 - <span>text2</span>
445 - <Header />
446 - <Footer />
447 - </React.Fragment>,
448 - );
449 - let parent = e.parentNode;
450 - expect(parent.childNodes[0].tagName).toBe('DIV');
451 - expect(parent.childNodes[1].tagName).toBe('SPAN');
452 - expect(parent.childNodes[2].tagName).toBe('P');
453 - expect(parent.childNodes[3].tagName).toBe('H2');
454 - expect(parent.childNodes[4].tagName).toBe('H3');
455 - });
456 -
457 - itRenders('a nested fragment', async render => {
458 - let e = await render(
459 - <React.Fragment>
460 - <React.Fragment>
461 - <div>text1</div>
462 - </React.Fragment>
463 - <span>text2</span>
464 - <React.Fragment>
465 - <React.Fragment>
466 - <React.Fragment>
467 - {null}
468 - <p />
469 - </React.Fragment>
470 - {false}
471 - </React.Fragment>
472 - </React.Fragment>
473 - </React.Fragment>,
474 - );
475 - let parent = e.parentNode;
476 - expect(parent.childNodes[0].tagName).toBe('DIV');
477 - expect(parent.childNodes[1].tagName).toBe('SPAN');
478 - expect(parent.childNodes[2].tagName).toBe('P');
479 - });
480 -
481 - itRenders('an iterable', async render => {
482 - const threeDivIterable = {
483 - '@@iterator': function() {
484 - var i = 0;
485 - return {
486 - next: function() {
487 - if (i++ < 3) {
488 - return {value: <div key={i} />, done: false};
489 - } else {
490 - return {value: undefined, done: true};
491 - }
492 - },
493 - };
494 - },
495 - };
496 - let e = await render(threeDivIterable);
497 - let parent = e.parentNode;
498 - expect(parent.childNodes.length).toBe(3);
499 - expect(parent.childNodes[0].tagName).toBe('DIV');
500 - expect(parent.childNodes[1].tagName).toBe('DIV');
501 - expect(parent.childNodes[2].tagName).toBe('DIV');
502 - });
503 -
504 - itRenders('emptyish values', async render => {
505 - let e = await render(0);
506 - expect(e.nodeType).toBe(TEXT_NODE_TYPE);
507 - expect(e.nodeValue).toMatch('0');
508 -
509 - // Empty string is special because client renders a node
510 - // but server returns empty HTML. So we compare parent text.
511 - expect((await render(<div>{''}</div>)).textContent).toBe('');
512 -
513 - expect(await render(<React.Fragment />)).toBe(null);
514 - expect(await render([])).toBe(null);
515 - expect(await render(false)).toBe(null);
516 - expect(await render(true)).toBe(null);
517 - expect(await render(undefined)).toBe(null);
518 - expect(await render([[[false]], undefined])).toBe(null);
519 - });
520 - });
521 -
522 - describe('property to attribute mapping', function() {
523 - describe('string properties', function() {
524 - itRenders('simple numbers', async render => {
525 - const e = await render(<div width={30} />);
526 - expect(e.getAttribute('width')).toBe('30');
527 - });
528 -
529 - itRenders('simple strings', async render => {
530 - const e = await render(<div width={'30'} />);
531 - expect(e.getAttribute('width')).toBe('30');
532 - });
533 -
534 - itRenders('no string prop with true value', async render => {
535 - const e = await render(<a href={true} />, 1);
536 - expect(e.hasAttribute('href')).toBe(false);
537 - });
538 -
539 - itRenders('no string prop with false value', async render => {
540 - const e = await render(<a href={false} />, 1);
541 - expect(e.hasAttribute('href')).toBe(false);
542 - });
543 -
544 - itRenders('no string prop with null value', async render => {
545 - const e = await render(<div width={null} />);
546 - expect(e.hasAttribute('width')).toBe(false);
547 - });
548 - });
549 -
550 - describe('boolean properties', function() {
551 - itRenders('boolean prop with true value', async render => {
552 - const e = await render(<div hidden={true} />);
553 - expect(e.getAttribute('hidden')).toBe('');
554 - });
555 -
556 - itRenders('boolean prop with false value', async render => {
557 - const e = await render(<div hidden={false} />);
558 - expect(e.getAttribute('hidden')).toBe(null);
559 - });
560 -
561 - itRenders('boolean prop with self value', async render => {
562 - const e = await render(<div hidden="hidden" />);
563 - expect(e.getAttribute('hidden')).toBe('');
564 - });
565 -
566 - // this does not seem like correct behavior, since hidden="" in HTML indicates
567 - // that the boolean property is present. however, it is how the current code
568 - // behaves, so the test is included here.
569 - itRenders('boolean prop with "" value', async render => {
570 - const e = await render(<div hidden="" />);
571 - expect(e.getAttribute('hidden')).toBe(null);
572 - });
573 -
574 - // this seems like it might mask programmer error, but it's existing behavior.
575 - itRenders('boolean prop with string value', async render => {
576 - const e = await render(<div hidden="foo" />);
577 - expect(e.getAttribute('hidden')).toBe('');
578 - });
579 -
580 - // this seems like it might mask programmer error, but it's existing behavior.
581 - itRenders('boolean prop with array value', async render => {
582 - const e = await render(<div hidden={['foo', 'bar']} />);
583 - expect(e.getAttribute('hidden')).toBe('');
584 - });
585 -
586 - // this seems like it might mask programmer error, but it's existing behavior.
587 - itRenders('boolean prop with object value', async render => {
588 - const e = await render(<div hidden={{foo: 'bar'}} />);
589 - expect(e.getAttribute('hidden')).toBe('');
590 - });
591 -
592 - // this seems like it might mask programmer error, but it's existing behavior.
593 - itRenders('boolean prop with non-zero number value', async render => {
594 - const e = await render(<div hidden={10} />);
595 - expect(e.getAttribute('hidden')).toBe('');
596 - });
597 -
598 - // this seems like it might mask programmer error, but it's existing behavior.
599 - itRenders('boolean prop with zero value', async render => {
600 - const e = await render(<div hidden={0} />);
601 - expect(e.getAttribute('hidden')).toBe(null);
602 - });
603 -
604 - itRenders('no boolean prop with null value', async render => {
605 - const e = await render(<div hidden={null} />);
606 - expect(e.hasAttribute('hidden')).toBe(false);
607 - });
608 - });
609 -
610 - describe('download property (combined boolean/string attribute)', function() {
611 - itRenders('download prop with true value', async render => {
612 - const e = await render(<a download={true} />);
613 - expect(e.getAttribute('download')).toBe('');
614 - });
615 -
616 - itRenders('download prop with false value', async render => {
617 - const e = await render(<a download={false} />);
618 - expect(e.getAttribute('download')).toBe(null);
619 - });
620 -
621 - itRenders('download prop with string value', async render => {
622 - const e = await render(<a download="myfile" />);
623 - expect(e.getAttribute('download')).toBe('myfile');
624 - });
625 -
626 - itRenders('download prop with string "false" value', async render => {
627 - const e = await render(<a download="false" />);
628 - expect(e.getAttribute('download')).toBe('false');
629 - });
630 -
631 - itRenders('download prop with string "true" value', async render => {
632 - const e = await render(<a download={'true'} />);
633 - expect(e.getAttribute('download')).toBe('true');
634 - });
635 -
636 - itRenders('download prop with number 0 value', async render => {
637 - const e = await render(<a download={0} />);
638 - expect(e.getAttribute('download')).toBe('0');
639 - });
640 -
641 - itRenders('no download prop with null value', async render => {
642 - const e = await render(<div download={null} />);
643 - expect(e.hasAttribute('download')).toBe(false);
644 - });
645 -
646 - itRenders('no download prop with undefined value', async render => {
647 - const e = await render(<div download={undefined} />);
648 - expect(e.hasAttribute('download')).toBe(false);
649 - });
650 - });
651 -
652 - describe('className property', function() {
653 - itRenders('className prop with string value', async render => {
654 - const e = await render(<div className="myClassName" />);
655 - expect(e.getAttribute('class')).toBe('myClassName');
656 - });
657 -
658 - itRenders('className prop with empty string value', async render => {
659 - const e = await render(<div className="" />);
660 - expect(e.getAttribute('class')).toBe('');
661 - });
662 -
663 - itRenders('no className prop with true value', async render => {
664 - const e = await render(<div className={true} />, 1);
665 - expect(e.hasAttribute('class')).toBe(false);
666 - });
667 -
668 - itRenders('no className prop with false value', async render => {
669 - const e = await render(<div className={false} />, 1);
670 - expect(e.hasAttribute('class')).toBe(false);
671 - });
672 -
673 - itRenders('no className prop with null value', async render => {
674 - const e = await render(<div className={null} />);
675 - expect(e.hasAttribute('className')).toBe(false);
676 - });
677 -
678 - itRenders('badly cased className with a warning', async render => {
679 - const e = await render(<div classname="test" />, 1);
680 - expect(e.hasAttribute('class')).toBe(false);
681 - expect(e.hasAttribute('classname')).toBe(true);
682 - });
683 -
684 - itRenders(
685 - 'className prop when given the alias with a warning',
686 - async render => {
687 - const e = await render(<div class="test" />, 1);
688 - expect(e.className).toBe('test');
689 - },
690 - );
691 -
692 - itRenders(
693 - 'className prop when given a badly cased alias',
694 - async render => {
695 - const e = await render(<div cLASs="test" />, 1);
696 - expect(e.className).toBe('test');
697 - },
698 - );
699 -
700 - itRenders('class for custom elements', async render => {
701 - const e = await render(<div is="custom-element" class="test" />, 0);
702 - expect(e.getAttribute('class')).toBe('test');
703 - });
704 -
705 - itRenders('className for custom elements', async render => {
706 - const e = await render(<div is="custom-element" className="test" />, 0);
707 - expect(e.getAttribute('className')).toBe('test');
708 - });
709 - });
710 -
711 - describe('htmlFor property', function() {
712 - itRenders('htmlFor with string value', async render => {
713 - const e = await render(<div htmlFor="myFor" />);
714 - expect(e.getAttribute('for')).toBe('myFor');
715 - });
716 -
717 - itRenders('no badly cased htmlfor', async render => {
718 - const e = await render(<div htmlfor="myFor" />, 1);
719 - expect(e.hasAttribute('for')).toBe(false);
720 - expect(e.getAttribute('htmlfor')).toBe('myFor');
721 - });
722 -
723 - itRenders('htmlFor with an empty string', async render => {
724 - const e = await render(<div htmlFor="" />);
725 - expect(e.getAttribute('for')).toBe('');
726 - });
727 -
728 - itRenders('no htmlFor prop with true value', async render => {
729 - const e = await render(<div htmlFor={true} />, 1);
730 - expect(e.hasAttribute('for')).toBe(false);
731 - });
732 -
733 - itRenders('no htmlFor prop with false value', async render => {
734 - const e = await render(<div htmlFor={false} />, 1);
735 - expect(e.hasAttribute('for')).toBe(false);
736 - });
737 -
738 - itRenders('no htmlFor prop with null value', async render => {
739 - const e = await render(<div htmlFor={null} />);
740 - expect(e.hasAttribute('htmlFor')).toBe(false);
741 - });
742 -
743 - itRenders('htmlFor attribute on custom elements', async render => {
744 - const e = await render(<div is="custom-element" htmlFor="test" />);
745 - expect(e.getAttribute('htmlFor')).toBe('test');
746 - });
747 -
748 - itRenders('for attribute on custom elements', async render => {
749 - const e = await render(<div is="custom-element" for="test" />);
750 - expect(e.getAttribute('for')).toBe('test');
751 - });
752 - });
753 -
754 - describe('numeric properties', function() {
755 - itRenders(
756 - 'positive numeric property with positive value',
757 - async render => {
758 - const e = await render(<input size={2} />);
759 - expect(e.getAttribute('size')).toBe('2');
760 - },
761 - );
762 -
763 - itRenders(
764 - 'no positive numeric property with zero value',
765 - async render => {
766 - const e = await render(<input size={0} />);
767 - expect(e.hasAttribute('size')).toBe(false);
768 - },
769 - );
770 -
771 - itRenders('numeric property with zero value', async render => {
772 - const e = await render(<ol start={0} />);
773 - expect(e.getAttribute('start')).toBe('0');
774 - });
775 - });
776 -
777 - describe('props with special meaning in React', function() {
778 - itRenders('no ref attribute', async render => {
779 - class RefComponent extends React.Component {
780 - render() {
781 - return <div ref="foo" />;
782 - }
783 - }
784 - const e = await render(<RefComponent />);
785 - expect(e.getAttribute('ref')).toBe(null);
786 - });
787 -
788 - itRenders('no children attribute', async render => {
789 - const e = await render(React.createElement('div', {}, 'foo'));
790 - expect(e.getAttribute('children')).toBe(null);
791 - });
792 -
793 - itRenders('no key attribute', async render => {
794 - const e = await render(<div key="foo" />);
795 - expect(e.getAttribute('key')).toBe(null);
796 - });
797 -
798 - itRenders('no dangerouslySetInnerHTML attribute', async render => {
799 - const e = await render(
800 - <div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
801 - );
802 - expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
803 - });
804 -
805 - itRenders('no suppressContentEditableWarning attribute', async render => {
806 - const e = await render(<div suppressContentEditableWarning={true} />);
807 - expect(e.getAttribute('suppressContentEditableWarning')).toBe(null);
808 - });
809 -
810 - itRenders('no suppressHydrationWarning attribute', async render => {
811 - const e = await render(<span suppressHydrationWarning={true} />);
812 - expect(e.getAttribute('suppressHydrationWarning')).toBe(null);
813 - });
814 - });
815 -
816 - describe('inline styles', function() {
817 - itRenders('simple styles', async render => {
818 - const e = await render(<div style={{color: 'red', width: '30px'}} />);
819 - expect(e.style.color).toBe('red');
820 - expect(e.style.width).toBe('30px');
821 - });
822 -
823 - itRenders('relevant styles with px', async render => {
824 - const e = await render(
825 - <div
826 - style={{
827 - left: 0,
828 - margin: 16,
829 - opacity: 0.5,
830 - padding: '4px',
831 - }}
832 - />,
833 - );
834 - expect(e.style.left).toBe('0px');
835 - expect(e.style.margin).toBe('16px');
836 - expect(e.style.opacity).toBe('0.5');
837 - expect(e.style.padding).toBe('4px');
838 - });
839 -
840 - itRenders('custom properties', async render => {
841 - const e = await render(<div style={{'--foo': 5}} />);
842 - // This seems like an odd way computed properties are exposed in jsdom.
843 - // In a real browser we'd read it with e.style.getPropertyValue('--foo')
844 - expect(e.style.Foo).toBe('5');
845 - });
846 -
847 - itRenders('no undefined styles', async render => {
848 - const e = await render(
849 - <div style={{color: undefined, width: '30px'}} />,
850 - );
851 - expect(e.style.color).toBe('');
852 - expect(e.style.width).toBe('30px');
853 - });
854 -
855 - itRenders('no null styles', async render => {
856 - const e = await render(<div style={{color: null, width: '30px'}} />);
857 - expect(e.style.color).toBe('');
858 - expect(e.style.width).toBe('30px');
859 - });
860 -
861 - itRenders('no empty styles', async render => {
862 - const e = await render(<div style={{color: null, width: null}} />);
863 - expect(e.style.color).toBe('');
864 - expect(e.style.width).toBe('');
865 - expect(e.hasAttribute('style')).toBe(false);
866 - });
867 -
868 - itRenders('unitless-number rules with prefixes', async render => {
869 - const {style} = await render(
870 - <div
871 - style={{
872 - lineClamp: 10,
873 - WebkitLineClamp: 10,
874 - MozFlexGrow: 10,
875 - msFlexGrow: 10,
876 - msGridRow: 10,
877 - msGridRowEnd: 10,
878 - msGridRowSpan: 10,
879 - msGridRowStart: 10,
880 - msGridColumn: 10,
881 - msGridColumnEnd: 10,
882 - msGridColumnSpan: 10,
883 - msGridColumnStart: 10,
884 - }}
885 - />,
886 - );
887 -
888 - expect(style.lineClamp).toBe('10');
889 - expect(style.WebkitLineClamp).toBe('10');
890 - expect(style.MozFlexGrow).toBe('10');
891 - // jsdom is inconsistent in the style property name
892 - // it uses on the client and when processing server markup.
893 - // But it should be there either way.
894 - expect(style.MsFlexGrow || style.msFlexGrow).toBe('10');
895 - expect(style.MsGridRow || style.msGridRow).toBe('10');
896 - expect(style.MsGridRowEnd || style.msGridRowEnd).toBe('10');
897 - expect(style.MsGridRowSpan || style.msGridRowSpan).toBe('10');
898 - expect(style.MsGridRowStart || style.msGridRowStart).toBe('10');
899 - expect(style.MsGridColumn || style.msGridColumn).toBe('10');
900 - expect(style.MsGridColumnEnd || style.msGridColumnEnd).toBe('10');
901 - expect(style.MsGridColumnSpan || style.msGridColumnSpan).toBe('10');
902 - expect(style.MsGridColumnStart || style.msGridColumnStart).toBe('10');
903 - });
904 - });
905 -
906 - describe('aria attributes', function() {
907 - itRenders('simple strings', async render => {
908 - const e = await render(<div aria-label="hello" />);
909 - expect(e.getAttribute('aria-label')).toBe('hello');
910 - });
911 -
912 - // this probably is just masking programmer error, but it is existing behavior.
913 - itRenders('aria string prop with false value', async render => {
914 - const e = await render(<div aria-label={false} />);
915 - expect(e.getAttribute('aria-label')).toBe('false');
916 - });
917 -
918 - itRenders('no aria prop with null value', async render => {
919 - const e = await render(<div aria-label={null} />);
920 - expect(e.hasAttribute('aria-label')).toBe(false);
921 - });
922 -
923 - itRenders('"aria" attribute with a warning', async render => {
924 - // Reserved for future use.
925 - const e = await render(<div aria="hello" />, 1);
926 - expect(e.getAttribute('aria')).toBe('hello');
927 - });
928 - });
929 -
930 - describe('cased attributes', function() {
931 - itRenders(
932 - 'badly cased aliased HTML attribute with a warning',
933 - async render => {
934 - const e = await render(<meta httpequiv="refresh" />, 1);
935 - expect(e.hasAttribute('http-equiv')).toBe(false);
936 - expect(e.getAttribute('httpequiv')).toBe('refresh');
937 - },
938 - );
939 -
940 - itRenders('badly cased SVG attribute with a warning', async render => {
941 - const e = await render(<text textlength="10" />, 1);
942 - expect(e.getAttribute('textLength')).toBe('10');
943 - });
944 -
945 - itRenders('no badly cased aliased SVG attribute alias', async render => {
946 - const e = await render(<text strokedasharray="10 10" />, 1);
947 - expect(e.hasAttribute('stroke-dasharray')).toBe(false);
948 - expect(e.getAttribute('strokedasharray')).toBe('10 10');
949 - });
950 -
951 - itRenders(
952 - 'no badly cased original SVG attribute that is aliased',
953 - async render => {
954 - const e = await render(<text stroke-dasharray="10 10" />, 1);
955 - expect(e.getAttribute('stroke-dasharray')).toBe('10 10');
956 - },
957 - );
958 - });
959 -
960 - describe('unknown attributes', function() {
961 - itRenders('unknown attributes', async render => {
962 - const e = await render(<div foo="bar" />);
963 - expect(e.getAttribute('foo')).toBe('bar');
964 - });
965 -
966 - itRenders('unknown data- attributes', async render => {
967 - const e = await render(<div data-foo="bar" />);
968 - expect(e.getAttribute('data-foo')).toBe('bar');
969 - });
970 -
971 - itRenders('badly cased reserved attributes', async render => {
972 - const e = await render(<div CHILDREN="5" />, 1);
973 - expect(e.getAttribute('CHILDREN')).toBe('5');
974 - });
975 -
976 - itRenders('"data" attribute', async render => {
977 - // For `<object />` acts as `src`.
978 - const e = await render(<object data="hello" />);
979 - expect(e.getAttribute('data')).toBe('hello');
980 - });
981 -
982 - itRenders('no unknown data- attributes with null value', async render => {
983 - const e = await render(<div data-foo={null} />);
984 - expect(e.hasAttribute('data-foo')).toBe(false);
985 - });
986 -
987 - itRenders('unknown data- attributes with casing', async render => {
988 - const e = await render(<div data-fooBar="true" />, 1);
989 - expect(e.getAttribute('data-foobar')).toBe('true');
990 - });
991 -
992 - itRenders('unknown data- attributes with boolean true', async render => {
993 - const e = await render(<div data-foobar={true} />);
994 - expect(e.getAttribute('data-foobar')).toBe('true');
995 - });
996 -
997 - itRenders('unknown data- attributes with boolean false', async render => {
998 - const e = await render(<div data-foobar={false} />);
999 - expect(e.getAttribute('data-foobar')).toBe('false');
1000 - });
1001 -
1002 - itRenders(
1003 - 'no unknown data- attributes with casing and null value',
1004 - async render => {
1005 - const e = await render(<div data-fooBar={null} />, 1);
1006 - expect(e.hasAttribute('data-foobar')).toBe(false);
1007 - },
1008 - );
1009 -
1010 - itRenders('custom attributes for non-standard elements', async render => {
1011 - const e = await render(<nonstandard foo="bar" />);
1012 - expect(e.getAttribute('foo')).toBe('bar');
1013 - });
1014 -
1015 - itRenders('unknown attributes for custom elements', async render => {
1016 - const e = await render(<custom-element foo="bar" />);
1017 - expect(e.getAttribute('foo')).toBe('bar');
1018 - });
1019 -
1020 - itRenders(
1021 - 'no unknown attributes for custom elements with null value',
1022 - async render => {
1023 - const e = await render(<custom-element foo={null} />);
1024 - expect(e.hasAttribute('foo')).toBe(false);
1025 - },
1026 - );
1027 -
1028 - itRenders(
1029 - 'unknown attributes for custom elements using is',
1030 - async render => {
1031 - const e = await render(<div is="custom-element" foo="bar" />);
1032 - expect(e.getAttribute('foo')).toBe('bar');
1033 - },
1034 - );
1035 -
1036 - itRenders(
1037 - 'no unknown attributes for custom elements using is with null value',
1038 - async render => {
1039 - const e = await render(<div is="custom-element" foo={null} />);
1040 - expect(e.hasAttribute('foo')).toBe(false);
1041 - },
1042 - );
1043 -
1044 - itRenders('SVG tags with dashes in them', async render => {
1045 - const e = await render(
1046 - <svg>
1047 - <font-face accentHeight={10} />
1048 - </svg>,
1049 - );
1050 - expect(e.firstChild.hasAttribute('accentHeight')).toBe(false);
1051 - expect(e.firstChild.getAttribute('accent-height')).toBe('10');
1052 - });
1053 -
1054 - itRenders('cased custom attributes', async render => {
1055 - const e = await render(<div fooBar="test" />, 1);
1056 - expect(e.getAttribute('foobar')).toBe('test');
1057 - });
1058 - });
1059 -
1060 - itRenders('no HTML events', async render => {
1061 - const e = await render(<div onClick={() => {}} />);
1062 - expect(e.getAttribute('onClick')).toBe(null);
1063 - expect(e.getAttribute('onClick')).toBe(null);
1064 - expect(e.getAttribute('click')).toBe(null);
1065 - });
1066 -
1067 - itRenders('no unknown events', async render => {
1068 - const e = await render(
1069 - <div onunknownevent="alert(&quot;hack&quot;)" />,
1070 - 1,
1071 - );
1072 - expect(e.getAttribute('onunknownevent')).toBe(null);
1073 - });
1074 -
1075 - itRenders('custom attribute named `on`', async render => {
1076 - const e = await render(<div on="tap:do-something" />);
1077 - expect(e.getAttribute('on')).toEqual('tap:do-something');
1078 - });
1079 - });
1080 -
1081 - describe('elements and children', function() {
1082 - function expectNode(node, type, value) {
1083 - expect(node).not.toBe(null);
1084 - expect(node.nodeType).toBe(type);
1085 - expect(node.nodeValue).toMatch(value);
1086 - }
1087 -
1088 - function expectTextNode(node, text) {
1089 - expectNode(node, TEXT_NODE_TYPE, text);
1090 - }
1091 -
1092 - describe('text children', function() {
1093 - itRenders('a div with text', async render => {
1094 - const e = await render(<div>Text</div>);
1095 - expect(e.tagName).toBe('DIV');
1096 - expect(e.childNodes.length).toBe(1);
1097 - expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
1098 - });
1099 -
1100 - itRenders('a div with text with flanking whitespace', async render => {
1101 - // prettier-ignore
1102 - const e = await render(<div> Text </div>);
1103 - expect(e.childNodes.length).toBe(1);
1104 - expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text ');
1105 - });
1106 -
1107 - itRenders('a div with an empty text child', async render => {
1108 - const e = await render(<div>{''}</div>);
1109 - expect(e.childNodes.length).toBe(0);
1110 - });
1111 -
1112 - itRenders('a div with multiple empty text children', async render => {
1113 - const e = await render(
1114 - <div>
1115 - {''}
1116 - {''}
1117 - {''}
1118 - </div>,
1119 - );
1120 - if (render === serverRender || render === streamRender) {
1121 - // For plain server markup result we should have no text nodes if
1122 - // they're all empty.
1123 - expect(e.childNodes.length).toBe(0);
1124 - expect(e.textContent).toBe('');
1125 - } else {
1126 - expect(e.childNodes.length).toBe(3);
1127 - expectTextNode(e.childNodes[0], '');
1128 - expectTextNode(e.childNodes[1], '');
1129 - expectTextNode(e.childNodes[2], '');
1130 - }
1131 - });
1132 -
1133 - itRenders('a div with multiple whitespace children', async render => {
1134 - // prettier-ignore
1135 - const e = await render(<div>{' '}{' '}{' '}</div>);
1136 - if (
1137 - render === serverRender ||
1138 - render === clientRenderOnServerString ||
1139 - render === streamRender
1140 - ) {
1141 - // For plain server markup result we have comments between.
1142 - // If we're able to hydrate, they remain.
1143 - expect(e.childNodes.length).toBe(5);
1144 - expectTextNode(e.childNodes[0], ' ');
1145 - expectTextNode(e.childNodes[2], ' ');
1146 - expectTextNode(e.childNodes[4], ' ');
1147 - } else {
1148 - expect(e.childNodes.length).toBe(3);
1149 - expectTextNode(e.childNodes[0], ' ');
1150 - expectTextNode(e.childNodes[1], ' ');
1151 - expectTextNode(e.childNodes[2], ' ');
1152 - }
1153 - });
1154 -
1155 - itRenders('a div with text sibling to a node', async render => {
1156 - const e = await render(
1157 - <div>
1158 - Text<span>More Text</span>
1159 - </div>,
1160 - );
1161 - let spanNode;
1162 - expect(e.childNodes.length).toBe(2);
1163 - spanNode = e.childNodes[1];
1164 - expectTextNode(e.childNodes[0], 'Text');
1165 - expect(spanNode.tagName).toBe('SPAN');
1166 - expect(spanNode.childNodes.length).toBe(1);
1167 - expectNode(spanNode.firstChild, TEXT_NODE_TYPE, 'More Text');
1168 - });
1169 -
1170 - itRenders('a non-standard element with text', async render => {
1171 - const e = await render(<nonstandard>Text</nonstandard>);
1172 - expect(e.tagName).toBe('NONSTANDARD');
1173 - expect(e.childNodes.length).toBe(1);
1174 - expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
1175 - });
1176 -
1177 - itRenders('a custom element with text', async render => {
1178 - const e = await render(<custom-element>Text</custom-element>);
1179 - expect(e.tagName).toBe('CUSTOM-ELEMENT');
1180 - expect(e.childNodes.length).toBe(1);
1181 - expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
1182 - });
1183 -
1184 - itRenders('a leading blank child with a text sibling', async render => {
1185 - const e = await render(<div>{''}foo</div>);
1186 - if (render === serverRender || render === streamRender) {
1187 - expect(e.childNodes.length).toBe(1);
1188 - expectTextNode(e.childNodes[0], 'foo');
1189 - } else {
1190 - expect(e.childNodes.length).toBe(2);
1191 - expectTextNode(e.childNodes[0], '');
1192 - expectTextNode(e.childNodes[1], 'foo');
1193 - }
1194 - });
1195 -
1196 - itRenders('a trailing blank child with a text sibling', async render => {
1197 - const e = await render(<div>foo{''}</div>);
1198 - // with Fiber, there are just two text nodes.
1199 - if (render === serverRender || render === streamRender) {
1200 - expect(e.childNodes.length).toBe(1);
1201 - expectTextNode(e.childNodes[0], 'foo');
1202 - } else {
1203 - expect(e.childNodes.length).toBe(2);
1204 - expectTextNode(e.childNodes[0], 'foo');
1205 - expectTextNode(e.childNodes[1], '');
1206 - }
1207 - });
1208 -
1209 - itRenders('an element with two text children', async render => {
1210 - const e = await render(
1211 - <div>
1212 - {'foo'}
1213 - {'bar'}
1214 - </div>,
1215 - );
1216 - if (
1217 - render === serverRender ||
1218 - render === clientRenderOnServerString ||
1219 - render === streamRender
1220 - ) {
1221 - // In the server render output there's a comment between them.
1222 - expect(e.childNodes.length).toBe(3);
1223 - expectTextNode(e.childNodes[0], 'foo');
1224 - expectTextNode(e.childNodes[2], 'bar');
1225 - } else {
1226 - expect(e.childNodes.length).toBe(2);
1227 - expectTextNode(e.childNodes[0], 'foo');
1228 - expectTextNode(e.childNodes[1], 'bar');
1229 - }
1230 - });
1231 -
1232 - itRenders(
1233 - 'a component returning text node between two text nodes',
1234 - async render => {
1235 - const B = () => 'b';
1236 - const e = await render(
1237 - <div>
1238 - {'a'}
1239 - <B />
1240 - {'c'}
1241 - </div>,
1242 - );
1243 - if (
1244 - render === serverRender ||
1245 - render === clientRenderOnServerString ||
1246 - render === streamRender
1247 - ) {
1248 - // In the server render output there's a comment between them.
1249 - expect(e.childNodes.length).toBe(5);
1250 - expectTextNode(e.childNodes[0], 'a');
1251 - expectTextNode(e.childNodes[2], 'b');
1252 - expectTextNode(e.childNodes[4], 'c');
1253 - } else {
1254 - expect(e.childNodes.length).toBe(3);
1255 - expectTextNode(e.childNodes[0], 'a');
1256 - expectTextNode(e.childNodes[1], 'b');
1257 - expectTextNode(e.childNodes[2], 'c');
1258 - }
1259 - },
1260 - );
1261 -
1262 - itRenders('a tree with sibling host and text nodes', async render => {
1263 - class X extends React.Component {
1264 - render() {
1265 - return [null, [<Y key="1" />], false];
1266 - }
1267 - }
1268 -
1269 - function Y() {
1270 - return [<Z key="1" />, ['c']];
1271 - }
1272 -
1273 - function Z() {
1274 - return null;
1275 - }
1276 -
1277 - const e = await render(
1278 - <div>
1279 - {[['a'], 'b']}
1280 - <div>
1281 - <X key="1" />
1282 - d
1283 - </div>
1284 - e
1285 - </div>,
1286 - );
1287 - if (
1288 - render === serverRender ||
1289 - render === clientRenderOnServerString ||
1290 - render === streamRender
1291 - ) {
1292 - // In the server render output there's comments between text nodes.
1293 - expect(e.childNodes.length).toBe(5);
1294 - expectTextNode(e.childNodes[0], 'a');
1295 - expectTextNode(e.childNodes[2], 'b');
1296 - expect(e.childNodes[3].childNodes.length).toBe(3);
1297 - expectTextNode(e.childNodes[3].childNodes[0], 'c');
1298 - expectTextNode(e.childNodes[3].childNodes[2], 'd');
1299 - expectTextNode(e.childNodes[4], 'e');
1300 - } else {
1301 - expect(e.childNodes.length).toBe(4);
1302 - expectTextNode(e.childNodes[0], 'a');
1303 - expectTextNode(e.childNodes[1], 'b');
1304 - expect(e.childNodes[2].childNodes.length).toBe(2);
1305 - expectTextNode(e.childNodes[2].childNodes[0], 'c');
1306 - expectTextNode(e.childNodes[2].childNodes[1], 'd');
1307 - expectTextNode(e.childNodes[3], 'e');
1308 - }
1309 - });
1310 - });
1311 -
1312 - describe('number children', function() {
1313 - itRenders('a number as single child', async render => {
1314 - const e = await render(<div>{3}</div>);
1315 - expect(e.textContent).toBe('3');
1316 - });
1317 -
1318 - // zero is falsey, so it could look like no children if the code isn't careful.
1319 - itRenders('zero as single child', async render => {
1320 - const e = await render(<div>{0}</div>);
1321 - expect(e.textContent).toBe('0');
1322 - });
1323 -
1324 - itRenders('an element with number and text children', async render => {
1325 - const e = await render(
1326 - <div>
1327 - {'foo'}
1328 - {40}
1329 - </div>,
1330 - );
1331 - // with Fiber, there are just two text nodes.
1332 - if (
1333 - render === serverRender ||
1334 - render === clientRenderOnServerString ||
1335 - render === streamRender
1336 - ) {
1337 - // In the server markup there's a comment between.
1338 - expect(e.childNodes.length).toBe(3);
1339 - expectTextNode(e.childNodes[0], 'foo');
1340 - expectTextNode(e.childNodes[2], '40');
1341 - } else {
1342 - expect(e.childNodes.length).toBe(2);
1343 - expectTextNode(e.childNodes[0], 'foo');
1344 - expectTextNode(e.childNodes[1], '40');
1345 - }
1346 - });
1347 - });
1348 -
1349 - describe('null, false, and undefined children', function() {
1350 - itRenders('null single child as blank', async render => {
1351 - const e = await render(<div>{null}</div>);
1352 - expect(e.childNodes.length).toBe(0);
1353 - });
1354 -
1355 - itRenders('false single child as blank', async render => {
1356 - const e = await render(<div>{false}</div>);
1357 - expect(e.childNodes.length).toBe(0);
1358 - });
1359 -
1360 - itRenders('undefined single child as blank', async render => {
1361 - const e = await render(<div>{undefined}</div>);
1362 - expect(e.childNodes.length).toBe(0);
1363 - });
1364 -
1365 - itRenders('a null component children as empty', async render => {
1366 - const NullComponent = () => null;
1367 - const e = await render(
1368 - <div>
1369 - <NullComponent />
1370 - </div>,
1371 - );
1372 - expect(e.childNodes.length).toBe(0);
1373 - });
1374 -
1375 - itRenders('null children as blank', async render => {
1376 - const e = await render(<div>{null}foo</div>);
1377 - expect(e.childNodes.length).toBe(1);
1378 - expectTextNode(e.childNodes[0], 'foo');
1379 - });
1380 -
1381 - itRenders('false children as blank', async render => {
1382 - const e = await render(<div>{false}foo</div>);
1383 - expect(e.childNodes.length).toBe(1);
1384 - expectTextNode(e.childNodes[0], 'foo');
1385 - });
1386 -
1387 - itRenders('null and false children together as blank', async render => {
1388 - const e = await render(
1389 - <div>
1390 - {false}
1391 - {null}foo{null}
1392 - {false}
1393 - </div>,
1394 - );
1395 - expect(e.childNodes.length).toBe(1);
1396 - expectTextNode(e.childNodes[0], 'foo');
1397 - });
1398 -
1399 - itRenders('only null and false children as blank', async render => {
1400 - const e = await render(
1401 - <div>
1402 - {false}
1403 - {null}
1404 - {null}
1405 - {false}
1406 - </div>,
1407 - );
1408 - expect(e.childNodes.length).toBe(0);
1409 - });
1410 - });
1411 -
1412 - describe('elements with implicit namespaces', function() {
1413 - itRenders('an svg element', async render => {
1414 - const e = await render(<svg />);
1415 - expect(e.childNodes.length).toBe(0);
1416 - expect(e.tagName).toBe('svg');
1417 - expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1418 - });
1419 -
1420 - itRenders('svg child element with an attribute', async render => {
1421 - let e = await render(<svg viewBox="0 0 0 0" />);
1422 - expect(e.childNodes.length).toBe(0);
1423 - expect(e.tagName).toBe('svg');
1424 - expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1425 - expect(e.getAttribute('viewBox')).toBe('0 0 0 0');
1426 - });
1427 -
1428 - itRenders(
1429 - 'svg child element with a namespace attribute',
1430 - async render => {
1431 - let e = await render(
1432 - <svg>
1433 - <image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
1434 - </svg>,
1435 - );
1436 - e = e.firstChild;
1437 - expect(e.childNodes.length).toBe(0);
1438 - expect(e.tagName).toBe('image');
1439 - expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1440 - expect(e.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
1441 - 'http://i.imgur.com/w7GCRPb.png',
1442 - );
1443 - },
1444 - );
1445 -
1446 - itRenders('svg child element with a badly cased alias', async render => {
1447 - let e = await render(
1448 - <svg>
1449 - <image xlinkhref="http://i.imgur.com/w7GCRPb.png" />
1450 - </svg>,
1451 - 1,
1452 - );
1453 - e = e.firstChild;
1454 - expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
1455 - false,
1456 - );
1457 - expect(e.getAttribute('xlinkhref')).toBe(
1458 - 'http://i.imgur.com/w7GCRPb.png',
1459 - );
1460 - });
1461 -
1462 - itRenders('svg element with a tabIndex attribute', async render => {
1463 - let e = await render(<svg tabIndex="1" />);
1464 - expect(e.tabIndex).toBe(1);
1465 - });
1466 -
1467 - itRenders(
1468 - 'svg element with a badly cased tabIndex attribute',
1469 - async render => {
1470 - let e = await render(<svg tabindex="1" />, 1);
1471 - expect(e.tabIndex).toBe(1);
1472 - },
1473 - );
1474 -
1475 - itRenders('svg element with a mixed case name', async render => {
1476 - let e = await render(
1477 - <svg>
1478 - <filter>
1479 - <feMorphology />
1480 - </filter>
1481 - </svg>,
1482 - );
1483 - e = e.firstChild.firstChild;
1484 - expect(e.childNodes.length).toBe(0);
1485 - expect(e.tagName).toBe('feMorphology');
1486 - expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
1487 - });
1488 -
1489 - itRenders('a math element', async render => {
1490 - const e = await render(<math />);
1491 - expect(e.childNodes.length).toBe(0);
1492 - expect(e.tagName).toBe('math');
1493 - expect(e.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
1494 - });
1495 - });
1496 - // specially wrapped components
1497 - // (see the big switch near the beginning ofReactDOMComponent.mountComponent)
1498 - itRenders('an img', async render => {
1499 - const e = await render(<img />);
1500 - expect(e.childNodes.length).toBe(0);
1501 - expect(e.nextSibling).toBe(null);
1502 - expect(e.tagName).toBe('IMG');
1503 - });
1504 -
1505 - itRenders('a button', async render => {
1506 - const e = await render(<button />);
1507 - expect(e.childNodes.length).toBe(0);
1508 - expect(e.nextSibling).toBe(null);
1509 - expect(e.tagName).toBe('BUTTON');
1510 - });
1511 -
1512 - itRenders('a div with dangerouslySetInnerHTML', async render => {
1513 - const e = await render(
1514 - <div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
1515 - );
1516 - expect(e.childNodes.length).toBe(1);
1517 - expect(e.firstChild.tagName).toBe('SPAN');
1518 - expect(e.firstChild.getAttribute('id')).toBe('child');
1519 - expect(e.firstChild.childNodes.length).toBe(0);
1520 - });
1521 -
1522 - describe('newline-eating elements', function() {
1523 - itRenders(
1524 - 'a newline-eating tag with content not starting with \\n',
1525 - async render => {
1526 - const e = await render(<pre>Hello</pre>);
1527 - expect(e.textContent).toBe('Hello');
1528 - },
1529 - );
1530 - itRenders(
1531 - 'a newline-eating tag with content starting with \\n',
1532 - async render => {
1533 - const e = await render(<pre>{'\nHello'}</pre>);
1534 - expect(e.textContent).toBe('\nHello');
1535 - },
1536 - );
1537 - itRenders('a normal tag with content starting with \\n', async render => {
1538 - const e = await render(<div>{'\nHello'}</div>);
1539 - expect(e.textContent).toBe('\nHello');
1540 - });
1541 - });
1542 -
1543 - describe('different component implementations', function() {
1544 - function checkFooDiv(e) {
1545 - expect(e.childNodes.length).toBe(1);
1546 - expectNode(e.firstChild, TEXT_NODE_TYPE, 'foo');
1547 - }
1548 -
1549 - itRenders('stateless components', async render => {
1550 - const StatelessComponent = () => <div>foo</div>;
1551 - checkFooDiv(await render(<StatelessComponent />));
1552 - });
1553 -
1554 - itRenders('ES6 class components', async render => {
1555 - class ClassComponent extends React.Component {
1556 - render() {
1557 - return <div>foo</div>;
1558 - }
1559 - }
1560 - checkFooDiv(await render(<ClassComponent />));
1561 - });
1562 -
1563 - itRenders('factory components', async render => {
1564 - const FactoryComponent = () => {
1565 - return {
1566 - render: function() {
1567 - return <div>foo</div>;
1568 - },
1569 - };
1570 - };
1571 - checkFooDiv(await render(<FactoryComponent />));
1572 - });
1573 - });
1574 -
1575 - describe('component hierarchies', async function() {
1576 - itRenders('single child hierarchies of components', async render => {
1577 - const Component = props => <div>{props.children}</div>;
1578 - let e = await render(
1579 - <Component>
1580 - <Component>
1581 - <Component>
1582 - <Component />
1583 - </Component>
1584 - </Component>
1585 - </Component>,
1586 - );
1587 - for (var i = 0; i < 3; i++) {
1588 - expect(e.tagName).toBe('DIV');
1589 - expect(e.childNodes.length).toBe(1);
1590 - e = e.firstChild;
1591 - }
1592 - expect(e.tagName).toBe('DIV');
1593 - expect(e.childNodes.length).toBe(0);
1594 - });
1595 -
1596 - itRenders('multi-child hierarchies of components', async render => {
1597 - const Component = props => <div>{props.children}</div>;
1598 - const e = await render(
1599 - <Component>
1600 - <Component>
1601 - <Component />
1602 - <Component />
1603 - </Component>
1604 - <Component>
1605 - <Component />
1606 - <Component />
1607 - </Component>
1608 - </Component>,
1609 - );
1610 - expect(e.tagName).toBe('DIV');
1611 - expect(e.childNodes.length).toBe(2);
1612 - for (var i = 0; i < 2; i++) {
1613 - var child = e.childNodes[i];
1614 - expect(child.tagName).toBe('DIV');
1615 - expect(child.childNodes.length).toBe(2);
1616 - for (var j = 0; j < 2; j++) {
1617 - var grandchild = child.childNodes[j];
1618 - expect(grandchild.tagName).toBe('DIV');
1619 - expect(grandchild.childNodes.length).toBe(0);
1620 - }
1621 - }
1622 - });
1623 -
1624 - itRenders('a div with a child', async render => {
1625 - const e = await render(
1626 - <div id="parent">
1627 - <div id="child" />
1628 - </div>,
1629 - );
1630 - expect(e.id).toBe('parent');
1631 - expect(e.childNodes.length).toBe(1);
1632 - expect(e.childNodes[0].id).toBe('child');
1633 - expect(e.childNodes[0].childNodes.length).toBe(0);
1634 - });
1635 -
1636 - itRenders('a div with multiple children', async render => {
1637 - const e = await render(
1638 - <div id="parent">
1639 - <div id="child1" />
1640 - <div id="child2" />
1641 - </div>,
1642 - );
1643 - expect(e.id).toBe('parent');
1644 - expect(e.childNodes.length).toBe(2);
1645 - expect(e.childNodes[0].id).toBe('child1');
1646 - expect(e.childNodes[0].childNodes.length).toBe(0);
1647 - expect(e.childNodes[1].id).toBe('child2');
1648 - expect(e.childNodes[1].childNodes.length).toBe(0);
1649 - });
1650 -
1651 - itRenders(
1652 - 'a div with multiple children separated by whitespace',
1653 - async render => {
1654 - const e = await render(
1655 - <div id="parent">
1656 - <div id="child1" /> <div id="child2" />
1657 - </div>,
1658 - );
1659 - expect(e.id).toBe('parent');
1660 - let child1, child2, textNode;
1661 - expect(e.childNodes.length).toBe(3);
1662 - child1 = e.childNodes[0];
1663 - textNode = e.childNodes[1];
1664 - child2 = e.childNodes[2];
1665 - expect(child1.id).toBe('child1');
1666 - expect(child1.childNodes.length).toBe(0);
1667 - expectTextNode(textNode, ' ');
1668 - expect(child2.id).toBe('child2');
1669 - expect(child2.childNodes.length).toBe(0);
1670 - },
1671 - );
1672 -
1673 - itRenders(
1674 - 'a div with a single child surrounded by whitespace',
1675 - async render => {
1676 - // prettier-ignore
1677 - const e = await render(<div id="parent"> <div id="child" /> </div>); // eslint-disable-line no-multi-spaces
1678 - let textNode1, child, textNode2;
1679 - expect(e.childNodes.length).toBe(3);
1680 - textNode1 = e.childNodes[0];
1681 - child = e.childNodes[1];
1682 - textNode2 = e.childNodes[2];
1683 - expect(e.id).toBe('parent');
1684 - expectTextNode(textNode1, ' ');
1685 - expect(child.id).toBe('child');
1686 - expect(child.childNodes.length).toBe(0);
1687 - expectTextNode(textNode2, ' ');
1688 - },
1689 - );
1690 -
1691 - itRenders('a composite with multiple children', async render => {
1692 - const Component = props => props.children;
1693 - const e = await render(
1694 - <Component>{['a', 'b', [undefined], [[false, 'c']]]}</Component>,
1695 - );
1696 -
1697 - let parent = e.parentNode;
1698 - if (
1699 - render === serverRender ||
1700 - render === clientRenderOnServerString ||
1701 - render === streamRender
1702 - ) {
1703 - // For plain server markup result we have comments between.
1704 - // If we're able to hydrate, they remain.
1705 - expect(parent.childNodes.length).toBe(5);
1706 - expectTextNode(parent.childNodes[0], 'a');
1707 - expectTextNode(parent.childNodes[2], 'b');
1708 - expectTextNode(parent.childNodes[4], 'c');
1709 - } else {
1710 - expect(parent.childNodes.length).toBe(3);
1711 - expectTextNode(parent.childNodes[0], 'a');
1712 - expectTextNode(parent.childNodes[1], 'b');
1713 - expectTextNode(parent.childNodes[2], 'c');
1714 - }
1715 - });
1716 - });
1717 -
1718 - describe('escaping >, <, and &', function() {
1719 - itRenders('>,<, and & as single child', async render => {
1720 - const e = await render(<div>{'<span>Text&quot;</span>'}</div>);
1721 - expect(e.childNodes.length).toBe(1);
1722 - expectNode(e.firstChild, TEXT_NODE_TYPE, '<span>Text&quot;</span>');
1723 - });
1724 -
1725 - itRenders('>,<, and & as multiple children', async render => {
1726 - const e = await render(
1727 - <div>
1728 - {'<span>Text1&quot;</span>'}
1729 - {'<span>Text2&quot;</span>'}
1730 - </div>,
1731 - );
1732 - if (
1733 - render === serverRender ||
1734 - render === clientRenderOnServerString ||
1735 - render === streamRender
1736 - ) {
1737 - expect(e.childNodes.length).toBe(3);
1738 - expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
1739 - expectTextNode(e.childNodes[2], '<span>Text2&quot;</span>');
1740 - } else {
1741 - expect(e.childNodes.length).toBe(2);
1742 - expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
1743 - expectTextNode(e.childNodes[1], '<span>Text2&quot;</span>');
1744 - }
1745 - });
1746 - });
1747 -
1748 - describe('carriage return and null character', () => {
1749 - // HTML parsing normalizes CR and CRLF to LF.
1750 - // It also ignores null character.
1751 - // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
1752 - // If we have a mismatch, it might be caused by that (and should not be reported).
1753 - // We won't be patching up in this case as that matches our past behavior.
1754 -
1755 - itRenders(
1756 - 'an element with one text child with special characters',
1757 - async render => {
1758 - const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
1759 - if (render === serverRender || render === streamRender) {
1760 - expect(e.childNodes.length).toBe(1);
1761 - // Everything becomes LF when parsed from server HTML.
1762 - // Null character is ignored.
1763 - expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
1764 - } else {
1765 - expect(e.childNodes.length).toBe(1);
1766 - // Client rendering (or hydration) uses JS value with CR.
1767 - // Null character stays.
1768 - expectNode(
1769 - e.childNodes[0],
1770 - TEXT_NODE_TYPE,
1771 - 'foo\rbar\r\nbaz\nqux\u0000',
1772 - );
1773 - }
1774 - },
1775 - );
1776 -
1777 - itRenders(
1778 - 'an element with two text children with special characters',
1779 - async render => {
1780 - const e = await render(
1781 - <div>
1782 - {'foo\rbar'}
1783 - {'\r\nbaz\nqux\u0000'}
1784 - </div>,
1785 - );
1786 - if (render === serverRender || render === streamRender) {
1787 - // We have three nodes because there is a comment between them.
1788 - expect(e.childNodes.length).toBe(3);
1789 - // Everything becomes LF when parsed from server HTML.
1790 - // Null character is ignored.
1791 - expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
1792 - expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
1793 - } else if (render === clientRenderOnServerString) {
1794 - // We have three nodes because there is a comment between them.
1795 - expect(e.childNodes.length).toBe(3);
1796 - // Hydration uses JS value with CR and null character.
1797 - expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
1798 - expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
1799 - } else {
1800 - expect(e.childNodes.length).toBe(2);
1801 - // Client rendering uses JS value with CR and null character.
1802 - expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
1803 - expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
1804 - }
1805 - },
1806 - );
1807 -
1808 - itRenders(
1809 - 'an element with an attribute value with special characters',
1810 - async render => {
1811 - const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
1812 - if (
1813 - render === serverRender ||
1814 - render === streamRender ||
1815 - render === clientRenderOnServerString
1816 - ) {
1817 - // Everything becomes LF when parsed from server HTML.
1818 - // Null character in an attribute becomes the replacement character.
1819 - // Hydration also ends up with LF because we don't patch up attributes.
1820 - expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
1821 - } else {
1822 - // Client rendering uses JS value with CR and null character.
1823 - expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
1824 - }
1825 - },
1826 - );
1827 - });
1828 -
1829 - describe('components that throw errors', function() {
1830 - itThrowsWhenRendering(
1831 - 'a function returning undefined',
1832 - async render => {
1833 - const UndefinedComponent = () => undefined;
1834 - await render(<UndefinedComponent />, 1);
1835 - },
1836 - 'UndefinedComponent(...): Nothing was returned from render. ' +
1837 - 'This usually means a return statement is missing. Or, to ' +
1838 - 'render nothing, return null.',
1839 - );
1840 -
1841 - itThrowsWhenRendering(
1842 - 'a class returning undefined',
1843 - async render => {
1844 - class UndefinedComponent extends React.Component {
1845 - render() {
1846 - return undefined;
1847 - }
1848 - }
1849 - await render(<UndefinedComponent />, 1);
1850 - },
1851 - 'UndefinedComponent(...): Nothing was returned from render. ' +
1852 - 'This usually means a return statement is missing. Or, to ' +
1853 - 'render nothing, return null.',
1854 - );
1855 -
1856 - itThrowsWhenRendering(
1857 - 'a function returning an object',
1858 - async render => {
1859 - const ObjectComponent = () => ({x: 123});
1860 - await render(<ObjectComponent />, 1);
1861 - },
1862 - 'Objects are not valid as a React child (found: object with keys {x}).' +
1863 - (__DEV__
1864 - ? ' If you meant to render a collection of children, use ' +
1865 - 'an array instead.'
1866 - : ''),
1867 - );
1868 -
1869 - itThrowsWhenRendering(
1870 - 'a class returning an object',
1871 - async render => {
1872 - class ObjectComponent extends React.Component {
1873 - render() {
1874 - return {x: 123};
1875 - }
1876 - }
1877 - await render(<ObjectComponent />, 1);
1878 - },
1879 - 'Objects are not valid as a React child (found: object with keys {x}).' +
1880 - (__DEV__
1881 - ? ' If you meant to render a collection of children, use ' +
1882 - 'an array instead.'
1883 - : ''),
1884 - );
1885 -
1886 - itThrowsWhenRendering(
1887 - 'top-level object',
1888 - async render => {
1889 - await render({x: 123});
1890 - },
1891 - 'Objects are not valid as a React child (found: object with keys {x}).' +
1892 - (__DEV__
1893 - ? ' If you meant to render a collection of children, use ' +
1894 - 'an array instead.'
1895 - : ''),
1896 - );
1897 - });
1898 - });
1899 -
1900 - describe('form controls', function() {
1901 - describe('inputs', function() {
1902 - itRenders('an input with a value and an onChange', async render => {
1903 - const e = await render(<input value="foo" onChange={() => {}} />);
1904 - expect(e.value).toBe('foo');
1905 - });
1906 -
1907 - itRenders('an input with a value and readOnly', async render => {
1908 - const e = await render(<input value="foo" readOnly={true} />);
1909 - expect(e.value).toBe('foo');
1910 - });
1911 -
1912 - itRenders(
1913 - 'an input with a value and no onChange/readOnly',
1914 - async render => {
1915 - // this configuration should raise a dev warning that value without
1916 - // onChange or readOnly is a mistake.
1917 - const e = await render(<input value="foo" />, 1);
1918 - expect(e.value).toBe('foo');
1919 - expect(e.getAttribute('value')).toBe('foo');
1920 - },
1921 - );
1922 -
1923 - itRenders('an input with a defaultValue', async render => {
1924 - const e = await render(<input defaultValue="foo" />);
1925 - expect(e.value).toBe('foo');
1926 - expect(e.getAttribute('value')).toBe('foo');
1927 - expect(e.getAttribute('defaultValue')).toBe(null);
1928 - });
1929 -
1930 - itRenders('an input value overriding defaultValue', async render => {
1931 - const e = await render(
1932 - <input value="foo" defaultValue="bar" readOnly={true} />,
1933 - 1,
1934 - );
1935 - expect(e.value).toBe('foo');
1936 - expect(e.getAttribute('value')).toBe('foo');
1937 - expect(e.getAttribute('defaultValue')).toBe(null);
1938 - });
1939 -
1940 - itRenders(
1941 - 'an input value overriding defaultValue no matter the prop order',
1942 - async render => {
1943 - const e = await render(
1944 - <input defaultValue="bar" value="foo" readOnly={true} />,
1945 - 1,
1946 - );
1947 - expect(e.value).toBe('foo');
1948 - expect(e.getAttribute('value')).toBe('foo');
1949 - expect(e.getAttribute('defaultValue')).toBe(null);
1950 - },
1951 - );
1952 - });
1953 -
1954 - describe('checkboxes', function() {
1955 - itRenders('a checkbox that is checked with an onChange', async render => {
1956 - const e = await render(
1957 - <input type="checkbox" checked={true} onChange={() => {}} />,
1958 - );
1959 - expect(e.checked).toBe(true);
1960 - });
1961 -
1962 - itRenders('a checkbox that is checked with readOnly', async render => {
1963 - const e = await render(
1964 - <input type="checkbox" checked={true} readOnly={true} />,
1965 - );
1966 - expect(e.checked).toBe(true);
1967 - });
1968 -
1969 - itRenders(
1970 - 'a checkbox that is checked and no onChange/readOnly',
1971 - async render => {
1972 - // this configuration should raise a dev warning that checked without
1973 - // onChange or readOnly is a mistake.
1974 - const e = await render(<input type="checkbox" checked={true} />, 1);
1975 - expect(e.checked).toBe(true);
1976 - },
1977 - );
1978 -
1979 - itRenders('a checkbox with defaultChecked', async render => {
1980 - const e = await render(<input type="checkbox" defaultChecked={true} />);
1981 - expect(e.checked).toBe(true);
1982 - expect(e.getAttribute('defaultChecked')).toBe(null);
1983 - });
1984 -
1985 - itRenders(
1986 - 'a checkbox checked overriding defaultChecked',
1987 - async render => {
1988 - const e = await render(
1989 - <input
1990 - type="checkbox"
1991 - checked={true}
1992 - defaultChecked={false}
1993 - readOnly={true}
1994 - />,
1995 - 1,
1996 - );
1997 - expect(e.checked).toBe(true);
1998 - expect(e.getAttribute('defaultChecked')).toBe(null);
1999 - },
2000 - );
2001 -
2002 - itRenders(
2003 - 'a checkbox checked overriding defaultChecked no matter the prop order',
2004 - async render => {
2005 - const e = await render(
2006 - <input
2007 - type="checkbox"
2008 - defaultChecked={false}
2009 - checked={true}
2010 - readOnly={true}
2011 - />,
2012 - 1,
2013 - );
2014 - expect(e.checked).toBe(true);
2015 - expect(e.getAttribute('defaultChecked')).toBe(null);
2016 - },
2017 - );
2018 - });
2019 -
2020 - describe('textareas', function() {
2021 - // textareas
2022 - // ---------
2023 - itRenders('a textarea with a value and an onChange', async render => {
2024 - const e = await render(<textarea value="foo" onChange={() => {}} />);
2025 - // textarea DOM elements don't have a value **attribute**, the text is
2026 - // a child of the element and accessible via the .value **property**.
2027 - expect(e.getAttribute('value')).toBe(null);
2028 - expect(e.value).toBe('foo');
2029 - });
2030 -
2031 - itRenders('a textarea with a value and readOnly', async render => {
2032 - const e = await render(<textarea value="foo" readOnly={true} />);
2033 - // textarea DOM elements don't have a value **attribute**, the text is
2034 - // a child of the element and accessible via the .value **property**.
2035 - expect(e.getAttribute('value')).toBe(null);
2036 - expect(e.value).toBe('foo');
2037 - });
2038 -
2039 - itRenders(
2040 - 'a textarea with a value and no onChange/readOnly',
2041 - async render => {
2042 - // this configuration should raise a dev warning that value without
2043 - // onChange or readOnly is a mistake.
2044 - const e = await render(<textarea value="foo" />, 1);
2045 - expect(e.getAttribute('value')).toBe(null);
2046 - expect(e.value).toBe('foo');
2047 - },
2048 - );
2049 -
2050 - itRenders('a textarea with a defaultValue', async render => {
2051 - const e = await render(<textarea defaultValue="foo" />);
2052 - expect(e.getAttribute('value')).toBe(null);
2053 - expect(e.getAttribute('defaultValue')).toBe(null);
2054 - expect(e.value).toBe('foo');
2055 - });
2056 -
2057 - itRenders('a textarea value overriding defaultValue', async render => {
2058 - const e = await render(
2059 - <textarea value="foo" defaultValue="bar" readOnly={true} />,
2060 - 1,
2061 - );
2062 - expect(e.getAttribute('value')).toBe(null);
2063 - expect(e.getAttribute('defaultValue')).toBe(null);
2064 - expect(e.value).toBe('foo');
2065 - });
2066 -
2067 - itRenders(
2068 - 'a textarea value overriding defaultValue no matter the prop order',
2069 - async render => {
2070 - const e = await render(
2071 - <textarea defaultValue="bar" value="foo" readOnly={true} />,
2072 - 1,
2073 - );
2074 - expect(e.getAttribute('value')).toBe(null);
2075 - expect(e.getAttribute('defaultValue')).toBe(null);
2076 - expect(e.value).toBe('foo');
2077 - },
2078 - );
2079 - });
2080 -
2081 - describe('selects', function() {
2082 - var options;
2083 - beforeEach(function() {
2084 - options = [
2085 - <option key={1} value="foo" id="foo">
2086 - Foo
2087 - </option>,
2088 - <option key={2} value="bar" id="bar">
2089 - Bar
2090 - </option>,
2091 - <option key={3} value="baz" id="baz">
2092 - Baz
2093 - </option>,
2094 - ];
2095 - });
2096 -
2097 - // a helper function to test the selected value of a <select> element.
2098 - // takes in a <select> DOM element (element) and a value or array of
2099 - // values that should be selected (selected).
2100 - const expectSelectValue = (element, selected) => {
2101 - if (!Array.isArray(selected)) {
2102 - selected = [selected];
2103 - }
2104 - // the select DOM element shouldn't ever have a value or defaultValue
2105 - // attribute; that is not how select values are expressed in the DOM.
2106 - expect(element.getAttribute('value')).toBe(null);
2107 - expect(element.getAttribute('defaultValue')).toBe(null);
2108 -
2109 - ['foo', 'bar', 'baz'].forEach(value => {
2110 - const expectedValue = selected.indexOf(value) !== -1;
2111 - var option = element.querySelector(`#${value}`);
2112 - expect(option.selected).toBe(expectedValue);
2113 - });
2114 - };
2115 -
2116 - itRenders('a select with a value and an onChange', async render => {
2117 - const e = await render(
2118 - <select value="bar" onChange={() => {}}>
2119 - {options}
2120 - </select>,
2121 - );
2122 - expectSelectValue(e, 'bar');
2123 - });
2124 -
2125 - itRenders('a select with a value and readOnly', async render => {
2126 - const e = await render(
2127 - <select value="bar" readOnly={true}>
2128 - {options}
2129 - </select>,
2130 - );
2131 - expectSelectValue(e, 'bar');
2132 - });
2133 -
2134 - itRenders(
2135 - 'a select with a multiple values and an onChange',
2136 - async render => {
2137 - const e = await render(
2138 - <select value={['bar', 'baz']} multiple={true} onChange={() => {}}>
2139 - {options}
2140 - </select>,
2141 - );
2142 - expectSelectValue(e, ['bar', 'baz']);
2143 - },
2144 - );
2145 -
2146 - itRenders(
2147 - 'a select with a multiple values and readOnly',
2148 - async render => {
2149 - const e = await render(
2150 - <select value={['bar', 'baz']} multiple={true} readOnly={true}>
2151 - {options}
2152 - </select>,
2153 - );
2154 - expectSelectValue(e, ['bar', 'baz']);
2155 - },
2156 - );
2157 -
2158 - itRenders(
2159 - 'a select with a value and no onChange/readOnly',
2160 - async render => {
2161 - // this configuration should raise a dev warning that value without
2162 - // onChange or readOnly is a mistake.
2163 - const e = await render(<select value="bar">{options}</select>, 1);
2164 - expectSelectValue(e, 'bar');
2165 - },
2166 - );
2167 -
2168 - itRenders('a select with a defaultValue', async render => {
2169 - const e = await render(<select defaultValue="bar">{options}</select>);
2170 - expectSelectValue(e, 'bar');
2171 - });
2172 -
2173 - itRenders('a select value overriding defaultValue', async render => {
2174 - const e = await render(
2175 - <select value="bar" defaultValue="baz" readOnly={true}>
2176 - {options}
2177 - </select>,
2178 - 1,
2179 - );
2180 - expectSelectValue(e, 'bar');
2181 - });
2182 -
2183 - itRenders(
2184 - 'a select value overriding defaultValue no matter the prop order',
2185 - async render => {
2186 - const e = await render(
2187 - <select defaultValue="baz" value="bar" readOnly={true}>
2188 - {options}
2189 - </select>,
2190 - 1,
2191 - );
2192 - expectSelectValue(e, 'bar');
2193 - },
2194 - );
2195 - });
2196 -
2197 - describe('user interaction', function() {
2198 - let ControlledInput,
2199 - ControlledTextArea,
2200 - ControlledCheckbox,
2201 - ControlledSelect;
2202 - beforeEach(() => {
2203 - ControlledInput = class extends React.Component {
2204 - constructor() {
2205 - super();
2206 - this.state = {value: 'Hello'};
2207 - }
2208 - handleChange(event) {
2209 - if (this.props.onChange) {
2210 - this.props.onChange(event);
2211 - }
2212 - this.setState({value: event.target.value});
2213 - }
2214 - render() {
2215 - return (
2216 - <input
2217 - value={this.state.value}
2218 - onChange={this.handleChange.bind(this)}
2219 - />
2220 - );
2221 - }
2222 - };
2223 - ControlledTextArea = class extends React.Component {
2224 - constructor() {
2225 - super();
2226 - this.state = {value: 'Hello'};
2227 - }
2228 - handleChange(event) {
2229 - if (this.props.onChange) {
2230 - this.props.onChange(event);
2231 - }
2232 - this.setState({value: event.target.value});
2233 - }
2234 - render() {
2235 - return (
2236 - <textarea
2237 - value={this.state.value}
2238 - onChange={this.handleChange.bind(this)}
2239 - />
2240 - );
2241 - }
2242 - };
2243 - ControlledCheckbox = class extends React.Component {
2244 - constructor() {
2245 - super();
2246 - this.state = {value: true};
2247 - }
2248 - handleChange(event) {
2249 - if (this.props.onChange) {
2250 - this.props.onChange(event);
2251 - }
2252 - this.setState({value: event.target.checked});
2253 - }
2254 - render() {
2255 - return (
2256 - <input
2257 - type="checkbox"
2258 - checked={this.state.value}
2259 - onChange={this.handleChange.bind(this)}
2260 - />
2261 - );
2262 - }
2263 - };
2264 - ControlledSelect = class extends React.Component {
2265 - constructor() {
2266 - super();
2267 - this.state = {value: 'Hello'};
2268 - }
2269 - handleChange(event) {
2270 - if (this.props.onChange) {
2271 - this.props.onChange(event);
2272 - }
2273 - this.setState({value: event.target.value});
2274 - }
2275 - render() {
2276 - return (
2277 - <select
2278 - value={this.state.value}
2279 - onChange={this.handleChange.bind(this)}>
2280 - <option key="1" value="Hello">
2281 - Hello
2282 - </option>
2283 - <option key="2" value="Goodbye">
2284 - Goodbye
2285 - </option>
2286 - </select>
2287 - );
2288 - }
2289 - };
2290 - });
2291 -
2292 - describe('user interaction with controlled inputs', function() {
2293 - itClientRenders('a controlled text input', async render => {
2294 - let changeCount = 0;
2295 - const e = await render(
2296 - <ControlledInput onChange={() => changeCount++} />,
2297 - );
2298 - expect(changeCount).toBe(0);
2299 - expect(e.value).toBe('Hello');
2300 -
2301 - // simulate a user typing.
2302 - e.value = 'Goodbye';
2303 - ReactTestUtils.Simulate.change(e);
2304 -
2305 - expect(changeCount).toBe(1);
2306 - expect(e.value).toBe('Goodbye');
2307 - });
2308 -
2309 - itClientRenders('a controlled textarea', async render => {
2310 - let changeCount = 0;
2311 - const e = await render(
2312 - <ControlledTextArea onChange={() => changeCount++} />,
2313 - );
2314 - expect(changeCount).toBe(0);
2315 - expect(e.value).toBe('Hello');
2316 -
2317 - // simulate a user typing.
2318 - e.value = 'Goodbye';
2319 - ReactTestUtils.Simulate.change(e);
2320 -
2321 - expect(changeCount).toBe(1);
2322 - expect(e.value).toBe('Goodbye');
2323 - });
2324 -
2325 - itClientRenders('a controlled checkbox', async render => {
2326 - let changeCount = 0;
2327 - const e = await render(
2328 - <ControlledCheckbox onChange={() => changeCount++} />,
2329 - );
2330 - expect(changeCount).toBe(0);
2331 - expect(e.checked).toBe(true);
2332 -
2333 - // simulate a user typing.
2334 - e.checked = false;
2335 - ReactTestUtils.Simulate.change(e);
2336 -
2337 - expect(changeCount).toBe(1);
2338 - expect(e.checked).toBe(false);
2339 - });
2340 -
2341 - itClientRenders('a controlled select', async render => {
2342 - let changeCount = 0;
2343 - const e = await render(
2344 - <ControlledSelect onChange={() => changeCount++} />,
2345 - );
2346 - expect(changeCount).toBe(0);
2347 - expect(e.value).toBe('Hello');
2348 -
2349 - // simulate a user typing.
2350 - e.value = 'Goodbye';
2351 - ReactTestUtils.Simulate.change(e);
2352 -
2353 - expect(changeCount).toBe(1);
2354 - expect(e.value).toBe('Goodbye');
2355 - });
2356 - });
2357 -
2358 - describe('user interaction with inputs before client render', function() {
2359 - // renders the element and changes the value **before** the client
2360 - // code has a chance to render; this simulates what happens when a
2361 - // user starts to interact with a server-rendered form before
2362 - // ReactDOM.render is called. the client render should NOT blow away
2363 - // the changes the user has made.
2364 - const testUserInteractionBeforeClientRender = async (
2365 - element,
2366 - initialValue = 'Hello',
2367 - changedValue = 'Goodbye',
2368 - valueKey = 'value',
2369 - ) => {
2370 - const field = await serverRender(element);
2371 - expect(field[valueKey]).toBe(initialValue);
2372 -
2373 - // simulate a user typing in the field **before** client-side reconnect happens.
2374 - field[valueKey] = changedValue;
2375 -
2376 - resetModules();
2377 - // client render on top of the server markup.
2378 - const clientField = await renderIntoDom(
2379 - element,
2380 - field.parentNode,
2381 - true,
2382 - );
2383 - // verify that the input field was not replaced.
2384 - // Note that we cannot use expect(clientField).toBe(field) because
2385 - // of jest bug #1772
2386 - expect(clientField === field).toBe(true);
2387 - // confirm that the client render has not changed what the user typed.
2388 - expect(clientField[valueKey]).toBe(changedValue);
2389 - };
2390 -
2391 - it('should not blow away user-entered text on successful reconnect to an uncontrolled input', () =>
2392 - testUserInteractionBeforeClientRender(
2393 - <input defaultValue="Hello" />,
2394 - ));
2395 -
2396 - it('should not blow away user-entered text on successful reconnect to a controlled input', async () => {
2397 - let changeCount = 0;
2398 - await testUserInteractionBeforeClientRender(
2399 - <ControlledInput onChange={() => changeCount++} />,
2400 - );
2401 - // note that there's a strong argument to be made that the DOM revival
2402 - // algorithm should notice that the user has changed the value and fire
2403 - // an onChange. however, it does not now, so that's what this tests.
2404 - expect(changeCount).toBe(0);
2405 - });
2406 -
2407 - it('should not blow away user-entered text on successful reconnect to an uncontrolled checkbox', () =>
2408 - testUserInteractionBeforeClientRender(
2409 - <input type="checkbox" defaultChecked={true} />,
2410 - true,
2411 - false,
2412 - 'checked',
2413 - ));
2414 -
2415 - it('should not blow away user-entered text on successful reconnect to a controlled checkbox', async () => {
2416 - let changeCount = 0;
2417 - await testUserInteractionBeforeClientRender(
2418 - <ControlledCheckbox onChange={() => changeCount++} />,
2419 - true,
2420 - false,
2421 - 'checked',
2422 - );
2423 - expect(changeCount).toBe(0);
2424 - });
2425 -
2426 - // skipping this test because React 15 does the wrong thing. it blows
2427 - // away the user's typing in the textarea.
2428 - xit('should not blow away user-entered text on successful reconnect to an uncontrolled textarea', () =>
2429 - testUserInteractionBeforeClientRender(
2430 - <textarea defaultValue="Hello" />,
2431 - ));
2432 -
2433 - // skipping this test because React 15 does the wrong thing. it blows
2434 - // away the user's typing in the textarea.
2435 - xit('should not blow away user-entered text on successful reconnect to a controlled textarea', async () => {
2436 - let changeCount = 0;
2437 - await testUserInteractionBeforeClientRender(
2438 - <ControlledTextArea onChange={() => changeCount++} />,
2439 - );
2440 - expect(changeCount).toBe(0);
2441 - });
2442 -
2443 - it('should not blow away user-selected value on successful reconnect to an uncontrolled select', () =>
2444 - testUserInteractionBeforeClientRender(
2445 - <select defaultValue="Hello">
2446 - <option key="1" value="Hello">
2447 - Hello
2448 - </option>
2449 - <option key="2" value="Goodbye">
2450 - Goodbye
2451 - </option>
2452 - </select>,
2453 - ));
2454 -
2455 - it('should not blow away user-selected value on successful reconnect to an controlled select', async () => {
2456 - let changeCount = 0;
2457 - await testUserInteractionBeforeClientRender(
2458 - <ControlledSelect onChange={() => changeCount++} />,
2459 - );
2460 - expect(changeCount).toBe(0);
2461 - });
2462 - });
2463 - });
2464 - });
2465 -
2466 - describe('context', function() {
2467 - let PurpleContext, RedContext;
2468 - beforeEach(() => {
2469 - class Parent extends React.Component {
2470 - getChildContext() {
2471 - return {text: this.props.text};
2472 - }
2473 - render() {
2474 - return this.props.children;
2475 - }
2476 - }
2477 - Parent.childContextTypes = {text: PropTypes.string};
2478 -
2479 - PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
2480 - RedContext = props => <Parent text="red">{props.children}</Parent>;
2481 - });
2482 -
2483 - itRenders('class child with context', async render => {
2484 - class ClassChildWithContext extends React.Component {
2485 - render() {
2486 - return <div>{this.context.text}</div>;
2487 - }
2488 - }
2489 - ClassChildWithContext.contextTypes = {text: PropTypes.string};
2490 -
2491 - const e = await render(
2492 - <PurpleContext>
2493 - <ClassChildWithContext />
2494 - </PurpleContext>,
2495 - );
2496 - expect(e.textContent).toBe('purple');
2497 - });
2498 -
2499 - itRenders('stateless child with context', async render => {
2500 - function StatelessChildWithContext(props, context) {
2501 - return <div>{context.text}</div>;
2502 - }
2503 - StatelessChildWithContext.contextTypes = {text: PropTypes.string};
2504 -
2505 - const e = await render(
2506 - <PurpleContext>
2507 - <StatelessChildWithContext />
2508 - </PurpleContext>,
2509 - );
2510 - expect(e.textContent).toBe('purple');
2511 - });
2512 -
2513 - itRenders('class child without context', async render => {
2514 - class ClassChildWithoutContext extends React.Component {
2515 - render() {
2516 - // this should render blank; context isn't passed to this component.
2517 - return <div>{this.context.text}</div>;
2518 - }
2519 - }
2520 -
2521 - const e = await render(
2522 - <PurpleContext>
2523 - <ClassChildWithoutContext />
2524 - </PurpleContext>,
2525 - );
2526 - expect(e.textContent).toBe('');
2527 - });
2528 -
2529 - itRenders('stateless child without context', async render => {
2530 - function StatelessChildWithoutContext(props, context) {
2531 - // this should render blank; context isn't passed to this component.
2532 - return <div>{context.text}</div>;
2533 - }
2534 -
2535 - const e = await render(
2536 - <PurpleContext>
2537 - <StatelessChildWithoutContext />
2538 - </PurpleContext>,
2539 - );
2540 - expect(e.textContent).toBe('');
2541 - });
2542 -
2543 - itRenders('class child with wrong context', async render => {
2544 - class ClassChildWithWrongContext extends React.Component {
2545 - render() {
2546 - // this should render blank; context.text isn't passed to this component.
2547 - return <div id="classWrongChild">{this.context.text}</div>;
2548 - }
2549 - }
2550 - ClassChildWithWrongContext.contextTypes = {foo: PropTypes.string};
2551 -
2552 - const e = await render(
2553 - <PurpleContext>
2554 - <ClassChildWithWrongContext />
2555 - </PurpleContext>,
2556 - );
2557 - expect(e.textContent).toBe('');
2558 - });
2559 -
2560 - itRenders('stateless child with wrong context', async render => {
2561 - function StatelessChildWithWrongContext(props, context) {
2562 - // this should render blank; context.text isn't passed to this component.
2563 - return <div id="statelessWrongChild">{context.text}</div>;
2564 - }
2565 - StatelessChildWithWrongContext.contextTypes = {
2566 - foo: PropTypes.string,
2567 - };
2568 -
2569 - const e = await render(
2570 - <PurpleContext>
2571 - <StatelessChildWithWrongContext />
2572 - </PurpleContext>,
2573 - );
2574 - expect(e.textContent).toBe('');
2575 - });
2576 -
2577 - itRenders('with context passed through to a grandchild', async render => {
2578 - function Grandchild(props, context) {
2579 - return <div>{context.text}</div>;
2580 - }
2581 - Grandchild.contextTypes = {text: PropTypes.string};
2582 -
2583 - const Child = props => <Grandchild />;
2584 -
2585 - const e = await render(
2586 - <PurpleContext>
2587 - <Child />
2588 - </PurpleContext>,
2589 - );
2590 - expect(e.textContent).toBe('purple');
2591 - });
2592 -
2593 - itRenders('a child context overriding a parent context', async render => {
2594 - const Grandchild = (props, context) => {
2595 - return <div>{context.text}</div>;
2596 - };
2597 - Grandchild.contextTypes = {text: PropTypes.string};
2598 -
2599 - const e = await render(
2600 - <PurpleContext>
2601 - <RedContext>
2602 - <Grandchild />
2603 - </RedContext>
2604 - </PurpleContext>,
2605 - );
2606 - expect(e.textContent).toBe('red');
2607 - });
2608 -
2609 - itRenders('a child context merged with a parent context', async render => {
2610 - class Parent extends React.Component {
2611 - getChildContext() {
2612 - return {text1: 'purple'};
2613 - }
2614 - render() {
2615 - return <Child />;
2616 - }
2617 - }
2618 - Parent.childContextTypes = {text1: PropTypes.string};
2619 -
2620 - class Child extends React.Component {
2621 - getChildContext() {
2622 - return {text2: 'red'};
2623 - }
2624 - render() {
2625 - return <Grandchild />;
2626 - }
2627 - }
2628 - Child.childContextTypes = {text2: PropTypes.string};
2629 -
2630 - const Grandchild = (props, context) => {
2631 - return (
2632 - <div>
2633 - <div id="first">{context.text1}</div>
2634 - <div id="second">{context.text2}</div>
2635 - </div>
2636 - );
2637 - };
2638 - Grandchild.contextTypes = {
2639 - text1: PropTypes.string,
2640 - text2: PropTypes.string,
2641 - };
2642 -
2643 - const e = await render(<Parent />);
2644 - expect(e.querySelector('#first').textContent).toBe('purple');
2645 - expect(e.querySelector('#second').textContent).toBe('red');
2646 - });
2647 -
2648 - itRenders(
2649 - 'with a call to componentWillMount before getChildContext',
2650 - async render => {
2651 - class WillMountContext extends React.Component {
2652 - getChildContext() {
2653 - return {text: this.state.text};
2654 - }
2655 - componentWillMount() {
2656 - this.setState({text: 'foo'});
2657 - }
2658 - render() {
2659 - return <Child />;
2660 - }
2661 - }
2662 - WillMountContext.childContextTypes = {text: PropTypes.string};
2663 -
2664 - const Child = (props, context) => {
2665 - return <div>{context.text}</div>;
2666 - };
2667 - Child.contextTypes = {text: PropTypes.string};
2668 -
2669 - const e = await render(<WillMountContext />);
2670 - expect(e.textContent).toBe('foo');
2671 - },
2672 - );
2673 -
2674 - itRenders(
2675 - 'if getChildContext exists but childContextTypes is missing with a warning',
2676 - async render => {
2677 - function HopefulChild(props, context) {
2678 - return context.foo || 'nope';
2679 - }
2680 - HopefulChild.contextTypes = {
2681 - foo: PropTypes.string,
2682 - };
2683 - class ForgetfulParent extends React.Component {
2684 - render() {
2685 - return <HopefulChild />;
2686 - }
2687 - getChildContext() {
2688 - return {foo: 'bar'};
2689 - }
2690 - }
2691 - const e = await render(<ForgetfulParent />, 1);
2692 - expect(e.textContent).toBe('nope');
2693 - },
2694 - );
2695 -
2696 - itThrowsWhenRendering(
2697 - 'if getChildContext returns a value not in childContextTypes',
2698 - render => {
2699 - class MyComponent extends React.Component {
2700 - render() {
2701 - return <div />;
2702 - }
2703 - getChildContext() {
2704 - return {value1: 'foo', value2: 'bar'};
2705 - }
2706 - }
2707 - MyComponent.childContextTypes = {value1: PropTypes.string};
2708 - return render(<MyComponent />);
2709 - },
2710 - 'MyComponent.getChildContext(): key "value2" is not defined in childContextTypes.',
2711 - );
2712 - });
2713 -
2714 - describe('refs', function() {
2715 - it('should not run ref code on server', async () => {
2716 - let refCount = 0;
2717 - class RefsComponent extends React.Component {
2718 - render() {
2719 - return <div ref={e => refCount++} />;
2720 - }
2721 - }
2722 - await expectMarkupMatch(<RefsComponent />, <div />);
2723 - expect(refCount).toBe(0);
2724 - });
2725 -
2726 - it('should run ref code on client', async () => {
2727 - let refCount = 0;
2728 - class RefsComponent extends React.Component {
2729 - render() {
2730 - return <div ref={e => refCount++} />;
2731 - }
2732 - }
2733 - await expectMarkupMatch(<div />, <RefsComponent />);
2734 - expect(refCount).toBe(1);
2735 - });
2736 -
2737 - it('should send the correct element to ref functions on client', async () => {
2738 - let refElement = null;
2739 - class RefsComponent extends React.Component {
2740 - render() {
2741 - return <div ref={e => (refElement = e)} />;
2742 - }
2743 - }
2744 - const e = await clientRenderOnServerString(<RefsComponent />);
2745 - expect(refElement).not.toBe(null);
2746 - expect(refElement).toBe(e);
2747 - });
2748 -
2749 - it('should have string refs on client when rendered over server markup', async () => {
2750 - class RefsComponent extends React.Component {
2751 - render() {
2752 - return <div ref="myDiv" />;
2753 - }
2754 - }
2755 -
2756 - const markup = ReactDOMServer.renderToString(<RefsComponent />);
2757 - const root = document.createElement('div');
2758 - root.innerHTML = markup;
2759 - let component = null;
2760 - resetModules();
2761 - await asyncReactDOMRender(
2762 - <RefsComponent ref={e => (component = e)} />,
2763 - root,
2764 - true,
2765 - );
2766 - expect(component.refs.myDiv).toBe(root.firstChild);
2767 - });
2768 - });
2769 -
2770 - describe('reconnecting to server markup', function() {
2771 - var EmptyComponent;
2772 - beforeEach(() => {
2773 - EmptyComponent = class extends React.Component {
2774 - render() {
2775 - return null;
2776 - }
2777 - };
2778 - });
2779 -
2780 - describe('elements', function() {
2781 - describe('reconnecting different component implementations', function() {
2782 - let ES6ClassComponent, PureComponent, bareElement;
2783 - beforeEach(() => {
2784 - // try each type of component on client and server.
2785 - ES6ClassComponent = class extends React.Component {
2786 - render() {
2787 - return <div id={this.props.id} />;
2788 - }
2789 - };
2790 - PureComponent = props => <div id={props.id} />;
2791 - bareElement = <div id="foobarbaz" />;
2792 - });
2793 -
2794 - it('should reconnect ES6 Class to ES6 Class', () =>
2795 - expectMarkupMatch(
2796 - <ES6ClassComponent id="foobarbaz" />,
2797 - <ES6ClassComponent id="foobarbaz" />,
2798 - ));
2799 -
2800 - it('should reconnect Pure Component to ES6 Class', () =>
2801 - expectMarkupMatch(
2802 - <ES6ClassComponent id="foobarbaz" />,
2803 - <PureComponent id="foobarbaz" />,
2804 - ));
2805 -
2806 - it('should reconnect Bare Element to ES6 Class', () =>
2807 - expectMarkupMatch(<ES6ClassComponent id="foobarbaz" />, bareElement));
2808 -
2809 - it('should reconnect ES6 Class to Pure Component', () =>
2810 - expectMarkupMatch(
2811 - <PureComponent id="foobarbaz" />,
2812 - <ES6ClassComponent id="foobarbaz" />,
2813 - ));
2814 -
2815 - it('should reconnect Pure Component to Pure Component', () =>
2816 - expectMarkupMatch(
2817 - <PureComponent id="foobarbaz" />,
2818 - <PureComponent id="foobarbaz" />,
2819 - ));
2820 -
2821 - it('should reconnect Bare Element to Pure Component', () =>
2822 - expectMarkupMatch(<PureComponent id="foobarbaz" />, bareElement));
2823 -
2824 - it('should reconnect ES6 Class to Bare Element', () =>
2825 - expectMarkupMatch(bareElement, <ES6ClassComponent id="foobarbaz" />));
2826 -
2827 - it('should reconnect Pure Component to Bare Element', () =>
2828 - expectMarkupMatch(bareElement, <PureComponent id="foobarbaz" />));
2829 -
2830 - it('should reconnect Bare Element to Bare Element', () =>
2831 - expectMarkupMatch(bareElement, bareElement));
2832 - });
2833 -
2834 - it('should error reconnecting different element types', () =>
2835 - expectMarkupMismatch(<div />, <span />));
2836 -
2837 - it('should error reconnecting fewer root children', () =>
2838 - expectMarkupMismatch(<span key="a" />, [
2839 - <span key="a" />,
2840 - <span key="b" />,
2841 - ]));
2842 -
2843 - it('should error reconnecting missing attributes', () =>
2844 - expectMarkupMismatch(<div id="foo" />, <div />));
2845 -
2846 - it('should error reconnecting added attributes', () =>
2847 - expectMarkupMismatch(<div />, <div id="foo" />));
2848 -
2849 - it('should error reconnecting different attribute values', () =>
2850 - expectMarkupMismatch(<div id="foo" />, <div id="bar" />));
2851 -
2852 - it('can explicitly ignore errors reconnecting different element types of children', () =>
2853 - expectMarkupMatch(
2854 - <div>
2855 - <div />
2856 - </div>,
2857 - <div suppressHydrationWarning={true}>
2858 - <span />
2859 - </div>,
2860 - ));
2861 -
2862 - it('can explicitly ignore errors reconnecting missing attributes', () =>
2863 - expectMarkupMatch(
2864 - <div id="foo" />,
2865 - <div suppressHydrationWarning={true} />,
2866 - ));
2867 -
2868 - it('can explicitly ignore errors reconnecting added attributes', () =>
2869 - expectMarkupMatch(
2870 - <div />,
2871 - <div id="foo" suppressHydrationWarning={true} />,
2872 - ));
2873 -
2874 - it('can explicitly ignore errors reconnecting different attribute values', () =>
2875 - expectMarkupMatch(
2876 - <div id="foo" />,
2877 - <div id="bar" suppressHydrationWarning={true} />,
2878 - ));
2879 -
2880 - it('can not deeply ignore errors reconnecting different attribute values', () =>
2881 - expectMarkupMismatch(
2882 - <div>
2883 - <div id="foo" />
2884 - </div>,
2885 - <div suppressHydrationWarning={true}>
2886 - <div id="bar" />
2887 - </div>,
2888 - ));
2889 - });
2890 -
2891 - describe('inline styles', function() {
2892 - it('should error reconnecting missing style attribute', () =>
2893 - expectMarkupMismatch(<div style={{width: '1px'}} />, <div />));
2894 -
2895 - it('should error reconnecting added style attribute', () =>
2896 - expectMarkupMismatch(<div />, <div style={{width: '1px'}} />));
2897 -
2898 - it('should error reconnecting empty style attribute', () =>
2899 - expectMarkupMismatch(
2900 - <div style={{width: '1px'}} />,
2901 - <div style={{}} />,
2902 - ));
2903 -
2904 - it('should error reconnecting added style values', () =>
2905 - expectMarkupMismatch(
2906 - <div style={{}} />,
2907 - <div style={{width: '1px'}} />,
2908 - ));
2909 -
2910 - it('should error reconnecting different style values', () =>
2911 - expectMarkupMismatch(
2912 - <div style={{width: '1px'}} />,
2913 - <div style={{width: '2px'}} />,
2914 - ));
2915 -
2916 - it('should reconnect number and string versions of a number', () =>
2917 - expectMarkupMatch(
2918 - <div style={{width: '1px', height: 2}} />,
2919 - <div style={{width: 1, height: '2px'}} />,
2920 - ));
2921 -
2922 - it('should error reconnecting reordered style values', () =>
2923 - expectMarkupMismatch(
2924 - <div style={{width: '1px', fontSize: '2px'}} />,
2925 - <div style={{fontSize: '2px', width: '1px'}} />,
2926 - ));
2927 -
2928 - it('can explicitly ignore errors reconnecting added style values', () =>
2929 - expectMarkupMatch(
2930 - <div style={{}} />,
2931 - <div style={{width: '1px'}} suppressHydrationWarning={true} />,
2932 - ));
2933 -
2934 - it('can explicitly ignore reconnecting different style values', () =>
2935 - expectMarkupMatch(
2936 - <div style={{width: '1px'}} />,
2937 - <div style={{width: '2px'}} suppressHydrationWarning={true} />,
2938 - ));
2939 - });
2940 -
2941 - describe('text nodes', function() {
2942 - it('should error reconnecting different text', () =>
2943 - expectMarkupMismatch(<div>Text</div>, <div>Other Text</div>));
2944 -
2945 - it('should reconnect a div with a number and string version of number', () =>
2946 - expectMarkupMatch(<div>{2}</div>, <div>2</div>));
2947 -
2948 - it('should error reconnecting different numbers', () =>
2949 - expectMarkupMismatch(<div>{2}</div>, <div>{3}</div>));
2950 -
2951 - it('should error reconnecting different number from text', () =>
2952 - expectMarkupMismatch(<div>{2}</div>, <div>3</div>));
2953 -
2954 - it('should error reconnecting different text in two code blocks', () =>
2955 - expectMarkupMismatch(
2956 - <div>
2957 - {'Text1'}
2958 - {'Text2'}
2959 - </div>,
2960 - <div>
2961 - {'Text1'}
2962 - {'Text3'}
2963 - </div>,
2964 - ));
2965 -
2966 - it('can explicitly ignore reconnecting different text', () =>
2967 - expectMarkupMatch(
2968 - <div>Text</div>,
2969 - <div suppressHydrationWarning={true}>Other Text</div>,
2970 - ));
2971 -
2972 - it('can explicitly ignore reconnecting different text in two code blocks', () =>
2973 - expectMarkupMatch(
2974 - <div suppressHydrationWarning={true}>
2975 - {'Text1'}
2976 - {'Text2'}
2977 - </div>,
2978 - <div suppressHydrationWarning={true}>
2979 - {'Text1'}
2980 - {'Text3'}
2981 - </div>,
2982 - ));
2983 - });
2984 -
2985 - describe('element trees and children', function() {
2986 - it('should error reconnecting missing children', () =>
2987 - expectMarkupMismatch(
2988 - <div>
2989 - <div />
2990 - </div>,
2991 - <div />,
2992 - ));
2993 -
2994 - it('should error reconnecting added children', () =>
2995 - expectMarkupMismatch(
2996 - <div />,
2997 - <div>
2998 - <div />
2999 - </div>,
3000 - ));
3001 -
3002 - it('should error reconnecting more children', () =>
3003 - expectMarkupMismatch(
3004 - <div>
3005 - <div />
3006 - </div>,
3007 - <div>
3008 - <div />
3009 - <div />
3010 - </div>,
3011 - ));
3012 -
3013 - it('should error reconnecting fewer children', () =>
3014 - expectMarkupMismatch(
3015 - <div>
3016 - <div />
3017 - <div />
3018 - </div>,
3019 - <div>
3020 - <div />
3021 - </div>,
3022 - ));
3023 -
3024 - it('should error reconnecting reordered children', () =>
3025 - expectMarkupMismatch(
3026 - <div>
3027 - <div />
3028 - <span />
3029 - </div>,
3030 - <div>
3031 - <span />
3032 - <div />
3033 - </div>,
3034 - ));
3035 -
3036 - it('should error reconnecting a div with children separated by whitespace on the client', () =>
3037 - expectMarkupMismatch(
3038 - <div id="parent">
3039 - <div id="child1" />
3040 - <div id="child2" />
3041 - </div>,
3042 - // prettier-ignore
3043 - <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
3044 - ));
3045 -
3046 - it('should error reconnecting a div with children separated by different whitespace on the server', () =>
3047 - expectMarkupMismatch(
3048 - // prettier-ignore
3049 - <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
3050 - <div id="parent">
3051 - <div id="child1" />
3052 - <div id="child2" />
3053 - </div>,
3054 - ));
3055 -
3056 - it('should error reconnecting a div with children separated by different whitespace', () =>
3057 - expectMarkupMismatch(
3058 - <div id="parent">
3059 - <div id="child1" /> <div id="child2" />
3060 - </div>,
3061 - // prettier-ignore
3062 - <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
3063 - ));
3064 -
3065 - it('can distinguish an empty component from a dom node', () =>
3066 - expectMarkupMismatch(
3067 - <div>
3068 - <span />
3069 - </div>,
3070 - <div>
3071 - <EmptyComponent />
3072 - </div>,
3073 - ));
3074 -
3075 - it('can distinguish an empty component from an empty text component', () =>
3076 - expectMarkupMatch(
3077 - <div>
3078 - <EmptyComponent />
3079 - </div>,
3080 - <div>{''}</div>,
3081 - ));
3082 -
3083 - it('can explicitly ignore reconnecting more children', () =>
3084 - expectMarkupMatch(
3085 - <div>
3086 - <div />
3087 - </div>,
3088 - <div suppressHydrationWarning={true}>
3089 - <div />
3090 - <div />
3091 - </div>,
3092 - ));
3093 -
3094 - it('can explicitly ignore reconnecting fewer children', () =>
3095 - expectMarkupMatch(
3096 - <div>
3097 - <div />
3098 - <div />
3099 - </div>,
3100 - <div suppressHydrationWarning={true}>
3101 - <div />
3102 - </div>,
3103 - ));
3104 -
3105 - it('can explicitly ignore reconnecting reordered children', () =>
3106 - expectMarkupMatch(
3107 - <div suppressHydrationWarning={true}>
3108 - <div />
3109 - <span />
3110 - </div>,
3111 - <div suppressHydrationWarning={true}>
3112 - <span />
3113 - <div />
3114 - </div>,
3115 - ));
3116 -
3117 - it('can not deeply ignore reconnecting reordered children', () =>
3118 - expectMarkupMismatch(
3119 - <div suppressHydrationWarning={true}>
3120 - <div>
3121 - <div />
3122 - <span />
3123 - </div>
3124 - </div>,
3125 - <div suppressHydrationWarning={true}>
3126 - <div>
3127 - <span />
3128 - <div />
3129 - </div>
3130 - </div>,
3131 - ));
3132 - });
3133 -
3134 - // Markup Mismatches: misc
3135 - it('should error reconnecting a div with different dangerouslySetInnerHTML', () =>
3136 - expectMarkupMismatch(
3137 - <div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
3138 - <div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
3139 - ));
3140 -
3141 - it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () =>
3142 - expectMarkupMatch(
3143 - <div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
3144 - <div
3145 - dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}}
3146 - suppressHydrationWarning={true}
3147 - />,
3148 - ));
3149 - });
3150 -});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationAttributes-test.js new
+597
@@ -0,0 +1,597 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +let React;
15 +let ReactDOM;
16 +let ReactDOMServer;
17 +
18 +function initModules() {
19 + // Reset warning cache.
20 + jest.resetModuleRegistry();
21 + React = require('react');
22 + ReactDOM = require('react-dom');
23 + ReactDOMServer = require('react-dom/server');
24 +
25 + // Make them available to the helpers.
26 + return {
27 + ReactDOM,
28 + ReactDOMServer,
29 + };
30 +}
31 +
32 +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
33 +
34 +describe('ReactDOMServerIntegration', () => {
35 + beforeEach(() => {
36 + resetModules();
37 + });
38 +
39 + describe('property to attribute mapping', function() {
40 + describe('string properties', function() {
41 + itRenders('simple numbers', async render => {
42 + const e = await render(<div width={30} />);
43 + expect(e.getAttribute('width')).toBe('30');
44 + });
45 +
46 + itRenders('simple strings', async render => {
47 + const e = await render(<div width={'30'} />);
48 + expect(e.getAttribute('width')).toBe('30');
49 + });
50 +
51 + itRenders('no string prop with true value', async render => {
52 + const e = await render(<a href={true} />, 1);
53 + expect(e.hasAttribute('href')).toBe(false);
54 + });
55 +
56 + itRenders('no string prop with false value', async render => {
57 + const e = await render(<a href={false} />, 1);
58 + expect(e.hasAttribute('href')).toBe(false);
59 + });
60 +
61 + itRenders('no string prop with null value', async render => {
62 + const e = await render(<div width={null} />);
63 + expect(e.hasAttribute('width')).toBe(false);
64 + });
65 + });
66 +
67 + describe('boolean properties', function() {
68 + itRenders('boolean prop with true value', async render => {
69 + const e = await render(<div hidden={true} />);
70 + expect(e.getAttribute('hidden')).toBe('');
71 + });
72 +
73 + itRenders('boolean prop with false value', async render => {
74 + const e = await render(<div hidden={false} />);
75 + expect(e.getAttribute('hidden')).toBe(null);
76 + });
77 +
78 + itRenders('boolean prop with self value', async render => {
79 + const e = await render(<div hidden="hidden" />);
80 + expect(e.getAttribute('hidden')).toBe('');
81 + });
82 +
83 + // this does not seem like correct behavior, since hidden="" in HTML indicates
84 + // that the boolean property is present. however, it is how the current code
85 + // behaves, so the test is included here.
86 + itRenders('boolean prop with "" value', async render => {
87 + const e = await render(<div hidden="" />);
88 + expect(e.getAttribute('hidden')).toBe(null);
89 + });
90 +
91 + // this seems like it might mask programmer error, but it's existing behavior.
92 + itRenders('boolean prop with string value', async render => {
93 + const e = await render(<div hidden="foo" />);
94 + expect(e.getAttribute('hidden')).toBe('');
95 + });
96 +
97 + // this seems like it might mask programmer error, but it's existing behavior.
98 + itRenders('boolean prop with array value', async render => {
99 + const e = await render(<div hidden={['foo', 'bar']} />);
100 + expect(e.getAttribute('hidden')).toBe('');
101 + });
102 +
103 + // this seems like it might mask programmer error, but it's existing behavior.
104 + itRenders('boolean prop with object value', async render => {
105 + const e = await render(<div hidden={{foo: 'bar'}} />);
106 + expect(e.getAttribute('hidden')).toBe('');
107 + });
108 +
109 + // this seems like it might mask programmer error, but it's existing behavior.
110 + itRenders('boolean prop with non-zero number value', async render => {
111 + const e = await render(<div hidden={10} />);
112 + expect(e.getAttribute('hidden')).toBe('');
113 + });
114 +
115 + // this seems like it might mask programmer error, but it's existing behavior.
116 + itRenders('boolean prop with zero value', async render => {
117 + const e = await render(<div hidden={0} />);
118 + expect(e.getAttribute('hidden')).toBe(null);
119 + });
120 +
121 + itRenders('no boolean prop with null value', async render => {
122 + const e = await render(<div hidden={null} />);
123 + expect(e.hasAttribute('hidden')).toBe(false);
124 + });
125 + });
126 +
127 + describe('download property (combined boolean/string attribute)', function() {
128 + itRenders('download prop with true value', async render => {
129 + const e = await render(<a download={true} />);
130 + expect(e.getAttribute('download')).toBe('');
131 + });
132 +
133 + itRenders('download prop with false value', async render => {
134 + const e = await render(<a download={false} />);
135 + expect(e.getAttribute('download')).toBe(null);
136 + });
137 +
138 + itRenders('download prop with string value', async render => {
139 + const e = await render(<a download="myfile" />);
140 + expect(e.getAttribute('download')).toBe('myfile');
141 + });
142 +
143 + itRenders('download prop with string "false" value', async render => {
144 + const e = await render(<a download="false" />);
145 + expect(e.getAttribute('download')).toBe('false');
146 + });
147 +
148 + itRenders('download prop with string "true" value', async render => {
149 + const e = await render(<a download={'true'} />);
150 + expect(e.getAttribute('download')).toBe('true');
151 + });
152 +
153 + itRenders('download prop with number 0 value', async render => {
154 + const e = await render(<a download={0} />);
155 + expect(e.getAttribute('download')).toBe('0');
156 + });
157 +
158 + itRenders('no download prop with null value', async render => {
159 + const e = await render(<div download={null} />);
160 + expect(e.hasAttribute('download')).toBe(false);
161 + });
162 +
163 + itRenders('no download prop with undefined value', async render => {
164 + const e = await render(<div download={undefined} />);
165 + expect(e.hasAttribute('download')).toBe(false);
166 + });
167 + });
168 +
169 + describe('className property', function() {
170 + itRenders('className prop with string value', async render => {
171 + const e = await render(<div className="myClassName" />);
172 + expect(e.getAttribute('class')).toBe('myClassName');
173 + });
174 +
175 + itRenders('className prop with empty string value', async render => {
176 + const e = await render(<div className="" />);
177 + expect(e.getAttribute('class')).toBe('');
178 + });
179 +
180 + itRenders('no className prop with true value', async render => {
181 + const e = await render(<div className={true} />, 1);
182 + expect(e.hasAttribute('class')).toBe(false);
183 + });
184 +
185 + itRenders('no className prop with false value', async render => {
186 + const e = await render(<div className={false} />, 1);
187 + expect(e.hasAttribute('class')).toBe(false);
188 + });
189 +
190 + itRenders('no className prop with null value', async render => {
191 + const e = await render(<div className={null} />);
192 + expect(e.hasAttribute('className')).toBe(false);
193 + });
194 +
195 + itRenders('badly cased className with a warning', async render => {
196 + const e = await render(<div classname="test" />, 1);
197 + expect(e.hasAttribute('class')).toBe(false);
198 + expect(e.hasAttribute('classname')).toBe(true);
199 + });
200 +
201 + itRenders(
202 + 'className prop when given the alias with a warning',
203 + async render => {
204 + const e = await render(<div class="test" />, 1);
205 + expect(e.className).toBe('test');
206 + },
207 + );
208 +
209 + itRenders(
210 + 'className prop when given a badly cased alias',
211 + async render => {
212 + const e = await render(<div cLASs="test" />, 1);
213 + expect(e.className).toBe('test');
214 + },
215 + );
216 +
217 + itRenders('class for custom elements', async render => {
218 + const e = await render(<div is="custom-element" class="test" />, 0);
219 + expect(e.getAttribute('class')).toBe('test');
220 + });
221 +
222 + itRenders('className for custom elements', async render => {
223 + const e = await render(<div is="custom-element" className="test" />, 0);
224 + expect(e.getAttribute('className')).toBe('test');
225 + });
226 + });
227 +
228 + describe('htmlFor property', function() {
229 + itRenders('htmlFor with string value', async render => {
230 + const e = await render(<div htmlFor="myFor" />);
231 + expect(e.getAttribute('for')).toBe('myFor');
232 + });
233 +
234 + itRenders('no badly cased htmlfor', async render => {
235 + const e = await render(<div htmlfor="myFor" />, 1);
236 + expect(e.hasAttribute('for')).toBe(false);
237 + expect(e.getAttribute('htmlfor')).toBe('myFor');
238 + });
239 +
240 + itRenders('htmlFor with an empty string', async render => {
241 + const e = await render(<div htmlFor="" />);
242 + expect(e.getAttribute('for')).toBe('');
243 + });
244 +
245 + itRenders('no htmlFor prop with true value', async render => {
246 + const e = await render(<div htmlFor={true} />, 1);
247 + expect(e.hasAttribute('for')).toBe(false);
248 + });
249 +
250 + itRenders('no htmlFor prop with false value', async render => {
251 + const e = await render(<div htmlFor={false} />, 1);
252 + expect(e.hasAttribute('for')).toBe(false);
253 + });
254 +
255 + itRenders('no htmlFor prop with null value', async render => {
256 + const e = await render(<div htmlFor={null} />);
257 + expect(e.hasAttribute('htmlFor')).toBe(false);
258 + });
259 +
260 + itRenders('htmlFor attribute on custom elements', async render => {
261 + const e = await render(<div is="custom-element" htmlFor="test" />);
262 + expect(e.getAttribute('htmlFor')).toBe('test');
263 + });
264 +
265 + itRenders('for attribute on custom elements', async render => {
266 + const e = await render(<div is="custom-element" for="test" />);
267 + expect(e.getAttribute('for')).toBe('test');
268 + });
269 + });
270 +
271 + describe('numeric properties', function() {
272 + itRenders(
273 + 'positive numeric property with positive value',
274 + async render => {
275 + const e = await render(<input size={2} />);
276 + expect(e.getAttribute('size')).toBe('2');
277 + },
278 + );
279 +
280 + itRenders(
281 + 'no positive numeric property with zero value',
282 + async render => {
283 + const e = await render(<input size={0} />);
284 + expect(e.hasAttribute('size')).toBe(false);
285 + },
286 + );
287 +
288 + itRenders('numeric property with zero value', async render => {
289 + const e = await render(<ol start={0} />);
290 + expect(e.getAttribute('start')).toBe('0');
291 + });
292 + });
293 +
294 + describe('props with special meaning in React', function() {
295 + itRenders('no ref attribute', async render => {
296 + class RefComponent extends React.Component {
297 + render() {
298 + return <div ref="foo" />;
299 + }
300 + }
301 + const e = await render(<RefComponent />);
302 + expect(e.getAttribute('ref')).toBe(null);
303 + });
304 +
305 + itRenders('no children attribute', async render => {
306 + const e = await render(React.createElement('div', {}, 'foo'));
307 + expect(e.getAttribute('children')).toBe(null);
308 + });
309 +
310 + itRenders('no key attribute', async render => {
311 + const e = await render(<div key="foo" />);
312 + expect(e.getAttribute('key')).toBe(null);
313 + });
314 +
315 + itRenders('no dangerouslySetInnerHTML attribute', async render => {
316 + const e = await render(
317 + <div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
318 + );
319 + expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
320 + });
321 +
322 + itRenders('no suppressContentEditableWarning attribute', async render => {
323 + const e = await render(<div suppressContentEditableWarning={true} />);
324 + expect(e.getAttribute('suppressContentEditableWarning')).toBe(null);
325 + });
326 +
327 + itRenders('no suppressHydrationWarning attribute', async render => {
328 + const e = await render(<span suppressHydrationWarning={true} />);
329 + expect(e.getAttribute('suppressHydrationWarning')).toBe(null);
330 + });
331 + });
332 +
333 + describe('inline styles', function() {
334 + itRenders('simple styles', async render => {
335 + const e = await render(<div style={{color: 'red', width: '30px'}} />);
336 + expect(e.style.color).toBe('red');
337 + expect(e.style.width).toBe('30px');
338 + });
339 +
340 + itRenders('relevant styles with px', async render => {
341 + const e = await render(
342 + <div
343 + style={{
344 + left: 0,
345 + margin: 16,
346 + opacity: 0.5,
347 + padding: '4px',
348 + }}
349 + />,
350 + );
351 + expect(e.style.left).toBe('0px');
352 + expect(e.style.margin).toBe('16px');
353 + expect(e.style.opacity).toBe('0.5');
354 + expect(e.style.padding).toBe('4px');
355 + });
356 +
357 + itRenders('custom properties', async render => {
358 + const e = await render(<div style={{'--foo': 5}} />);
359 + // This seems like an odd way computed properties are exposed in jsdom.
360 + // In a real browser we'd read it with e.style.getPropertyValue('--foo')
361 + expect(e.style.Foo).toBe('5');
362 + });
363 +
364 + itRenders('no undefined styles', async render => {
365 + const e = await render(
366 + <div style={{color: undefined, width: '30px'}} />,
367 + );
368 + expect(e.style.color).toBe('');
369 + expect(e.style.width).toBe('30px');
370 + });
371 +
372 + itRenders('no null styles', async render => {
373 + const e = await render(<div style={{color: null, width: '30px'}} />);
374 + expect(e.style.color).toBe('');
375 + expect(e.style.width).toBe('30px');
376 + });
377 +
378 + itRenders('no empty styles', async render => {
379 + const e = await render(<div style={{color: null, width: null}} />);
380 + expect(e.style.color).toBe('');
381 + expect(e.style.width).toBe('');
382 + expect(e.hasAttribute('style')).toBe(false);
383 + });
384 +
385 + itRenders('unitless-number rules with prefixes', async render => {
386 + const {style} = await render(
387 + <div
388 + style={{
389 + lineClamp: 10,
390 + WebkitLineClamp: 10,
391 + MozFlexGrow: 10,
392 + msFlexGrow: 10,
393 + msGridRow: 10,
394 + msGridRowEnd: 10,
395 + msGridRowSpan: 10,
396 + msGridRowStart: 10,
397 + msGridColumn: 10,
398 + msGridColumnEnd: 10,
399 + msGridColumnSpan: 10,
400 + msGridColumnStart: 10,
401 + }}
402 + />,
403 + );
404 +
405 + expect(style.lineClamp).toBe('10');
406 + expect(style.WebkitLineClamp).toBe('10');
407 + expect(style.MozFlexGrow).toBe('10');
408 + // jsdom is inconsistent in the style property name
409 + // it uses on the client and when processing server markup.
410 + // But it should be there either way.
411 + expect(style.MsFlexGrow || style.msFlexGrow).toBe('10');
412 + expect(style.MsGridRow || style.msGridRow).toBe('10');
413 + expect(style.MsGridRowEnd || style.msGridRowEnd).toBe('10');
414 + expect(style.MsGridRowSpan || style.msGridRowSpan).toBe('10');
415 + expect(style.MsGridRowStart || style.msGridRowStart).toBe('10');
416 + expect(style.MsGridColumn || style.msGridColumn).toBe('10');
417 + expect(style.MsGridColumnEnd || style.msGridColumnEnd).toBe('10');
418 + expect(style.MsGridColumnSpan || style.msGridColumnSpan).toBe('10');
419 + expect(style.MsGridColumnStart || style.msGridColumnStart).toBe('10');
420 + });
421 + });
422 +
423 + describe('aria attributes', function() {
424 + itRenders('simple strings', async render => {
425 + const e = await render(<div aria-label="hello" />);
426 + expect(e.getAttribute('aria-label')).toBe('hello');
427 + });
428 +
429 + // this probably is just masking programmer error, but it is existing behavior.
430 + itRenders('aria string prop with false value', async render => {
431 + const e = await render(<div aria-label={false} />);
432 + expect(e.getAttribute('aria-label')).toBe('false');
433 + });
434 +
435 + itRenders('no aria prop with null value', async render => {
436 + const e = await render(<div aria-label={null} />);
437 + expect(e.hasAttribute('aria-label')).toBe(false);
438 + });
439 +
440 + itRenders('"aria" attribute with a warning', async render => {
441 + // Reserved for future use.
442 + const e = await render(<div aria="hello" />, 1);
443 + expect(e.getAttribute('aria')).toBe('hello');
444 + });
445 + });
446 +
447 + describe('cased attributes', function() {
448 + itRenders(
449 + 'badly cased aliased HTML attribute with a warning',
450 + async render => {
451 + const e = await render(<meta httpequiv="refresh" />, 1);
452 + expect(e.hasAttribute('http-equiv')).toBe(false);
453 + expect(e.getAttribute('httpequiv')).toBe('refresh');
454 + },
455 + );
456 +
457 + itRenders('badly cased SVG attribute with a warning', async render => {
458 + const e = await render(<text textlength="10" />, 1);
459 + expect(e.getAttribute('textLength')).toBe('10');
460 + });
461 +
462 + itRenders('no badly cased aliased SVG attribute alias', async render => {
463 + const e = await render(<text strokedasharray="10 10" />, 1);
464 + expect(e.hasAttribute('stroke-dasharray')).toBe(false);
465 + expect(e.getAttribute('strokedasharray')).toBe('10 10');
466 + });
467 +
468 + itRenders(
469 + 'no badly cased original SVG attribute that is aliased',
470 + async render => {
471 + const e = await render(<text stroke-dasharray="10 10" />, 1);
472 + expect(e.getAttribute('stroke-dasharray')).toBe('10 10');
473 + },
474 + );
475 + });
476 +
477 + describe('unknown attributes', function() {
478 + itRenders('unknown attributes', async render => {
479 + const e = await render(<div foo="bar" />);
480 + expect(e.getAttribute('foo')).toBe('bar');
481 + });
482 +
483 + itRenders('unknown data- attributes', async render => {
484 + const e = await render(<div data-foo="bar" />);
485 + expect(e.getAttribute('data-foo')).toBe('bar');
486 + });
487 +
488 + itRenders('badly cased reserved attributes', async render => {
489 + const e = await render(<div CHILDREN="5" />, 1);
490 + expect(e.getAttribute('CHILDREN')).toBe('5');
491 + });
492 +
493 + itRenders('"data" attribute', async render => {
494 + // For `<object />` acts as `src`.
495 + const e = await render(<object data="hello" />);
496 + expect(e.getAttribute('data')).toBe('hello');
497 + });
498 +
499 + itRenders('no unknown data- attributes with null value', async render => {
500 + const e = await render(<div data-foo={null} />);
501 + expect(e.hasAttribute('data-foo')).toBe(false);
502 + });
503 +
504 + itRenders('unknown data- attributes with casing', async render => {
505 + const e = await render(<div data-fooBar="true" />, 1);
506 + expect(e.getAttribute('data-foobar')).toBe('true');
507 + });
508 +
509 + itRenders('unknown data- attributes with boolean true', async render => {
510 + const e = await render(<div data-foobar={true} />);
511 + expect(e.getAttribute('data-foobar')).toBe('true');
512 + });
513 +
514 + itRenders('unknown data- attributes with boolean false', async render => {
515 + const e = await render(<div data-foobar={false} />);
516 + expect(e.getAttribute('data-foobar')).toBe('false');
517 + });
518 +
519 + itRenders(
520 + 'no unknown data- attributes with casing and null value',
521 + async render => {
522 + const e = await render(<div data-fooBar={null} />, 1);
523 + expect(e.hasAttribute('data-foobar')).toBe(false);
524 + },
525 + );
526 +
527 + itRenders('custom attributes for non-standard elements', async render => {
528 + const e = await render(<nonstandard foo="bar" />);
529 + expect(e.getAttribute('foo')).toBe('bar');
530 + });
531 +
532 + itRenders('unknown attributes for custom elements', async render => {
533 + const e = await render(<custom-element foo="bar" />);
534 + expect(e.getAttribute('foo')).toBe('bar');
535 + });
536 +
537 + itRenders(
538 + 'no unknown attributes for custom elements with null value',
539 + async render => {
540 + const e = await render(<custom-element foo={null} />);
541 + expect(e.hasAttribute('foo')).toBe(false);
542 + },
543 + );
544 +
545 + itRenders(
546 + 'unknown attributes for custom elements using is',
547 + async render => {
548 + const e = await render(<div is="custom-element" foo="bar" />);
549 + expect(e.getAttribute('foo')).toBe('bar');
550 + },
551 + );
552 +
553 + itRenders(
554 + 'no unknown attributes for custom elements using is with null value',
555 + async render => {
556 + const e = await render(<div is="custom-element" foo={null} />);
557 + expect(e.hasAttribute('foo')).toBe(false);
558 + },
559 + );
560 +
561 + itRenders('SVG tags with dashes in them', async render => {
562 + const e = await render(
563 + <svg>
564 + <font-face accentHeight={10} />
565 + </svg>,
566 + );
567 + expect(e.firstChild.hasAttribute('accentHeight')).toBe(false);
568 + expect(e.firstChild.getAttribute('accent-height')).toBe('10');
569 + });
570 +
571 + itRenders('cased custom attributes', async render => {
572 + const e = await render(<div fooBar="test" />, 1);
573 + expect(e.getAttribute('foobar')).toBe('test');
574 + });
575 + });
576 +
577 + itRenders('no HTML events', async render => {
578 + const e = await render(<div onClick={() => {}} />);
579 + expect(e.getAttribute('onClick')).toBe(null);
580 + expect(e.getAttribute('onClick')).toBe(null);
581 + expect(e.getAttribute('click')).toBe(null);
582 + });
583 +
584 + itRenders('no unknown events', async render => {
585 + const e = await render(
586 + <div onunknownevent="alert(&quot;hack&quot;)" />,
587 + 1,
588 + );
589 + expect(e.getAttribute('onunknownevent')).toBe(null);
590 + });
591 +
592 + itRenders('custom attribute named `on`', async render => {
593 + const e = await render(<div on="tap:do-something" />);
594 + expect(e.getAttribute('on')).toEqual('tap:do-something');
595 + });
596 + });
597 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationBasic-test.js new
+152
@@ -0,0 +1,152 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +const TEXT_NODE_TYPE = 3;
15 +
16 +let React;
17 +let ReactDOM;
18 +let ReactDOMServer;
19 +
20 +function initModules() {
21 + // Reset warning cache.
22 + jest.resetModuleRegistry();
23 + React = require('react');
24 + ReactDOM = require('react-dom');
25 + ReactDOMServer = require('react-dom/server');
26 +
27 + // Make them available to the helpers.
28 + return {
29 + ReactDOM,
30 + ReactDOMServer,
31 + };
32 +}
33 +
34 +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
35 +
36 +describe('ReactDOMServerIntegration', () => {
37 + beforeEach(() => {
38 + resetModules();
39 + });
40 +
41 + describe('basic rendering', function() {
42 + itRenders('a blank div', async render => {
43 + const e = await render(<div />);
44 + expect(e.tagName).toBe('DIV');
45 + });
46 +
47 + itRenders('a self-closing tag', async render => {
48 + const e = await render(<br />);
49 + expect(e.tagName).toBe('BR');
50 + });
51 +
52 + itRenders('a self-closing tag as a child', async render => {
53 + const e = await render(
54 + <div>
55 + <br />
56 + </div>,
57 + );
58 + expect(e.childNodes.length).toBe(1);
59 + expect(e.firstChild.tagName).toBe('BR');
60 + });
61 +
62 + itRenders('a string', async render => {
63 + let e = await render('Hello');
64 + expect(e.nodeType).toBe(3);
65 + expect(e.nodeValue).toMatch('Hello');
66 + });
67 +
68 + itRenders('a number', async render => {
69 + let e = await render(42);
70 + expect(e.nodeType).toBe(3);
71 + expect(e.nodeValue).toMatch('42');
72 + });
73 +
74 + itRenders('an array with one child', async render => {
75 + let e = await render([<div key={1}>text1</div>]);
76 + let parent = e.parentNode;
77 + expect(parent.childNodes[0].tagName).toBe('DIV');
78 + });
79 +
80 + itRenders('an array with several children', async render => {
81 + let Header = props => {
82 + return <p>header</p>;
83 + };
84 + let Footer = props => {
85 + return [<h2 key={1}>footer</h2>, <h3 key={2}>about</h3>];
86 + };
87 + let e = await render([
88 + <div key={1}>text1</div>,
89 + <span key={2}>text2</span>,
90 + <Header key={3} />,
91 + <Footer key={4} />,
92 + ]);
93 + let parent = e.parentNode;
94 + expect(parent.childNodes[0].tagName).toBe('DIV');
95 + expect(parent.childNodes[1].tagName).toBe('SPAN');
96 + expect(parent.childNodes[2].tagName).toBe('P');
97 + expect(parent.childNodes[3].tagName).toBe('H2');
98 + expect(parent.childNodes[4].tagName).toBe('H3');
99 + });
100 +
101 + itRenders('a nested array', async render => {
102 + let e = await render([
103 + [<div key={1}>text1</div>],
104 + <span key={1}>text2</span>,
105 + [[[null, <p key={1} />], false]],
106 + ]);
107 + let parent = e.parentNode;
108 + expect(parent.childNodes[0].tagName).toBe('DIV');
109 + expect(parent.childNodes[1].tagName).toBe('SPAN');
110 + expect(parent.childNodes[2].tagName).toBe('P');
111 + });
112 +
113 + itRenders('an iterable', async render => {
114 + const threeDivIterable = {
115 + '@@iterator': function() {
116 + var i = 0;
117 + return {
118 + next: function() {
119 + if (i++ < 3) {
120 + return {value: <div key={i} />, done: false};
121 + } else {
122 + return {value: undefined, done: true};
123 + }
124 + },
125 + };
126 + },
127 + };
128 + let e = await render(threeDivIterable);
129 + let parent = e.parentNode;
130 + expect(parent.childNodes.length).toBe(3);
131 + expect(parent.childNodes[0].tagName).toBe('DIV');
132 + expect(parent.childNodes[1].tagName).toBe('DIV');
133 + expect(parent.childNodes[2].tagName).toBe('DIV');
134 + });
135 +
136 + itRenders('emptyish values', async render => {
137 + let e = await render(0);
138 + expect(e.nodeType).toBe(TEXT_NODE_TYPE);
139 + expect(e.nodeValue).toMatch('0');
140 +
141 + // Empty string is special because client renders a node
142 + // but server returns empty HTML. So we compare parent text.
143 + expect((await render(<div>{''}</div>)).textContent).toBe('');
144 +
145 + expect(await render([])).toBe(null);
146 + expect(await render(false)).toBe(null);
147 + expect(await render(true)).toBe(null);
148 + expect(await render(undefined)).toBe(null);
149 + expect(await render([[[false]], undefined])).toBe(null);
150 + });
151 + });
152 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationContext-test.js new
+292
@@ -0,0 +1,292 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +let PropTypes;
15 +let React;
16 +let ReactDOM;
17 +let ReactDOMServer;
18 +
19 +function initModules() {
20 + // Reset warning cache.
21 + jest.resetModuleRegistry();
22 + PropTypes = require('prop-types');
23 + React = require('react');
24 + ReactDOM = require('react-dom');
25 + ReactDOMServer = require('react-dom/server');
26 +
27 + // Make them available to the helpers.
28 + return {
29 + ReactDOM,
30 + ReactDOMServer,
31 + };
32 +}
33 +
34 +const {
35 + resetModules,
36 + itRenders,
37 + itThrowsWhenRendering,
38 +} = ReactDOMServerIntegrationUtils(initModules);
39 +
40 +describe('ReactDOMServerIntegration', () => {
41 + beforeEach(() => {
42 + resetModules();
43 + });
44 +
45 + describe('context', function() {
46 + let PurpleContext, RedContext;
47 + beforeEach(() => {
48 + class Parent extends React.Component {
49 + getChildContext() {
50 + return {text: this.props.text};
51 + }
52 + render() {
53 + return this.props.children;
54 + }
55 + }
56 + Parent.childContextTypes = {text: PropTypes.string};
57 +
58 + PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
59 + RedContext = props => <Parent text="red">{props.children}</Parent>;
60 + });
61 +
62 + itRenders('class child with context', async render => {
63 + class ClassChildWithContext extends React.Component {
64 + render() {
65 + return <div>{this.context.text}</div>;
66 + }
67 + }
68 + ClassChildWithContext.contextTypes = {text: PropTypes.string};
69 +
70 + const e = await render(
71 + <PurpleContext>
72 + <ClassChildWithContext />
73 + </PurpleContext>,
74 + );
75 + expect(e.textContent).toBe('purple');
76 + });
77 +
78 + itRenders('stateless child with context', async render => {
79 + function StatelessChildWithContext(props, context) {
80 + return <div>{context.text}</div>;
81 + }
82 + StatelessChildWithContext.contextTypes = {text: PropTypes.string};
83 +
84 + const e = await render(
85 + <PurpleContext>
86 + <StatelessChildWithContext />
87 + </PurpleContext>,
88 + );
89 + expect(e.textContent).toBe('purple');
90 + });
91 +
92 + itRenders('class child without context', async render => {
93 + class ClassChildWithoutContext extends React.Component {
94 + render() {
95 + // this should render blank; context isn't passed to this component.
96 + return <div>{this.context.text}</div>;
97 + }
98 + }
99 +
100 + const e = await render(
101 + <PurpleContext>
102 + <ClassChildWithoutContext />
103 + </PurpleContext>,
104 + );
105 + expect(e.textContent).toBe('');
106 + });
107 +
108 + itRenders('stateless child without context', async render => {
109 + function StatelessChildWithoutContext(props, context) {
110 + // this should render blank; context isn't passed to this component.
111 + return <div>{context.text}</div>;
112 + }
113 +
114 + const e = await render(
115 + <PurpleContext>
116 + <StatelessChildWithoutContext />
117 + </PurpleContext>,
118 + );
119 + expect(e.textContent).toBe('');
120 + });
121 +
122 + itRenders('class child with wrong context', async render => {
123 + class ClassChildWithWrongContext extends React.Component {
124 + render() {
125 + // this should render blank; context.text isn't passed to this component.
126 + return <div id="classWrongChild">{this.context.text}</div>;
127 + }
128 + }
129 + ClassChildWithWrongContext.contextTypes = {foo: PropTypes.string};
130 +
131 + const e = await render(
132 + <PurpleContext>
133 + <ClassChildWithWrongContext />
134 + </PurpleContext>,
135 + );
136 + expect(e.textContent).toBe('');
137 + });
138 +
139 + itRenders('stateless child with wrong context', async render => {
140 + function StatelessChildWithWrongContext(props, context) {
141 + // this should render blank; context.text isn't passed to this component.
142 + return <div id="statelessWrongChild">{context.text}</div>;
143 + }
144 + StatelessChildWithWrongContext.contextTypes = {
145 + foo: PropTypes.string,
146 + };
147 +
148 + const e = await render(
149 + <PurpleContext>
150 + <StatelessChildWithWrongContext />
151 + </PurpleContext>,
152 + );
153 + expect(e.textContent).toBe('');
154 + });
155 +
156 + itRenders('with context passed through to a grandchild', async render => {
157 + function Grandchild(props, context) {
158 + return <div>{context.text}</div>;
159 + }
160 + Grandchild.contextTypes = {text: PropTypes.string};
161 +
162 + const Child = props => <Grandchild />;
163 +
164 + const e = await render(
165 + <PurpleContext>
166 + <Child />
167 + </PurpleContext>,
168 + );
169 + expect(e.textContent).toBe('purple');
170 + });
171 +
172 + itRenders('a child context overriding a parent context', async render => {
173 + const Grandchild = (props, context) => {
174 + return <div>{context.text}</div>;
175 + };
176 + Grandchild.contextTypes = {text: PropTypes.string};
177 +
178 + const e = await render(
179 + <PurpleContext>
180 + <RedContext>
181 + <Grandchild />
182 + </RedContext>
183 + </PurpleContext>,
184 + );
185 + expect(e.textContent).toBe('red');
186 + });
187 +
188 + itRenders('a child context merged with a parent context', async render => {
189 + class Parent extends React.Component {
190 + getChildContext() {
191 + return {text1: 'purple'};
192 + }
193 + render() {
194 + return <Child />;
195 + }
196 + }
197 + Parent.childContextTypes = {text1: PropTypes.string};
198 +
199 + class Child extends React.Component {
200 + getChildContext() {
201 + return {text2: 'red'};
202 + }
203 + render() {
204 + return <Grandchild />;
205 + }
206 + }
207 + Child.childContextTypes = {text2: PropTypes.string};
208 +
209 + const Grandchild = (props, context) => {
210 + return (
211 + <div>
212 + <div id="first">{context.text1}</div>
213 + <div id="second">{context.text2}</div>
214 + </div>
215 + );
216 + };
217 + Grandchild.contextTypes = {
218 + text1: PropTypes.string,
219 + text2: PropTypes.string,
220 + };
221 +
222 + const e = await render(<Parent />);
223 + expect(e.querySelector('#first').textContent).toBe('purple');
224 + expect(e.querySelector('#second').textContent).toBe('red');
225 + });
226 +
227 + itRenders(
228 + 'with a call to componentWillMount before getChildContext',
229 + async render => {
230 + class WillMountContext extends React.Component {
231 + getChildContext() {
232 + return {text: this.state.text};
233 + }
234 + componentWillMount() {
235 + this.setState({text: 'foo'});
236 + }
237 + render() {
238 + return <Child />;
239 + }
240 + }
241 + WillMountContext.childContextTypes = {text: PropTypes.string};
242 +
243 + const Child = (props, context) => {
244 + return <div>{context.text}</div>;
245 + };
246 + Child.contextTypes = {text: PropTypes.string};
247 +
248 + const e = await render(<WillMountContext />);
249 + expect(e.textContent).toBe('foo');
250 + },
251 + );
252 +
253 + itRenders(
254 + 'if getChildContext exists but childContextTypes is missing with a warning',
255 + async render => {
256 + function HopefulChild(props, context) {
257 + return context.foo || 'nope';
258 + }
259 + HopefulChild.contextTypes = {
260 + foo: PropTypes.string,
261 + };
262 + class ForgetfulParent extends React.Component {
263 + render() {
264 + return <HopefulChild />;
265 + }
266 + getChildContext() {
267 + return {foo: 'bar'};
268 + }
269 + }
270 + const e = await render(<ForgetfulParent />, 1);
271 + expect(e.textContent).toBe('nope');
272 + },
273 + );
274 +
275 + itThrowsWhenRendering(
276 + 'if getChildContext returns a value not in childContextTypes',
277 + render => {
278 + class MyComponent extends React.Component {
279 + render() {
280 + return <div />;
281 + }
282 + getChildContext() {
283 + return {value1: 'foo', value2: 'bar'};
284 + }
285 + }
286 + MyComponent.childContextTypes = {value1: PropTypes.string};
287 + return render(<MyComponent />);
288 + },
289 + 'MyComponent.getChildContext(): key "value2" is not defined in childContextTypes.',
290 + );
291 + });
292 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationElements-test.js new
+865
@@ -0,0 +1,865 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +const TEXT_NODE_TYPE = 3;
15 +
16 +let React;
17 +let ReactDOM;
18 +let ReactDOMServer;
19 +
20 +function initModules() {
21 + jest.resetModuleRegistry();
22 + React = require('react');
23 + ReactDOM = require('react-dom');
24 + ReactDOMServer = require('react-dom/server');
25 +
26 + // Make them available to the helpers.
27 + return {
28 + ReactDOM,
29 + ReactDOMServer,
30 + };
31 +}
32 +
33 +const {
34 + resetModules,
35 + itRenders,
36 + itThrowsWhenRendering,
37 + serverRender,
38 + streamRender,
39 + clientRenderOnServerString,
40 +} = ReactDOMServerIntegrationUtils(initModules);
41 +
42 +describe('ReactDOMServerIntegration', () => {
43 + beforeEach(() => {
44 + resetModules();
45 + });
46 +
47 + describe('elements and children', function() {
48 + function expectNode(node, type, value) {
49 + expect(node).not.toBe(null);
50 + expect(node.nodeType).toBe(type);
51 + expect(node.nodeValue).toMatch(value);
52 + }
53 +
54 + function expectTextNode(node, text) {
55 + expectNode(node, TEXT_NODE_TYPE, text);
56 + }
57 +
58 + describe('text children', function() {
59 + itRenders('a div with text', async render => {
60 + const e = await render(<div>Text</div>);
61 + expect(e.tagName).toBe('DIV');
62 + expect(e.childNodes.length).toBe(1);
63 + expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
64 + });
65 +
66 + itRenders('a div with text with flanking whitespace', async render => {
67 + // prettier-ignore
68 + const e = await render(<div> Text </div>);
69 + expect(e.childNodes.length).toBe(1);
70 + expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text ');
71 + });
72 +
73 + itRenders('a div with an empty text child', async render => {
74 + const e = await render(<div>{''}</div>);
75 + expect(e.childNodes.length).toBe(0);
76 + });
77 +
78 + itRenders('a div with multiple empty text children', async render => {
79 + const e = await render(
80 + <div>
81 + {''}
82 + {''}
83 + {''}
84 + </div>,
85 + );
86 + if (render === serverRender || render === streamRender) {
87 + // For plain server markup result we should have no text nodes if
88 + // they're all empty.
89 + expect(e.childNodes.length).toBe(0);
90 + expect(e.textContent).toBe('');
91 + } else {
92 + expect(e.childNodes.length).toBe(3);
93 + expectTextNode(e.childNodes[0], '');
94 + expectTextNode(e.childNodes[1], '');
95 + expectTextNode(e.childNodes[2], '');
96 + }
97 + });
98 +
99 + itRenders('a div with multiple whitespace children', async render => {
100 + // prettier-ignore
101 + const e = await render(<div>{' '}{' '}{' '}</div>);
102 + if (
103 + render === serverRender ||
104 + render === clientRenderOnServerString ||
105 + render === streamRender
106 + ) {
107 + // For plain server markup result we have comments between.
108 + // If we're able to hydrate, they remain.
109 + expect(e.childNodes.length).toBe(5);
110 + expectTextNode(e.childNodes[0], ' ');
111 + expectTextNode(e.childNodes[2], ' ');
112 + expectTextNode(e.childNodes[4], ' ');
113 + } else {
114 + expect(e.childNodes.length).toBe(3);
115 + expectTextNode(e.childNodes[0], ' ');
116 + expectTextNode(e.childNodes[1], ' ');
117 + expectTextNode(e.childNodes[2], ' ');
118 + }
119 + });
120 +
121 + itRenders('a div with text sibling to a node', async render => {
122 + const e = await render(
123 + <div>
124 + Text<span>More Text</span>
125 + </div>,
126 + );
127 + let spanNode;
128 + expect(e.childNodes.length).toBe(2);
129 + spanNode = e.childNodes[1];
130 + expectTextNode(e.childNodes[0], 'Text');
131 + expect(spanNode.tagName).toBe('SPAN');
132 + expect(spanNode.childNodes.length).toBe(1);
133 + expectNode(spanNode.firstChild, TEXT_NODE_TYPE, 'More Text');
134 + });
135 +
136 + itRenders('a non-standard element with text', async render => {
137 + const e = await render(<nonstandard>Text</nonstandard>);
138 + expect(e.tagName).toBe('NONSTANDARD');
139 + expect(e.childNodes.length).toBe(1);
140 + expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
141 + });
142 +
143 + itRenders('a custom element with text', async render => {
144 + const e = await render(<custom-element>Text</custom-element>);
145 + expect(e.tagName).toBe('CUSTOM-ELEMENT');
146 + expect(e.childNodes.length).toBe(1);
147 + expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
148 + });
149 +
150 + itRenders('a leading blank child with a text sibling', async render => {
151 + const e = await render(<div>{''}foo</div>);
152 + if (render === serverRender || render === streamRender) {
153 + expect(e.childNodes.length).toBe(1);
154 + expectTextNode(e.childNodes[0], 'foo');
155 + } else {
156 + expect(e.childNodes.length).toBe(2);
157 + expectTextNode(e.childNodes[0], '');
158 + expectTextNode(e.childNodes[1], 'foo');
159 + }
160 + });
161 +
162 + itRenders('a trailing blank child with a text sibling', async render => {
163 + const e = await render(<div>foo{''}</div>);
164 + // with Fiber, there are just two text nodes.
165 + if (render === serverRender || render === streamRender) {
166 + expect(e.childNodes.length).toBe(1);
167 + expectTextNode(e.childNodes[0], 'foo');
168 + } else {
169 + expect(e.childNodes.length).toBe(2);
170 + expectTextNode(e.childNodes[0], 'foo');
171 + expectTextNode(e.childNodes[1], '');
172 + }
173 + });
174 +
175 + itRenders('an element with two text children', async render => {
176 + const e = await render(
177 + <div>
178 + {'foo'}
179 + {'bar'}
180 + </div>,
181 + );
182 + if (
183 + render === serverRender ||
184 + render === clientRenderOnServerString ||
185 + render === streamRender
186 + ) {
187 + // In the server render output there's a comment between them.
188 + expect(e.childNodes.length).toBe(3);
189 + expectTextNode(e.childNodes[0], 'foo');
190 + expectTextNode(e.childNodes[2], 'bar');
191 + } else {
192 + expect(e.childNodes.length).toBe(2);
193 + expectTextNode(e.childNodes[0], 'foo');
194 + expectTextNode(e.childNodes[1], 'bar');
195 + }
196 + });
197 +
198 + itRenders(
199 + 'a component returning text node between two text nodes',
200 + async render => {
201 + const B = () => 'b';
202 + const e = await render(
203 + <div>
204 + {'a'}
205 + <B />
206 + {'c'}
207 + </div>,
208 + );
209 + if (
210 + render === serverRender ||
211 + render === clientRenderOnServerString ||
212 + render === streamRender
213 + ) {
214 + // In the server render output there's a comment between them.
215 + expect(e.childNodes.length).toBe(5);
216 + expectTextNode(e.childNodes[0], 'a');
217 + expectTextNode(e.childNodes[2], 'b');
218 + expectTextNode(e.childNodes[4], 'c');
219 + } else {
220 + expect(e.childNodes.length).toBe(3);
221 + expectTextNode(e.childNodes[0], 'a');
222 + expectTextNode(e.childNodes[1], 'b');
223 + expectTextNode(e.childNodes[2], 'c');
224 + }
225 + },
226 + );
227 +
228 + itRenders('a tree with sibling host and text nodes', async render => {
229 + class X extends React.Component {
230 + render() {
231 + return [null, [<Y key="1" />], false];
232 + }
233 + }
234 +
235 + function Y() {
236 + return [<Z key="1" />, ['c']];
237 + }
238 +
239 + function Z() {
240 + return null;
241 + }
242 +
243 + const e = await render(
244 + <div>
245 + {[['a'], 'b']}
246 + <div>
247 + <X key="1" />
248 + d
249 + </div>
250 + e
251 + </div>,
252 + );
253 + if (
254 + render === serverRender ||
255 + render === clientRenderOnServerString ||
256 + render === streamRender
257 + ) {
258 + // In the server render output there's comments between text nodes.
259 + expect(e.childNodes.length).toBe(5);
260 + expectTextNode(e.childNodes[0], 'a');
261 + expectTextNode(e.childNodes[2], 'b');
262 + expect(e.childNodes[3].childNodes.length).toBe(3);
263 + expectTextNode(e.childNodes[3].childNodes[0], 'c');
264 + expectTextNode(e.childNodes[3].childNodes[2], 'd');
265 + expectTextNode(e.childNodes[4], 'e');
266 + } else {
267 + expect(e.childNodes.length).toBe(4);
268 + expectTextNode(e.childNodes[0], 'a');
269 + expectTextNode(e.childNodes[1], 'b');
270 + expect(e.childNodes[2].childNodes.length).toBe(2);
271 + expectTextNode(e.childNodes[2].childNodes[0], 'c');
272 + expectTextNode(e.childNodes[2].childNodes[1], 'd');
273 + expectTextNode(e.childNodes[3], 'e');
274 + }
275 + });
276 + });
277 +
278 + describe('number children', function() {
279 + itRenders('a number as single child', async render => {
280 + const e = await render(<div>{3}</div>);
281 + expect(e.textContent).toBe('3');
282 + });
283 +
284 + // zero is falsey, so it could look like no children if the code isn't careful.
285 + itRenders('zero as single child', async render => {
286 + const e = await render(<div>{0}</div>);
287 + expect(e.textContent).toBe('0');
288 + });
289 +
290 + itRenders('an element with number and text children', async render => {
291 + const e = await render(
292 + <div>
293 + {'foo'}
294 + {40}
295 + </div>,
296 + );
297 + // with Fiber, there are just two text nodes.
298 + if (
299 + render === serverRender ||
300 + render === clientRenderOnServerString ||
301 + render === streamRender
302 + ) {
303 + // In the server markup there's a comment between.
304 + expect(e.childNodes.length).toBe(3);
305 + expectTextNode(e.childNodes[0], 'foo');
306 + expectTextNode(e.childNodes[2], '40');
307 + } else {
308 + expect(e.childNodes.length).toBe(2);
309 + expectTextNode(e.childNodes[0], 'foo');
310 + expectTextNode(e.childNodes[1], '40');
311 + }
312 + });
313 + });
314 +
315 + describe('null, false, and undefined children', function() {
316 + itRenders('null single child as blank', async render => {
317 + const e = await render(<div>{null}</div>);
318 + expect(e.childNodes.length).toBe(0);
319 + });
320 +
321 + itRenders('false single child as blank', async render => {
322 + const e = await render(<div>{false}</div>);
323 + expect(e.childNodes.length).toBe(0);
324 + });
325 +
326 + itRenders('undefined single child as blank', async render => {
327 + const e = await render(<div>{undefined}</div>);
328 + expect(e.childNodes.length).toBe(0);
329 + });
330 +
331 + itRenders('a null component children as empty', async render => {
332 + const NullComponent = () => null;
333 + const e = await render(
334 + <div>
335 + <NullComponent />
336 + </div>,
337 + );
338 + expect(e.childNodes.length).toBe(0);
339 + });
340 +
341 + itRenders('null children as blank', async render => {
342 + const e = await render(<div>{null}foo</div>);
343 + expect(e.childNodes.length).toBe(1);
344 + expectTextNode(e.childNodes[0], 'foo');
345 + });
346 +
347 + itRenders('false children as blank', async render => {
348 + const e = await render(<div>{false}foo</div>);
349 + expect(e.childNodes.length).toBe(1);
350 + expectTextNode(e.childNodes[0], 'foo');
351 + });
352 +
353 + itRenders('null and false children together as blank', async render => {
354 + const e = await render(
355 + <div>
356 + {false}
357 + {null}foo{null}
358 + {false}
359 + </div>,
360 + );
361 + expect(e.childNodes.length).toBe(1);
362 + expectTextNode(e.childNodes[0], 'foo');
363 + });
364 +
365 + itRenders('only null and false children as blank', async render => {
366 + const e = await render(
367 + <div>
368 + {false}
369 + {null}
370 + {null}
371 + {false}
372 + </div>,
373 + );
374 + expect(e.childNodes.length).toBe(0);
375 + });
376 + });
377 +
378 + describe('elements with implicit namespaces', function() {
379 + itRenders('an svg element', async render => {
380 + const e = await render(<svg />);
381 + expect(e.childNodes.length).toBe(0);
382 + expect(e.tagName).toBe('svg');
383 + expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
384 + });
385 +
386 + itRenders('svg child element with an attribute', async render => {
387 + let e = await render(<svg viewBox="0 0 0 0" />);
388 + expect(e.childNodes.length).toBe(0);
389 + expect(e.tagName).toBe('svg');
390 + expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
391 + expect(e.getAttribute('viewBox')).toBe('0 0 0 0');
392 + });
393 +
394 + itRenders(
395 + 'svg child element with a namespace attribute',
396 + async render => {
397 + let e = await render(
398 + <svg>
399 + <image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
400 + </svg>,
401 + );
402 + e = e.firstChild;
403 + expect(e.childNodes.length).toBe(0);
404 + expect(e.tagName).toBe('image');
405 + expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
406 + expect(e.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
407 + 'http://i.imgur.com/w7GCRPb.png',
408 + );
409 + },
410 + );
411 +
412 + itRenders('svg child element with a badly cased alias', async render => {
413 + let e = await render(
414 + <svg>
415 + <image xlinkhref="http://i.imgur.com/w7GCRPb.png" />
416 + </svg>,
417 + 1,
418 + );
419 + e = e.firstChild;
420 + expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
421 + false,
422 + );
423 + expect(e.getAttribute('xlinkhref')).toBe(
424 + 'http://i.imgur.com/w7GCRPb.png',
425 + );
426 + });
427 +
428 + itRenders('svg element with a tabIndex attribute', async render => {
429 + let e = await render(<svg tabIndex="1" />);
430 + expect(e.tabIndex).toBe(1);
431 + });
432 +
433 + itRenders(
434 + 'svg element with a badly cased tabIndex attribute',
435 + async render => {
436 + let e = await render(<svg tabindex="1" />, 1);
437 + expect(e.tabIndex).toBe(1);
438 + },
439 + );
440 +
441 + itRenders('svg element with a mixed case name', async render => {
442 + let e = await render(
443 + <svg>
444 + <filter>
445 + <feMorphology />
446 + </filter>
447 + </svg>,
448 + );
449 + e = e.firstChild.firstChild;
450 + expect(e.childNodes.length).toBe(0);
451 + expect(e.tagName).toBe('feMorphology');
452 + expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
453 + });
454 +
455 + itRenders('a math element', async render => {
456 + const e = await render(<math />);
457 + expect(e.childNodes.length).toBe(0);
458 + expect(e.tagName).toBe('math');
459 + expect(e.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
460 + });
461 + });
462 + // specially wrapped components
463 + // (see the big switch near the beginning ofReactDOMComponent.mountComponent)
464 + itRenders('an img', async render => {
465 + const e = await render(<img />);
466 + expect(e.childNodes.length).toBe(0);
467 + expect(e.nextSibling).toBe(null);
468 + expect(e.tagName).toBe('IMG');
469 + });
470 +
471 + itRenders('a button', async render => {
472 + const e = await render(<button />);
473 + expect(e.childNodes.length).toBe(0);
474 + expect(e.nextSibling).toBe(null);
475 + expect(e.tagName).toBe('BUTTON');
476 + });
477 +
478 + itRenders('a div with dangerouslySetInnerHTML', async render => {
479 + const e = await render(
480 + <div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
481 + );
482 + expect(e.childNodes.length).toBe(1);
483 + expect(e.firstChild.tagName).toBe('SPAN');
484 + expect(e.firstChild.getAttribute('id')).toBe('child');
485 + expect(e.firstChild.childNodes.length).toBe(0);
486 + });
487 +
488 + describe('newline-eating elements', function() {
489 + itRenders(
490 + 'a newline-eating tag with content not starting with \\n',
491 + async render => {
492 + const e = await render(<pre>Hello</pre>);
493 + expect(e.textContent).toBe('Hello');
494 + },
495 + );
496 + itRenders(
497 + 'a newline-eating tag with content starting with \\n',
498 + async render => {
499 + const e = await render(<pre>{'\nHello'}</pre>);
500 + expect(e.textContent).toBe('\nHello');
501 + },
502 + );
503 + itRenders('a normal tag with content starting with \\n', async render => {
504 + const e = await render(<div>{'\nHello'}</div>);
505 + expect(e.textContent).toBe('\nHello');
506 + });
507 + });
508 +
509 + describe('different component implementations', function() {
510 + function checkFooDiv(e) {
511 + expect(e.childNodes.length).toBe(1);
512 + expectNode(e.firstChild, TEXT_NODE_TYPE, 'foo');
513 + }
514 +
515 + itRenders('stateless components', async render => {
516 + const StatelessComponent = () => <div>foo</div>;
517 + checkFooDiv(await render(<StatelessComponent />));
518 + });
519 +
520 + itRenders('ES6 class components', async render => {
521 + class ClassComponent extends React.Component {
522 + render() {
523 + return <div>foo</div>;
524 + }
525 + }
526 + checkFooDiv(await render(<ClassComponent />));
527 + });
528 +
529 + itRenders('factory components', async render => {
530 + const FactoryComponent = () => {
531 + return {
532 + render: function() {
533 + return <div>foo</div>;
534 + },
535 + };
536 + };
537 + checkFooDiv(await render(<FactoryComponent />));
538 + });
539 + });
540 +
541 + describe('component hierarchies', async function() {
542 + itRenders('single child hierarchies of components', async render => {
543 + const Component = props => <div>{props.children}</div>;
544 + let e = await render(
545 + <Component>
546 + <Component>
547 + <Component>
548 + <Component />
549 + </Component>
550 + </Component>
551 + </Component>,
552 + );
553 + for (var i = 0; i < 3; i++) {
554 + expect(e.tagName).toBe('DIV');
555 + expect(e.childNodes.length).toBe(1);
556 + e = e.firstChild;
557 + }
558 + expect(e.tagName).toBe('DIV');
559 + expect(e.childNodes.length).toBe(0);
560 + });
561 +
562 + itRenders('multi-child hierarchies of components', async render => {
563 + const Component = props => <div>{props.children}</div>;
564 + const e = await render(
565 + <Component>
566 + <Component>
567 + <Component />
568 + <Component />
569 + </Component>
570 + <Component>
571 + <Component />
572 + <Component />
573 + </Component>
574 + </Component>,
575 + );
576 + expect(e.tagName).toBe('DIV');
577 + expect(e.childNodes.length).toBe(2);
578 + for (var i = 0; i < 2; i++) {
579 + var child = e.childNodes[i];
580 + expect(child.tagName).toBe('DIV');
581 + expect(child.childNodes.length).toBe(2);
582 + for (var j = 0; j < 2; j++) {
583 + var grandchild = child.childNodes[j];
584 + expect(grandchild.tagName).toBe('DIV');
585 + expect(grandchild.childNodes.length).toBe(0);
586 + }
587 + }
588 + });
589 +
590 + itRenders('a div with a child', async render => {
591 + const e = await render(
592 + <div id="parent">
593 + <div id="child" />
594 + </div>,
595 + );
596 + expect(e.id).toBe('parent');
597 + expect(e.childNodes.length).toBe(1);
598 + expect(e.childNodes[0].id).toBe('child');
599 + expect(e.childNodes[0].childNodes.length).toBe(0);
600 + });
601 +
602 + itRenders('a div with multiple children', async render => {
603 + const e = await render(
604 + <div id="parent">
605 + <div id="child1" />
606 + <div id="child2" />
607 + </div>,
608 + );
609 + expect(e.id).toBe('parent');
610 + expect(e.childNodes.length).toBe(2);
611 + expect(e.childNodes[0].id).toBe('child1');
612 + expect(e.childNodes[0].childNodes.length).toBe(0);
613 + expect(e.childNodes[1].id).toBe('child2');
614 + expect(e.childNodes[1].childNodes.length).toBe(0);
615 + });
616 +
617 + itRenders(
618 + 'a div with multiple children separated by whitespace',
619 + async render => {
620 + const e = await render(
621 + <div id="parent">
622 + <div id="child1" /> <div id="child2" />
623 + </div>,
624 + );
625 + expect(e.id).toBe('parent');
626 + let child1, child2, textNode;
627 + expect(e.childNodes.length).toBe(3);
628 + child1 = e.childNodes[0];
629 + textNode = e.childNodes[1];
630 + child2 = e.childNodes[2];
631 + expect(child1.id).toBe('child1');
632 + expect(child1.childNodes.length).toBe(0);
633 + expectTextNode(textNode, ' ');
634 + expect(child2.id).toBe('child2');
635 + expect(child2.childNodes.length).toBe(0);
636 + },
637 + );
638 +
639 + itRenders(
640 + 'a div with a single child surrounded by whitespace',
641 + async render => {
642 + // prettier-ignore
643 + const e = await render(<div id="parent"> <div id="child" /> </div>); // eslint-disable-line no-multi-spaces
644 + let textNode1, child, textNode2;
645 + expect(e.childNodes.length).toBe(3);
646 + textNode1 = e.childNodes[0];
647 + child = e.childNodes[1];
648 + textNode2 = e.childNodes[2];
649 + expect(e.id).toBe('parent');
650 + expectTextNode(textNode1, ' ');
651 + expect(child.id).toBe('child');
652 + expect(child.childNodes.length).toBe(0);
653 + expectTextNode(textNode2, ' ');
654 + },
655 + );
656 +
657 + itRenders('a composite with multiple children', async render => {
658 + const Component = props => props.children;
659 + const e = await render(
660 + <Component>{['a', 'b', [undefined], [[false, 'c']]]}</Component>,
661 + );
662 +
663 + let parent = e.parentNode;
664 + if (
665 + render === serverRender ||
666 + render === clientRenderOnServerString ||
667 + render === streamRender
668 + ) {
669 + // For plain server markup result we have comments between.
670 + // If we're able to hydrate, they remain.
671 + expect(parent.childNodes.length).toBe(5);
672 + expectTextNode(parent.childNodes[0], 'a');
673 + expectTextNode(parent.childNodes[2], 'b');
674 + expectTextNode(parent.childNodes[4], 'c');
675 + } else {
676 + expect(parent.childNodes.length).toBe(3);
677 + expectTextNode(parent.childNodes[0], 'a');
678 + expectTextNode(parent.childNodes[1], 'b');
679 + expectTextNode(parent.childNodes[2], 'c');
680 + }
681 + });
682 + });
683 +
684 + describe('escaping >, <, and &', function() {
685 + itRenders('>,<, and & as single child', async render => {
686 + const e = await render(<div>{'<span>Text&quot;</span>'}</div>);
687 + expect(e.childNodes.length).toBe(1);
688 + expectNode(e.firstChild, TEXT_NODE_TYPE, '<span>Text&quot;</span>');
689 + });
690 +
691 + itRenders('>,<, and & as multiple children', async render => {
692 + const e = await render(
693 + <div>
694 + {'<span>Text1&quot;</span>'}
695 + {'<span>Text2&quot;</span>'}
696 + </div>,
697 + );
698 + if (
699 + render === serverRender ||
700 + render === clientRenderOnServerString ||
701 + render === streamRender
702 + ) {
703 + expect(e.childNodes.length).toBe(3);
704 + expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
705 + expectTextNode(e.childNodes[2], '<span>Text2&quot;</span>');
706 + } else {
707 + expect(e.childNodes.length).toBe(2);
708 + expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
709 + expectTextNode(e.childNodes[1], '<span>Text2&quot;</span>');
710 + }
711 + });
712 + });
713 +
714 + describe('carriage return and null character', () => {
715 + // HTML parsing normalizes CR and CRLF to LF.
716 + // It also ignores null character.
717 + // https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
718 + // If we have a mismatch, it might be caused by that (and should not be reported).
719 + // We won't be patching up in this case as that matches our past behavior.
720 +
721 + itRenders(
722 + 'an element with one text child with special characters',
723 + async render => {
724 + const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
725 + if (render === serverRender || render === streamRender) {
726 + expect(e.childNodes.length).toBe(1);
727 + // Everything becomes LF when parsed from server HTML.
728 + // Null character is ignored.
729 + expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
730 + } else {
731 + expect(e.childNodes.length).toBe(1);
732 + // Client rendering (or hydration) uses JS value with CR.
733 + // Null character stays.
734 + expectNode(
735 + e.childNodes[0],
736 + TEXT_NODE_TYPE,
737 + 'foo\rbar\r\nbaz\nqux\u0000',
738 + );
739 + }
740 + },
741 + );
742 +
743 + itRenders(
744 + 'an element with two text children with special characters',
745 + async render => {
746 + const e = await render(
747 + <div>
748 + {'foo\rbar'}
749 + {'\r\nbaz\nqux\u0000'}
750 + </div>,
751 + );
752 + if (render === serverRender || render === streamRender) {
753 + // We have three nodes because there is a comment between them.
754 + expect(e.childNodes.length).toBe(3);
755 + // Everything becomes LF when parsed from server HTML.
756 + // Null character is ignored.
757 + expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
758 + expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
759 + } else if (render === clientRenderOnServerString) {
760 + // We have three nodes because there is a comment between them.
761 + expect(e.childNodes.length).toBe(3);
762 + // Hydration uses JS value with CR and null character.
763 + expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
764 + expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
765 + } else {
766 + expect(e.childNodes.length).toBe(2);
767 + // Client rendering uses JS value with CR and null character.
768 + expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
769 + expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
770 + }
771 + },
772 + );
773 +
774 + itRenders(
775 + 'an element with an attribute value with special characters',
776 + async render => {
777 + const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
778 + if (
779 + render === serverRender ||
780 + render === streamRender ||
781 + render === clientRenderOnServerString
782 + ) {
783 + // Everything becomes LF when parsed from server HTML.
784 + // Null character in an attribute becomes the replacement character.
785 + // Hydration also ends up with LF because we don't patch up attributes.
786 + expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
787 + } else {
788 + // Client rendering uses JS value with CR and null character.
789 + expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
790 + }
791 + },
792 + );
793 + });
794 +
795 + describe('components that throw errors', function() {
796 + itThrowsWhenRendering(
797 + 'a function returning undefined',
798 + async render => {
799 + const UndefinedComponent = () => undefined;
800 + await render(<UndefinedComponent />, 1);
801 + },
802 + 'UndefinedComponent(...): Nothing was returned from render. ' +
803 + 'This usually means a return statement is missing. Or, to ' +
804 + 'render nothing, return null.',
805 + );
806 +
807 + itThrowsWhenRendering(
808 + 'a class returning undefined',
809 + async render => {
810 + class UndefinedComponent extends React.Component {
811 + render() {
812 + return undefined;
813 + }
814 + }
815 + await render(<UndefinedComponent />, 1);
816 + },
817 + 'UndefinedComponent(...): Nothing was returned from render. ' +
818 + 'This usually means a return statement is missing. Or, to ' +
819 + 'render nothing, return null.',
820 + );
821 +
822 + itThrowsWhenRendering(
823 + 'a function returning an object',
824 + async render => {
825 + const ObjectComponent = () => ({x: 123});
826 + await render(<ObjectComponent />, 1);
827 + },
828 + 'Objects are not valid as a React child (found: object with keys {x}).' +
829 + (__DEV__
830 + ? ' If you meant to render a collection of children, use ' +
831 + 'an array instead.'
832 + : ''),
833 + );
834 +
835 + itThrowsWhenRendering(
836 + 'a class returning an object',
837 + async render => {
838 + class ObjectComponent extends React.Component {
839 + render() {
840 + return {x: 123};
841 + }
842 + }
843 + await render(<ObjectComponent />, 1);
844 + },
845 + 'Objects are not valid as a React child (found: object with keys {x}).' +
846 + (__DEV__
847 + ? ' If you meant to render a collection of children, use ' +
848 + 'an array instead.'
849 + : ''),
850 + );
851 +
852 + itThrowsWhenRendering(
853 + 'top-level object',
854 + async render => {
855 + await render({x: 123});
856 + },
857 + 'Objects are not valid as a React child (found: object with keys {x}).' +
858 + (__DEV__
859 + ? ' If you meant to render a collection of children, use ' +
860 + 'an array instead.'
861 + : ''),
862 + );
863 + });
864 + });
865 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationForms-test.js new
+612
@@ -0,0 +1,612 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +let React;
15 +let ReactDOM;
16 +let ReactDOMServer;
17 +let ReactTestUtils;
18 +
19 +function initModules() {
20 + // Reset warning cache.
21 + jest.resetModuleRegistry();
22 + React = require('react');
23 + ReactDOM = require('react-dom');
24 + ReactTestUtils = require('react-dom/test-utils');
25 + ReactDOMServer = require('react-dom/server');
26 +
27 + // Make them available to the helpers.
28 + return {
29 + ReactDOM,
30 + ReactDOMServer,
31 + };
32 +}
33 +
34 +const {
35 + resetModules,
36 + itRenders,
37 + itClientRenders,
38 + renderIntoDom,
39 + serverRender,
40 +} = ReactDOMServerIntegrationUtils(initModules);
41 +
42 +describe('ReactDOMServerIntegration', () => {
43 + beforeEach(() => {
44 + resetModules();
45 + });
46 +
47 + describe('form controls', function() {
48 + describe('inputs', function() {
49 + itRenders('an input with a value and an onChange', async render => {
50 + const e = await render(<input value="foo" onChange={() => {}} />);
51 + expect(e.value).toBe('foo');
52 + });
53 +
54 + itRenders('an input with a value and readOnly', async render => {
55 + const e = await render(<input value="foo" readOnly={true} />);
56 + expect(e.value).toBe('foo');
57 + });
58 +
59 + itRenders(
60 + 'an input with a value and no onChange/readOnly',
61 + async render => {
62 + // this configuration should raise a dev warning that value without
63 + // onChange or readOnly is a mistake.
64 + const e = await render(<input value="foo" />, 1);
65 + expect(e.value).toBe('foo');
66 + expect(e.getAttribute('value')).toBe('foo');
67 + },
68 + );
69 +
70 + itRenders('an input with a defaultValue', async render => {
71 + const e = await render(<input defaultValue="foo" />);
72 + expect(e.value).toBe('foo');
73 + expect(e.getAttribute('value')).toBe('foo');
74 + expect(e.getAttribute('defaultValue')).toBe(null);
75 + });
76 +
77 + itRenders('an input value overriding defaultValue', async render => {
78 + const e = await render(
79 + <input value="foo" defaultValue="bar" readOnly={true} />,
80 + 1,
81 + );
82 + expect(e.value).toBe('foo');
83 + expect(e.getAttribute('value')).toBe('foo');
84 + expect(e.getAttribute('defaultValue')).toBe(null);
85 + });
86 +
87 + itRenders(
88 + 'an input value overriding defaultValue no matter the prop order',
89 + async render => {
90 + const e = await render(
91 + <input defaultValue="bar" value="foo" readOnly={true} />,
92 + 1,
93 + );
94 + expect(e.value).toBe('foo');
95 + expect(e.getAttribute('value')).toBe('foo');
96 + expect(e.getAttribute('defaultValue')).toBe(null);
97 + },
98 + );
99 + });
100 +
101 + describe('checkboxes', function() {
102 + itRenders('a checkbox that is checked with an onChange', async render => {
103 + const e = await render(
104 + <input type="checkbox" checked={true} onChange={() => {}} />,
105 + );
106 + expect(e.checked).toBe(true);
107 + });
108 +
109 + itRenders('a checkbox that is checked with readOnly', async render => {
110 + const e = await render(
111 + <input type="checkbox" checked={true} readOnly={true} />,
112 + );
113 + expect(e.checked).toBe(true);
114 + });
115 +
116 + itRenders(
117 + 'a checkbox that is checked and no onChange/readOnly',
118 + async render => {
119 + // this configuration should raise a dev warning that checked without
120 + // onChange or readOnly is a mistake.
121 + const e = await render(<input type="checkbox" checked={true} />, 1);
122 + expect(e.checked).toBe(true);
123 + },
124 + );
125 +
126 + itRenders('a checkbox with defaultChecked', async render => {
127 + const e = await render(<input type="checkbox" defaultChecked={true} />);
128 + expect(e.checked).toBe(true);
129 + expect(e.getAttribute('defaultChecked')).toBe(null);
130 + });
131 +
132 + itRenders(
133 + 'a checkbox checked overriding defaultChecked',
134 + async render => {
135 + const e = await render(
136 + <input
137 + type="checkbox"
138 + checked={true}
139 + defaultChecked={false}
140 + readOnly={true}
141 + />,
142 + 1,
143 + );
144 + expect(e.checked).toBe(true);
145 + expect(e.getAttribute('defaultChecked')).toBe(null);
146 + },
147 + );
148 +
149 + itRenders(
150 + 'a checkbox checked overriding defaultChecked no matter the prop order',
151 + async render => {
152 + const e = await render(
153 + <input
154 + type="checkbox"
155 + defaultChecked={false}
156 + checked={true}
157 + readOnly={true}
158 + />,
159 + 1,
160 + );
161 + expect(e.checked).toBe(true);
162 + expect(e.getAttribute('defaultChecked')).toBe(null);
163 + },
164 + );
165 + });
166 +
167 + describe('textareas', function() {
168 + // textareas
169 + // ---------
170 + itRenders('a textarea with a value and an onChange', async render => {
171 + const e = await render(<textarea value="foo" onChange={() => {}} />);
172 + // textarea DOM elements don't have a value **attribute**, the text is
173 + // a child of the element and accessible via the .value **property**.
174 + expect(e.getAttribute('value')).toBe(null);
175 + expect(e.value).toBe('foo');
176 + });
177 +
178 + itRenders('a textarea with a value and readOnly', async render => {
179 + const e = await render(<textarea value="foo" readOnly={true} />);
180 + // textarea DOM elements don't have a value **attribute**, the text is
181 + // a child of the element and accessible via the .value **property**.
182 + expect(e.getAttribute('value')).toBe(null);
183 + expect(e.value).toBe('foo');
184 + });
185 +
186 + itRenders(
187 + 'a textarea with a value and no onChange/readOnly',
188 + async render => {
189 + // this configuration should raise a dev warning that value without
190 + // onChange or readOnly is a mistake.
191 + const e = await render(<textarea value="foo" />, 1);
192 + expect(e.getAttribute('value')).toBe(null);
193 + expect(e.value).toBe('foo');
194 + },
195 + );
196 +
197 + itRenders('a textarea with a defaultValue', async render => {
198 + const e = await render(<textarea defaultValue="foo" />);
199 + expect(e.getAttribute('value')).toBe(null);
200 + expect(e.getAttribute('defaultValue')).toBe(null);
201 + expect(e.value).toBe('foo');
202 + });
203 +
204 + itRenders('a textarea value overriding defaultValue', async render => {
205 + const e = await render(
206 + <textarea value="foo" defaultValue="bar" readOnly={true} />,
207 + 1,
208 + );
209 + expect(e.getAttribute('value')).toBe(null);
210 + expect(e.getAttribute('defaultValue')).toBe(null);
211 + expect(e.value).toBe('foo');
212 + });
213 +
214 + itRenders(
215 + 'a textarea value overriding defaultValue no matter the prop order',
216 + async render => {
217 + const e = await render(
218 + <textarea defaultValue="bar" value="foo" readOnly={true} />,
219 + 1,
220 + );
221 + expect(e.getAttribute('value')).toBe(null);
222 + expect(e.getAttribute('defaultValue')).toBe(null);
223 + expect(e.value).toBe('foo');
224 + },
225 + );
226 + });
227 +
228 + describe('selects', function() {
229 + var options;
230 + beforeEach(function() {
231 + options = [
232 + <option key={1} value="foo" id="foo">
233 + Foo
234 + </option>,
235 + <option key={2} value="bar" id="bar">
236 + Bar
237 + </option>,
238 + <option key={3} value="baz" id="baz">
239 + Baz
240 + </option>,
241 + ];
242 + });
243 +
244 + // a helper function to test the selected value of a <select> element.
245 + // takes in a <select> DOM element (element) and a value or array of
246 + // values that should be selected (selected).
247 + const expectSelectValue = (element, selected) => {
248 + if (!Array.isArray(selected)) {
249 + selected = [selected];
250 + }
251 + // the select DOM element shouldn't ever have a value or defaultValue
252 + // attribute; that is not how select values are expressed in the DOM.
253 + expect(element.getAttribute('value')).toBe(null);
254 + expect(element.getAttribute('defaultValue')).toBe(null);
255 +
256 + ['foo', 'bar', 'baz'].forEach(value => {
257 + const expectedValue = selected.indexOf(value) !== -1;
258 + var option = element.querySelector(`#${value}`);
259 + expect(option.selected).toBe(expectedValue);
260 + });
261 + };
262 +
263 + itRenders('a select with a value and an onChange', async render => {
264 + const e = await render(
265 + <select value="bar" onChange={() => {}}>
266 + {options}
267 + </select>,
268 + );
269 + expectSelectValue(e, 'bar');
270 + });
271 +
272 + itRenders('a select with a value and readOnly', async render => {
273 + const e = await render(
274 + <select value="bar" readOnly={true}>
275 + {options}
276 + </select>,
277 + );
278 + expectSelectValue(e, 'bar');
279 + });
280 +
281 + itRenders(
282 + 'a select with a multiple values and an onChange',
283 + async render => {
284 + const e = await render(
285 + <select value={['bar', 'baz']} multiple={true} onChange={() => {}}>
286 + {options}
287 + </select>,
288 + );
289 + expectSelectValue(e, ['bar', 'baz']);
290 + },
291 + );
292 +
293 + itRenders(
294 + 'a select with a multiple values and readOnly',
295 + async render => {
296 + const e = await render(
297 + <select value={['bar', 'baz']} multiple={true} readOnly={true}>
298 + {options}
299 + </select>,
300 + );
301 + expectSelectValue(e, ['bar', 'baz']);
302 + },
303 + );
304 +
305 + itRenders(
306 + 'a select with a value and no onChange/readOnly',
307 + async render => {
308 + // this configuration should raise a dev warning that value without
309 + // onChange or readOnly is a mistake.
310 + const e = await render(<select value="bar">{options}</select>, 1);
311 + expectSelectValue(e, 'bar');
312 + },
313 + );
314 +
315 + itRenders('a select with a defaultValue', async render => {
316 + const e = await render(<select defaultValue="bar">{options}</select>);
317 + expectSelectValue(e, 'bar');
318 + });
319 +
320 + itRenders('a select value overriding defaultValue', async render => {
321 + const e = await render(
322 + <select value="bar" defaultValue="baz" readOnly={true}>
323 + {options}
324 + </select>,
325 + 1,
326 + );
327 + expectSelectValue(e, 'bar');
328 + });
329 +
330 + itRenders(
331 + 'a select value overriding defaultValue no matter the prop order',
332 + async render => {
333 + const e = await render(
334 + <select defaultValue="baz" value="bar" readOnly={true}>
335 + {options}
336 + </select>,
337 + 1,
338 + );
339 + expectSelectValue(e, 'bar');
340 + },
341 + );
342 + });
343 +
344 + describe('user interaction', function() {
345 + let ControlledInput,
346 + ControlledTextArea,
347 + ControlledCheckbox,
348 + ControlledSelect;
349 + beforeEach(() => {
350 + ControlledInput = class extends React.Component {
351 + constructor() {
352 + super();
353 + this.state = {value: 'Hello'};
354 + }
355 + handleChange(event) {
356 + if (this.props.onChange) {
357 + this.props.onChange(event);
358 + }
359 + this.setState({value: event.target.value});
360 + }
361 + render() {
362 + return (
363 + <input
364 + value={this.state.value}
365 + onChange={this.handleChange.bind(this)}
366 + />
367 + );
368 + }
369 + };
370 + ControlledTextArea = class extends React.Component {
371 + constructor() {
372 + super();
373 + this.state = {value: 'Hello'};
374 + }
375 + handleChange(event) {
376 + if (this.props.onChange) {
377 + this.props.onChange(event);
378 + }
379 + this.setState({value: event.target.value});
380 + }
381 + render() {
382 + return (
383 + <textarea
384 + value={this.state.value}
385 + onChange={this.handleChange.bind(this)}
386 + />
387 + );
388 + }
389 + };
390 + ControlledCheckbox = class extends React.Component {
391 + constructor() {
392 + super();
393 + this.state = {value: true};
394 + }
395 + handleChange(event) {
396 + if (this.props.onChange) {
397 + this.props.onChange(event);
398 + }
399 + this.setState({value: event.target.checked});
400 + }
401 + render() {
402 + return (
403 + <input
404 + type="checkbox"
405 + checked={this.state.value}
406 + onChange={this.handleChange.bind(this)}
407 + />
408 + );
409 + }
410 + };
411 + ControlledSelect = class extends React.Component {
412 + constructor() {
413 + super();
414 + this.state = {value: 'Hello'};
415 + }
416 + handleChange(event) {
417 + if (this.props.onChange) {
418 + this.props.onChange(event);
419 + }
420 + this.setState({value: event.target.value});
421 + }
422 + render() {
423 + return (
424 + <select
425 + value={this.state.value}
426 + onChange={this.handleChange.bind(this)}>
427 + <option key="1" value="Hello">
428 + Hello
429 + </option>
430 + <option key="2" value="Goodbye">
431 + Goodbye
432 + </option>
433 + </select>
434 + );
435 + }
436 + };
437 + });
438 +
439 + describe('user interaction with controlled inputs', function() {
440 + itClientRenders('a controlled text input', async render => {
441 + let changeCount = 0;
442 + const e = await render(
443 + <ControlledInput onChange={() => changeCount++} />,
444 + );
445 + expect(changeCount).toBe(0);
446 + expect(e.value).toBe('Hello');
447 +
448 + // simulate a user typing.
449 + e.value = 'Goodbye';
450 + ReactTestUtils.Simulate.change(e);
451 +
452 + expect(changeCount).toBe(1);
453 + expect(e.value).toBe('Goodbye');
454 + });
455 +
456 + itClientRenders('a controlled textarea', async render => {
457 + let changeCount = 0;
458 + const e = await render(
459 + <ControlledTextArea onChange={() => changeCount++} />,
460 + );
461 + expect(changeCount).toBe(0);
462 + expect(e.value).toBe('Hello');
463 +
464 + // simulate a user typing.
465 + e.value = 'Goodbye';
466 + ReactTestUtils.Simulate.change(e);
467 +
468 + expect(changeCount).toBe(1);
469 + expect(e.value).toBe('Goodbye');
470 + });
471 +
472 + itClientRenders('a controlled checkbox', async render => {
473 + let changeCount = 0;
474 + const e = await render(
475 + <ControlledCheckbox onChange={() => changeCount++} />,
476 + );
477 + expect(changeCount).toBe(0);
478 + expect(e.checked).toBe(true);
479 +
480 + // simulate a user typing.
481 + e.checked = false;
482 + ReactTestUtils.Simulate.change(e);
483 +
484 + expect(changeCount).toBe(1);
485 + expect(e.checked).toBe(false);
486 + });
487 +
488 + itClientRenders('a controlled select', async render => {
489 + let changeCount = 0;
490 + const e = await render(
491 + <ControlledSelect onChange={() => changeCount++} />,
492 + );
493 + expect(changeCount).toBe(0);
494 + expect(e.value).toBe('Hello');
495 +
496 + // simulate a user typing.
497 + e.value = 'Goodbye';
498 + ReactTestUtils.Simulate.change(e);
499 +
500 + expect(changeCount).toBe(1);
501 + expect(e.value).toBe('Goodbye');
502 + });
503 + });
504 +
505 + describe('user interaction with inputs before client render', function() {
506 + // renders the element and changes the value **before** the client
507 + // code has a chance to render; this simulates what happens when a
508 + // user starts to interact with a server-rendered form before
509 + // ReactDOM.render is called. the client render should NOT blow away
510 + // the changes the user has made.
511 + const testUserInteractionBeforeClientRender = async (
512 + element,
513 + initialValue = 'Hello',
514 + changedValue = 'Goodbye',
515 + valueKey = 'value',
516 + ) => {
517 + const field = await serverRender(element);
518 + expect(field[valueKey]).toBe(initialValue);
519 +
520 + // simulate a user typing in the field **before** client-side reconnect happens.
521 + field[valueKey] = changedValue;
522 +
523 + resetModules();
524 + // client render on top of the server markup.
525 + const clientField = await renderIntoDom(
526 + element,
527 + field.parentNode,
528 + true,
529 + );
530 + // verify that the input field was not replaced.
531 + // Note that we cannot use expect(clientField).toBe(field) because
532 + // of jest bug #1772
533 + expect(clientField === field).toBe(true);
534 + // confirm that the client render has not changed what the user typed.
535 + expect(clientField[valueKey]).toBe(changedValue);
536 + };
537 +
538 + it('should not blow away user-entered text on successful reconnect to an uncontrolled input', () =>
539 + testUserInteractionBeforeClientRender(
540 + <input defaultValue="Hello" />,
541 + ));
542 +
543 + it('should not blow away user-entered text on successful reconnect to a controlled input', async () => {
544 + let changeCount = 0;
545 + await testUserInteractionBeforeClientRender(
546 + <ControlledInput onChange={() => changeCount++} />,
547 + );
548 + // note that there's a strong argument to be made that the DOM revival
549 + // algorithm should notice that the user has changed the value and fire
550 + // an onChange. however, it does not now, so that's what this tests.
551 + expect(changeCount).toBe(0);
552 + });
553 +
554 + it('should not blow away user-entered text on successful reconnect to an uncontrolled checkbox', () =>
555 + testUserInteractionBeforeClientRender(
556 + <input type="checkbox" defaultChecked={true} />,
557 + true,
558 + false,
559 + 'checked',
560 + ));
561 +
562 + it('should not blow away user-entered text on successful reconnect to a controlled checkbox', async () => {
563 + let changeCount = 0;
564 + await testUserInteractionBeforeClientRender(
565 + <ControlledCheckbox onChange={() => changeCount++} />,
566 + true,
567 + false,
568 + 'checked',
569 + );
570 + expect(changeCount).toBe(0);
571 + });
572 +
573 + // skipping this test because React 15 does the wrong thing. it blows
574 + // away the user's typing in the textarea.
575 + xit('should not blow away user-entered text on successful reconnect to an uncontrolled textarea', () =>
576 + testUserInteractionBeforeClientRender(
577 + <textarea defaultValue="Hello" />,
578 + ));
579 +
580 + // skipping this test because React 15 does the wrong thing. it blows
581 + // away the user's typing in the textarea.
582 + xit('should not blow away user-entered text on successful reconnect to a controlled textarea', async () => {
583 + let changeCount = 0;
584 + await testUserInteractionBeforeClientRender(
585 + <ControlledTextArea onChange={() => changeCount++} />,
586 + );
587 + expect(changeCount).toBe(0);
588 + });
589 +
590 + it('should not blow away user-selected value on successful reconnect to an uncontrolled select', () =>
591 + testUserInteractionBeforeClientRender(
592 + <select defaultValue="Hello">
593 + <option key="1" value="Hello">
594 + Hello
595 + </option>
596 + <option key="2" value="Goodbye">
597 + Goodbye
598 + </option>
599 + </select>,
600 + ));
601 +
602 + it('should not blow away user-selected value on successful reconnect to an controlled select', async () => {
603 + let changeCount = 0;
604 + await testUserInteractionBeforeClientRender(
605 + <ControlledSelect onChange={() => changeCount++} />,
606 + );
607 + expect(changeCount).toBe(0);
608 + });
609 + });
610 + });
611 + });
612 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationFragment-test.internal.js new
+107
@@ -0,0 +1,107 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +let React;
15 +let ReactDOM;
16 +let ReactDOMServer;
17 +
18 +function initModules() {
19 + // Reset warning cache.
20 + jest.resetModuleRegistry();
21 + require('shared/ReactFeatureFlags').enableReactFragment = true;
22 + React = require('react');
23 + ReactDOM = require('react-dom');
24 + ReactDOMServer = require('react-dom/server');
25 +
26 + // Make them available to the helpers.
27 + return {
28 + ReactDOM,
29 + ReactDOMServer,
30 + };
31 +}
32 +
33 +const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
34 +
35 +describe('ReactDOMServerIntegration', () => {
36 + beforeEach(() => {
37 + resetModules();
38 + });
39 +
40 + describe('React.Fragment', () => {
41 + itRenders('a fragment with one child', async render => {
42 + let e = await render(
43 + <React.Fragment>
44 + <div>text1</div>
45 + </React.Fragment>,
46 + );
47 + let parent = e.parentNode;
48 + expect(parent.childNodes[0].tagName).toBe('DIV');
49 + });
50 +
51 + itRenders('a fragment with several children', async render => {
52 + let Header = props => {
53 + return <p>header</p>;
54 + };
55 + let Footer = props => {
56 + return (
57 + <React.Fragment>
58 + <h2>footer</h2>
59 + <h3>about</h3>
60 + </React.Fragment>
61 + );
62 + };
63 + let e = await render(
64 + <React.Fragment>
65 + <div>text1</div>
66 + <span>text2</span>
67 + <Header />
68 + <Footer />
69 + </React.Fragment>,
70 + );
71 + let parent = e.parentNode;
72 + expect(parent.childNodes[0].tagName).toBe('DIV');
73 + expect(parent.childNodes[1].tagName).toBe('SPAN');
74 + expect(parent.childNodes[2].tagName).toBe('P');
75 + expect(parent.childNodes[3].tagName).toBe('H2');
76 + expect(parent.childNodes[4].tagName).toBe('H3');
77 + });
78 +
79 + itRenders('a nested fragment', async render => {
80 + let e = await render(
81 + <React.Fragment>
82 + <React.Fragment>
83 + <div>text1</div>
84 + </React.Fragment>
85 + <span>text2</span>
86 + <React.Fragment>
87 + <React.Fragment>
88 + <React.Fragment>
89 + {null}
90 + <p />
91 + </React.Fragment>
92 + {false}
93 + </React.Fragment>
94 + </React.Fragment>
95 + </React.Fragment>,
96 + );
97 + let parent = e.parentNode;
98 + expect(parent.childNodes[0].tagName).toBe('DIV');
99 + expect(parent.childNodes[1].tagName).toBe('SPAN');
100 + expect(parent.childNodes[2].tagName).toBe('P');
101 + });
102 +
103 + itRenders('an empty framgent', async render => {
104 + expect(await render(<React.Fragment />)).toBe(null);
105 + });
106 + });
107 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationReconnecting-test.js new
+424
@@ -0,0 +1,424 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +let React;
15 +let ReactDOM;
16 +let ReactDOMServer;
17 +
18 +function initModules() {
19 + // Reset warning cache.
20 + jest.resetModuleRegistry();
21 +
22 + React = require('react');
23 + ReactDOM = require('react-dom');
24 + ReactDOMServer = require('react-dom/server');
25 +
26 + // Make them available to the helpers.
27 + return {
28 + ReactDOM,
29 + ReactDOMServer,
30 + };
31 +}
32 +
33 +const {
34 + resetModules,
35 + expectMarkupMismatch,
36 + expectMarkupMatch,
37 +} = ReactDOMServerIntegrationUtils(initModules);
38 +
39 +describe('ReactDOMServerIntegration', () => {
40 + beforeEach(() => {
41 + resetModules();
42 + });
43 +
44 + describe('reconnecting to server markup', function() {
45 + var EmptyComponent;
46 + beforeEach(() => {
47 + EmptyComponent = class extends React.Component {
48 + render() {
49 + return null;
50 + }
51 + };
52 + });
53 +
54 + describe('elements', function() {
55 + describe('reconnecting different component implementations', function() {
56 + let ES6ClassComponent, PureComponent, bareElement;
57 + beforeEach(() => {
58 + // try each type of component on client and server.
59 + ES6ClassComponent = class extends React.Component {
60 + render() {
61 + return <div id={this.props.id} />;
62 + }
63 + };
64 + PureComponent = props => <div id={props.id} />;
65 + bareElement = <div id="foobarbaz" />;
66 + });
67 +
68 + it('should reconnect ES6 Class to ES6 Class', () =>
69 + expectMarkupMatch(
70 + <ES6ClassComponent id="foobarbaz" />,
71 + <ES6ClassComponent id="foobarbaz" />,
72 + ));
73 +
74 + it('should reconnect Pure Component to ES6 Class', () =>
75 + expectMarkupMatch(
76 + <ES6ClassComponent id="foobarbaz" />,
77 + <PureComponent id="foobarbaz" />,
78 + ));
79 +
80 + it('should reconnect Bare Element to ES6 Class', () =>
81 + expectMarkupMatch(<ES6ClassComponent id="foobarbaz" />, bareElement));
82 +
83 + it('should reconnect ES6 Class to Pure Component', () =>
84 + expectMarkupMatch(
85 + <PureComponent id="foobarbaz" />,
86 + <ES6ClassComponent id="foobarbaz" />,
87 + ));
88 +
89 + it('should reconnect Pure Component to Pure Component', () =>
90 + expectMarkupMatch(
91 + <PureComponent id="foobarbaz" />,
92 + <PureComponent id="foobarbaz" />,
93 + ));
94 +
95 + it('should reconnect Bare Element to Pure Component', () =>
96 + expectMarkupMatch(<PureComponent id="foobarbaz" />, bareElement));
97 +
98 + it('should reconnect ES6 Class to Bare Element', () =>
99 + expectMarkupMatch(bareElement, <ES6ClassComponent id="foobarbaz" />));
100 +
101 + it('should reconnect Pure Component to Bare Element', () =>
102 + expectMarkupMatch(bareElement, <PureComponent id="foobarbaz" />));
103 +
104 + it('should reconnect Bare Element to Bare Element', () =>
105 + expectMarkupMatch(bareElement, bareElement));
106 + });
107 +
108 + it('should error reconnecting different element types', () =>
109 + expectMarkupMismatch(<div />, <span />));
110 +
111 + it('should error reconnecting fewer root children', () =>
112 + expectMarkupMismatch(<span key="a" />, [
113 + <span key="a" />,
114 + <span key="b" />,
115 + ]));
116 +
117 + it('should error reconnecting missing attributes', () =>
118 + expectMarkupMismatch(<div id="foo" />, <div />));
119 +
120 + it('should error reconnecting added attributes', () =>
121 + expectMarkupMismatch(<div />, <div id="foo" />));
122 +
123 + it('should error reconnecting different attribute values', () =>
124 + expectMarkupMismatch(<div id="foo" />, <div id="bar" />));
125 +
126 + it('can explicitly ignore errors reconnecting different element types of children', () =>
127 + expectMarkupMatch(
128 + <div>
129 + <div />
130 + </div>,
131 + <div suppressHydrationWarning={true}>
132 + <span />
133 + </div>,
134 + ));
135 +
136 + it('can explicitly ignore errors reconnecting missing attributes', () =>
137 + expectMarkupMatch(
138 + <div id="foo" />,
139 + <div suppressHydrationWarning={true} />,
140 + ));
141 +
142 + it('can explicitly ignore errors reconnecting added attributes', () =>
143 + expectMarkupMatch(
144 + <div />,
145 + <div id="foo" suppressHydrationWarning={true} />,
146 + ));
147 +
148 + it('can explicitly ignore errors reconnecting different attribute values', () =>
149 + expectMarkupMatch(
150 + <div id="foo" />,
151 + <div id="bar" suppressHydrationWarning={true} />,
152 + ));
153 +
154 + it('can not deeply ignore errors reconnecting different attribute values', () =>
155 + expectMarkupMismatch(
156 + <div>
157 + <div id="foo" />
158 + </div>,
159 + <div suppressHydrationWarning={true}>
160 + <div id="bar" />
161 + </div>,
162 + ));
163 + });
164 +
165 + describe('inline styles', function() {
166 + it('should error reconnecting missing style attribute', () =>
167 + expectMarkupMismatch(<div style={{width: '1px'}} />, <div />));
168 +
169 + it('should error reconnecting added style attribute', () =>
170 + expectMarkupMismatch(<div />, <div style={{width: '1px'}} />));
171 +
172 + it('should error reconnecting empty style attribute', () =>
173 + expectMarkupMismatch(
174 + <div style={{width: '1px'}} />,
175 + <div style={{}} />,
176 + ));
177 +
178 + it('should error reconnecting added style values', () =>
179 + expectMarkupMismatch(
180 + <div style={{}} />,
181 + <div style={{width: '1px'}} />,
182 + ));
183 +
184 + it('should error reconnecting different style values', () =>
185 + expectMarkupMismatch(
186 + <div style={{width: '1px'}} />,
187 + <div style={{width: '2px'}} />,
188 + ));
189 +
190 + it('should reconnect number and string versions of a number', () =>
191 + expectMarkupMatch(
192 + <div style={{width: '1px', height: 2}} />,
193 + <div style={{width: 1, height: '2px'}} />,
194 + ));
195 +
196 + it('should error reconnecting reordered style values', () =>
197 + expectMarkupMismatch(
198 + <div style={{width: '1px', fontSize: '2px'}} />,
199 + <div style={{fontSize: '2px', width: '1px'}} />,
200 + ));
201 +
202 + it('can explicitly ignore errors reconnecting added style values', () =>
203 + expectMarkupMatch(
204 + <div style={{}} />,
205 + <div style={{width: '1px'}} suppressHydrationWarning={true} />,
206 + ));
207 +
208 + it('can explicitly ignore reconnecting different style values', () =>
209 + expectMarkupMatch(
210 + <div style={{width: '1px'}} />,
211 + <div style={{width: '2px'}} suppressHydrationWarning={true} />,
212 + ));
213 + });
214 +
215 + describe('text nodes', function() {
216 + it('should error reconnecting different text', () =>
217 + expectMarkupMismatch(<div>Text</div>, <div>Other Text</div>));
218 +
219 + it('should reconnect a div with a number and string version of number', () =>
220 + expectMarkupMatch(<div>{2}</div>, <div>2</div>));
221 +
222 + it('should error reconnecting different numbers', () =>
223 + expectMarkupMismatch(<div>{2}</div>, <div>{3}</div>));
224 +
225 + it('should error reconnecting different number from text', () =>
226 + expectMarkupMismatch(<div>{2}</div>, <div>3</div>));
227 +
228 + it('should error reconnecting different text in two code blocks', () =>
229 + expectMarkupMismatch(
230 + <div>
231 + {'Text1'}
232 + {'Text2'}
233 + </div>,
234 + <div>
235 + {'Text1'}
236 + {'Text3'}
237 + </div>,
238 + ));
239 +
240 + it('can explicitly ignore reconnecting different text', () =>
241 + expectMarkupMatch(
242 + <div>Text</div>,
243 + <div suppressHydrationWarning={true}>Other Text</div>,
244 + ));
245 +
246 + it('can explicitly ignore reconnecting different text in two code blocks', () =>
247 + expectMarkupMatch(
248 + <div suppressHydrationWarning={true}>
249 + {'Text1'}
250 + {'Text2'}
251 + </div>,
252 + <div suppressHydrationWarning={true}>
253 + {'Text1'}
254 + {'Text3'}
255 + </div>,
256 + ));
257 + });
258 +
259 + describe('element trees and children', function() {
260 + it('should error reconnecting missing children', () =>
261 + expectMarkupMismatch(
262 + <div>
263 + <div />
264 + </div>,
265 + <div />,
266 + ));
267 +
268 + it('should error reconnecting added children', () =>
269 + expectMarkupMismatch(
270 + <div />,
271 + <div>
272 + <div />
273 + </div>,
274 + ));
275 +
276 + it('should error reconnecting more children', () =>
277 + expectMarkupMismatch(
278 + <div>
279 + <div />
280 + </div>,
281 + <div>
282 + <div />
283 + <div />
284 + </div>,
285 + ));
286 +
287 + it('should error reconnecting fewer children', () =>
288 + expectMarkupMismatch(
289 + <div>
290 + <div />
291 + <div />
292 + </div>,
293 + <div>
294 + <div />
295 + </div>,
296 + ));
297 +
298 + it('should error reconnecting reordered children', () =>
299 + expectMarkupMismatch(
300 + <div>
301 + <div />
302 + <span />
303 + </div>,
304 + <div>
305 + <span />
306 + <div />
307 + </div>,
308 + ));
309 +
310 + it('should error reconnecting a div with children separated by whitespace on the client', () =>
311 + expectMarkupMismatch(
312 + <div id="parent">
313 + <div id="child1" />
314 + <div id="child2" />
315 + </div>,
316 + // prettier-ignore
317 + <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
318 + ));
319 +
320 + it('should error reconnecting a div with children separated by different whitespace on the server', () =>
321 + expectMarkupMismatch(
322 + // prettier-ignore
323 + <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
324 + <div id="parent">
325 + <div id="child1" />
326 + <div id="child2" />
327 + </div>,
328 + ));
329 +
330 + it('should error reconnecting a div with children separated by different whitespace', () =>
331 + expectMarkupMismatch(
332 + <div id="parent">
333 + <div id="child1" /> <div id="child2" />
334 + </div>,
335 + // prettier-ignore
336 + <div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
337 + ));
338 +
339 + it('can distinguish an empty component from a dom node', () =>
340 + expectMarkupMismatch(
341 + <div>
342 + <span />
343 + </div>,
344 + <div>
345 + <EmptyComponent />
346 + </div>,
347 + ));
348 +
349 + it('can distinguish an empty component from an empty text component', () =>
350 + expectMarkupMatch(
351 + <div>
352 + <EmptyComponent />
353 + </div>,
354 + <div>{''}</div>,
355 + ));
356 +
357 + it('can explicitly ignore reconnecting more children', () =>
358 + expectMarkupMatch(
359 + <div>
360 + <div />
361 + </div>,
362 + <div suppressHydrationWarning={true}>
363 + <div />
364 + <div />
365 + </div>,
366 + ));
367 +
368 + it('can explicitly ignore reconnecting fewer children', () =>
369 + expectMarkupMatch(
370 + <div>
371 + <div />
372 + <div />
373 + </div>,
374 + <div suppressHydrationWarning={true}>
375 + <div />
376 + </div>,
377 + ));
378 +
379 + it('can explicitly ignore reconnecting reordered children', () =>
380 + expectMarkupMatch(
381 + <div suppressHydrationWarning={true}>
382 + <div />
383 + <span />
384 + </div>,
385 + <div suppressHydrationWarning={true}>
386 + <span />
387 + <div />
388 + </div>,
389 + ));
390 +
391 + it('can not deeply ignore reconnecting reordered children', () =>
392 + expectMarkupMismatch(
393 + <div suppressHydrationWarning={true}>
394 + <div>
395 + <div />
396 + <span />
397 + </div>
398 + </div>,
399 + <div suppressHydrationWarning={true}>
400 + <div>
401 + <span />
402 + <div />
403 + </div>
404 + </div>,
405 + ));
406 + });
407 +
408 + // Markup Mismatches: misc
409 + it('should error reconnecting a div with different dangerouslySetInnerHTML', () =>
410 + expectMarkupMismatch(
411 + <div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
412 + <div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
413 + ));
414 +
415 + it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () =>
416 + expectMarkupMatch(
417 + <div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
418 + <div
419 + dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}}
420 + suppressHydrationWarning={true}
421 + />,
422 + ));
423 + });
424 +});
packages/react-dom/src/__tests__/ReactDOMServerIntegrationRefs-test.js new
+99
@@ -0,0 +1,99 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
13 +
14 +let React;
15 +let ReactDOM;
16 +let ReactDOMServer;
17 +
18 +function initModules() {
19 + // Reset warning cache.
20 + jest.resetModuleRegistry();
21 + React = require('react');
22 + ReactDOM = require('react-dom');
23 + ReactDOMServer = require('react-dom/server');
24 +
25 + // Make them available to the helpers.
26 + return {
27 + ReactDOM,
28 + ReactDOMServer,
29 + };
30 +}
31 +
32 +const {
33 + resetModules,
34 + asyncReactDOMRender,
35 + clientRenderOnServerString,
36 + expectMarkupMatch,
37 +} = ReactDOMServerIntegrationUtils(initModules);
38 +
39 +describe('ReactDOMServerIntegration', () => {
40 + beforeEach(() => {
41 + resetModules();
42 + });
43 +
44 + describe('refs', function() {
45 + it('should not run ref code on server', async () => {
46 + let refCount = 0;
47 + class RefsComponent extends React.Component {
48 + render() {
49 + return <div ref={e => refCount++} />;
50 + }
51 + }
52 + await expectMarkupMatch(<RefsComponent />, <div />);
53 + expect(refCount).toBe(0);
54 + });
55 +
56 + it('should run ref code on client', async () => {
57 + let refCount = 0;
58 + class RefsComponent extends React.Component {
59 + render() {
60 + return <div ref={e => refCount++} />;
61 + }
62 + }
63 + await expectMarkupMatch(<div />, <RefsComponent />);
64 + expect(refCount).toBe(1);
65 + });
66 +
67 + it('should send the correct element to ref functions on client', async () => {
68 + let refElement = null;
69 + class RefsComponent extends React.Component {
70 + render() {
71 + return <div ref={e => (refElement = e)} />;
72 + }
73 + }
74 + const e = await clientRenderOnServerString(<RefsComponent />);
75 + expect(refElement).not.toBe(null);
76 + expect(refElement).toBe(e);
77 + });
78 +
79 + it('should have string refs on client when rendered over server markup', async () => {
80 + class RefsComponent extends React.Component {
81 + render() {
82 + return <div ref="myDiv" />;
83 + }
84 + }
85 +
86 + const markup = ReactDOMServer.renderToString(<RefsComponent />);
87 + const root = document.createElement('div');
88 + root.innerHTML = markup;
89 + let component = null;
90 + resetModules();
91 + await asyncReactDOMRender(
92 + <RefsComponent ref={e => (component = e)} />,
93 + root,
94 + true,
95 + );
96 + expect(component.refs.myDiv).toBe(root.firstChild);
97 + });
98 + });
99 +});
packages/react-dom/src/__tests__/ReactMount-test.js
+1 -3
@@ -11,8 +11,6 @@
11
12 const {COMMENT_NODE} = require('../shared/HTMLNodeType');
13
14 -const invariant = require('invariant');
15 -
14 var React;
15 var ReactDOM;
16 var ReactDOMServer;
@@ -351,7 +349,7 @@ describe('ReactMount', () => {
349 containerDiv = document.createElement('div');
350 containerDiv.innerHTML = 'A<!-- react-mount-point-unstable -->B';
351 mountPoint = containerDiv.childNodes[1];
354 - invariant(mountPoint.nodeType === COMMENT_NODE, 'Expected comment');
352 + expect(mountPoint.nodeType).toBe(COMMENT_NODE);
353 });
354
355 it('renders at a comment node', () => {
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+5 -37
@@ -9,15 +9,12 @@
9
10 'use strict';
11
12 -var ExecutionEnvironment;
12 var React;
13 var ReactDOM;
14 var ReactDOMServer;
15 var ReactTestUtils;
16 var PropTypes;
17
19 -var ROOT_ATTRIBUTE_NAME;
20 -
18 function normalizeCodeLocInfo(str) {
19 return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
20 }
@@ -29,39 +26,26 @@ describe('ReactDOMServer', () => {
26 ReactDOM = require('react-dom');
27 ReactTestUtils = require('react-dom/test-utils');
28 PropTypes = require('prop-types');
32 -
33 - ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
34 - ExecutionEnvironment.canUseDOM = false;
29 ReactDOMServer = require('react-dom/server');
36 -
37 - // TODO: can we express this test with only public API?
38 - var DOMProperty = require('../shared/DOMProperty');
39 - ROOT_ATTRIBUTE_NAME = DOMProperty.ROOT_ATTRIBUTE_NAME;
30 });
31
32 describe('renderToString', () => {
33 it('should generate simple markup', () => {
34 var response = ReactDOMServer.renderToString(<span>hello world</span>);
35 expect(response).toMatch(
46 - new RegExp(
47 - '<span ' + ROOT_ATTRIBUTE_NAME + '=""' + '>hello world</span>',
48 - ),
36 + new RegExp('<span data-reactroot=""' + '>hello world</span>'),
37 );
38 });
39
40 it('should generate simple markup for self-closing tags', () => {
41 var response = ReactDOMServer.renderToString(<img />);
54 - expect(response).toMatch(
55 - new RegExp('<img ' + ROOT_ATTRIBUTE_NAME + '=""' + '/>'),
56 - );
42 + expect(response).toMatch(new RegExp('<img data-reactroot=""' + '/>'));
43 });
44
45 it('should generate simple markup for attribute with `>` symbol', () => {
46 var response = ReactDOMServer.renderToString(<img data-attr=">" />);
47 expect(response).toMatch(
62 - new RegExp(
63 - '<img data-attr="&gt;" ' + ROOT_ATTRIBUTE_NAME + '=""' + '/>',
64 - ),
48 + new RegExp('<img data-attr="&gt;" data-reactroot=""' + '/>'),
49 );
50 });
51
@@ -99,7 +83,7 @@ describe('ReactDOMServer', () => {
83 expect(response).toMatch(
84 new RegExp(
85 '<div ' +
102 - ROOT_ATTRIBUTE_NAME +
86 + 'data-reactroot' +
87 '=""' +
88 '>' +
89 '<span' +
@@ -161,7 +145,7 @@ describe('ReactDOMServer', () => {
145 expect(response).toMatch(
146 new RegExp(
147 '<span ' +
164 - ROOT_ATTRIBUTE_NAME +
148 + 'data-reactroot' +
149 '=""' +
150 '>' +
151 'Component name: <!-- -->TestComponent' +
@@ -176,17 +160,11 @@ describe('ReactDOMServer', () => {
160 }
161
162 runTest();
179 -
180 - // This should work the same regardless of whether you can use DOM or not.
181 - ExecutionEnvironment.canUseDOM = true;
182 - runTest();
163 });
164
165 it('should have the correct mounting behavior (old hydrate API)', () => {
166 spyOnDev(console, 'warn');
167 spyOnDev(console, 'error');
188 - // This test is testing client-side behavior.
189 - ExecutionEnvironment.canUseDOM = true;
168
169 var mountCount = 0;
170 var numClicks = 0;
@@ -234,9 +212,7 @@ describe('ReactDOMServer', () => {
212 ReactDOM.unmountComponentAtNode(element);
213 expect(element.innerHTML).toEqual('');
214
237 - ExecutionEnvironment.canUseDOM = false;
215 lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
239 - ExecutionEnvironment.canUseDOM = true;
216 element.innerHTML = lastMarkup;
217
218 var instance = ReactDOM.render(<TestComponent name="x" />, element);
@@ -287,8 +263,6 @@ describe('ReactDOMServer', () => {
263
264 it('should have the correct mounting behavior (new hydrate API)', () => {
265 spyOnDev(console, 'error');
290 - // This test is testing client-side behavior.
291 - ExecutionEnvironment.canUseDOM = true;
266
267 var mountCount = 0;
268 var numClicks = 0;
@@ -336,9 +310,7 @@ describe('ReactDOMServer', () => {
310 ReactDOM.unmountComponentAtNode(element);
311 expect(element.innerHTML).toEqual('');
312
339 - ExecutionEnvironment.canUseDOM = false;
313 lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
341 - ExecutionEnvironment.canUseDOM = true;
314 element.innerHTML = lastMarkup;
315
316 var instance = ReactDOM.hydrate(<TestComponent name="x" />, element);
@@ -536,10 +508,6 @@ describe('ReactDOMServer', () => {
508 }
509
510 runTest();
539 -
540 - // This should work the same regardless of whether you can use DOM or not.
541 - ExecutionEnvironment.canUseDOM = true;
542 - runTest();
511 });
512
513 it('should throw with silly args', () => {
packages/react-dom/src/__tests__/ReactTreeTraversal-test.internal.js renamed
packages/react-dom/src/__tests__/utils/ReactDOMServerIntegrationTestUtils.js new
+328
@@ -0,0 +1,328 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +const stream = require('stream');
13 +
14 +module.exports = function(initModules) {
15 + let ReactDOM;
16 + let ReactDOMServer;
17 +
18 + function resetModules() {
19 + ({ReactDOM, ReactDOMServer} = initModules());
20 + }
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(
74 + reactElement,
75 + domElement,
76 + forceHydrate,
77 + errorCount = 0,
78 + ) {
79 + return expectErrors(async () => {
80 + await asyncReactDOMRender(reactElement, domElement, forceHydrate);
81 + return domElement.firstChild;
82 + }, errorCount);
83 + }
84 +
85 + async function renderIntoString(reactElement, errorCount = 0) {
86 + return await expectErrors(
87 + () =>
88 + new Promise(resolve =>
89 + resolve(ReactDOMServer.renderToString(reactElement)),
90 + ),
91 + errorCount,
92 + );
93 + }
94 +
95 + // Renders text using SSR and then stuffs it into a DOM node; returns the DOM
96 + // element that corresponds with the reactElement.
97 + // Does not render on client or perform client-side revival.
98 + async function serverRender(reactElement, errorCount = 0) {
99 + const markup = await renderIntoString(reactElement, errorCount);
100 + var domElement = document.createElement('div');
101 + domElement.innerHTML = markup;
102 + return domElement.firstChild;
103 + }
104 +
105 + // this just drains a readable piped into it to a string, which can be accessed
106 + // via .buffer.
107 + class DrainWritable extends stream.Writable {
108 + constructor(options) {
109 + super(options);
110 + this.buffer = '';
111 + }
112 +
113 + _write(chunk, encoding, cb) {
114 + this.buffer += chunk;
115 + cb();
116 + }
117 + }
118 +
119 + async function renderIntoStream(reactElement, errorCount = 0) {
120 + return await expectErrors(
121 + () =>
122 + new Promise(resolve => {
123 + let writable = new DrainWritable();
124 + ReactDOMServer.renderToNodeStream(reactElement).pipe(writable);
125 + writable.on('finish', () => resolve(writable.buffer));
126 + }),
127 + errorCount,
128 + );
129 + }
130 +
131 + // Renders text using node stream SSR and then stuffs it into a DOM node;
132 + // returns the DOM element that corresponds with the reactElement.
133 + // Does not render on client or perform client-side revival.
134 + async function streamRender(reactElement, errorCount = 0) {
135 + const markup = await renderIntoStream(reactElement, errorCount);
136 + var domElement = document.createElement('div');
137 + domElement.innerHTML = markup;
138 + return domElement.firstChild;
139 + }
140 +
141 + const clientCleanRender = (element, errorCount = 0) => {
142 + const div = document.createElement('div');
143 + return renderIntoDom(element, div, false, errorCount);
144 + };
145 +
146 + const clientRenderOnServerString = async (element, errorCount = 0) => {
147 + const markup = await renderIntoString(element, errorCount);
148 + resetModules();
149 +
150 + var domElement = document.createElement('div');
151 + domElement.innerHTML = markup;
152 + let serverNode = domElement.firstChild;
153 +
154 + const firstClientNode = await renderIntoDom(
155 + element,
156 + domElement,
157 + true,
158 + errorCount,
159 + );
160 + let clientNode = firstClientNode;
161 +
162 + // Make sure all top level nodes match up
163 + while (serverNode || clientNode) {
164 + expect(serverNode != null).toBe(true);
165 + expect(clientNode != null).toBe(true);
166 + expect(clientNode.nodeType).toBe(serverNode.nodeType);
167 + // Assert that the DOM element hasn't been replaced.
168 + // Note that we cannot use expect(serverNode).toBe(clientNode) because
169 + // of jest bug #1772.
170 + expect(serverNode === clientNode).toBe(true);
171 + serverNode = serverNode.nextSibling;
172 + clientNode = clientNode.nextSibling;
173 + }
174 + return firstClientNode;
175 + };
176 +
177 + function BadMarkupExpected() {}
178 +
179 + const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
180 + // First we render the top of bad mark up.
181 + var domElement = document.createElement('div');
182 + domElement.innerHTML =
183 + '<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
184 + await renderIntoDom(element, domElement, true, errorCount + 1);
185 +
186 + // This gives us the resulting text content.
187 + var hydratedTextContent = domElement.textContent;
188 +
189 + // Next we render the element into a clean DOM node client side.
190 + const cleanDomElement = document.createElement('div');
191 + await asyncReactDOMRender(element, cleanDomElement, true);
192 + // This gives us the expected text content.
193 + const cleanTextContent = cleanDomElement.textContent;
194 +
195 + // The only guarantee is that text content has been patched up if needed.
196 + expect(hydratedTextContent).toBe(cleanTextContent);
197 +
198 + // Abort any further expects. All bets are off at this point.
199 + throw new BadMarkupExpected();
200 + };
201 +
202 + // runs a DOM rendering test as four different tests, with four different rendering
203 + // scenarios:
204 + // -- render to string on server
205 + // -- render on client without any server markup "clean client render"
206 + // -- render on client on top of good server-generated string markup
207 + // -- render on client on top of bad server-generated markup
208 + //
209 + // testFn is a test that has one arg, which is a render function. the render
210 + // function takes in a ReactElement and an optional expected error count and
211 + // returns a promise of a DOM Element.
212 + //
213 + // You should only perform tests that examine the DOM of the results of
214 + // render; you should not depend on the interactivity of the returned DOM element,
215 + // as that will not work in the server string scenario.
216 + function itRenders(desc, testFn) {
217 + it(`renders ${desc} with server string render`, () => testFn(serverRender));
218 + it(`renders ${desc} with server stream render`, () => testFn(streamRender));
219 + itClientRenders(desc, testFn);
220 + }
221 +
222 + // run testFn in three different rendering scenarios:
223 + // -- render on client without any server markup "clean client render"
224 + // -- render on client on top of good server-generated string markup
225 + // -- render on client on top of bad server-generated markup
226 + //
227 + // testFn is a test that has one arg, which is a render function. the render
228 + // function takes in a ReactElement and an optional expected error count and
229 + // returns a promise of a DOM Element.
230 + //
231 + // Since all of the renders in this function are on the client, you can test interactivity,
232 + // unlike with itRenders.
233 + function itClientRenders(desc, testFn) {
234 + it(`renders ${desc} with clean client render`, () =>
235 + testFn(clientCleanRender));
236 + it(`renders ${desc} with client render on top of good server markup`, () =>
237 + testFn(clientRenderOnServerString));
238 + it(`renders ${
239 + desc
240 + } with client render on top of bad server markup`, async () => {
241 + try {
242 + await testFn(clientRenderOnBadMarkup);
243 + } catch (x) {
244 + // We expect this to trigger the BadMarkupExpected rejection.
245 + if (!(x instanceof BadMarkupExpected)) {
246 + // If not, rethrow.
247 + throw x;
248 + }
249 + }
250 + });
251 + }
252 +
253 + function itThrows(desc, testFn, partialMessage) {
254 + it(`throws ${desc}`, () => {
255 + return testFn().then(
256 + () => expect(false).toBe('The promise resolved and should not have.'),
257 + err => {
258 + expect(err).toBeInstanceOf(Error);
259 + expect(err.message).toContain(partialMessage);
260 + },
261 + );
262 + });
263 + }
264 +
265 + function itThrowsWhenRendering(desc, testFn, partialMessage) {
266 + itThrows(
267 + `when rendering ${desc} with server string render`,
268 + () => testFn(serverRender),
269 + partialMessage,
270 + );
271 + itThrows(
272 + `when rendering ${desc} with clean client render`,
273 + () => testFn(clientCleanRender),
274 + partialMessage,
275 + );
276 +
277 + // we subtract one from the warning count here because the throw means that it won't
278 + // get the usual markup mismatch warning.
279 + itThrows(
280 + `when rendering ${desc} with client render on top of bad server markup`,
281 + () =>
282 + testFn((element, warningCount = 0) =>
283 + clientRenderOnBadMarkup(element, warningCount - 1),
284 + ),
285 + partialMessage,
286 + );
287 + }
288 +
289 + // renders serverElement to a string, sticks it into a DOM element, and then
290 + // tries to render clientElement on top of it. shouldMatch is a boolean
291 + // telling whether we should expect the markup to match or not.
292 + async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
293 + const domElement = await serverRender(serverElement);
294 + resetModules();
295 + return renderIntoDom(
296 + clientElement,
297 + domElement.parentNode,
298 + true,
299 + shouldMatch ? 0 : 1,
300 + );
301 + }
302 +
303 + // expects that rendering clientElement on top of a server-rendered
304 + // serverElement does NOT raise a markup mismatch warning.
305 + function expectMarkupMatch(serverElement, clientElement) {
306 + return testMarkupMatch(serverElement, clientElement, true);
307 + }
308 +
309 + // expects that rendering clientElement on top of a server-rendered
310 + // serverElement DOES raise a markup mismatch warning.
311 + function expectMarkupMismatch(serverElement, clientElement) {
312 + return testMarkupMatch(serverElement, clientElement, false);
313 + }
314 +
315 + return {
316 + resetModules,
317 + expectMarkupMismatch,
318 + expectMarkupMatch,
319 + itRenders,
320 + itClientRenders,
321 + itThrowsWhenRendering,
322 + asyncReactDOMRender,
323 + serverRender,
324 + clientRenderOnServerString,
325 + renderIntoDom,
326 + streamRender,
327 + };
328 +};
packages/react-dom/src/__tests__/validateDOMNesting-test.internal.js renamed
packages/react-dom/src/client/ReactDOMFiberComponent.js
+1 -1
@@ -67,7 +67,7 @@ if (__DEV__) {
67 var validatePropertiesInDevelopment = function(type, props) {
68 validateARIAProperties(type, props);
69 validateInputProperties(type, props);
70 - validateUnknownProperties(type, props);
70 + validateUnknownProperties(type, props, /* canUseEventSystem */ true);
71 };
72
73 // HTML parsing normalizes CR and CRLF to LF.
packages/react-dom/src/events/__tests__/BeforeInputEventPlugin-test.internal.js renamed
packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js renamed
packages/react-dom/src/events/__tests__/SelectEventPlugin-test.internal.js renamed
packages/react-dom/src/events/__tests__/SyntheticKeyboardEvent-test.internal.js renamed
packages/react-dom/src/server/ReactPartialRenderer.js
+1 -1
@@ -54,7 +54,7 @@ if (__DEV__) {
54 var validatePropertiesInDevelopment = function(type, props) {
55 validateARIAProperties(type, props);
56 validateInputProperties(type, props);
57 - validateUnknownProperties(type, props);
57 + validateUnknownProperties(type, props, /* canUseEventSystem */ false);
58 };
59
60 var describeStackFrame = function(element): string {
packages/react-dom/src/shared/ReactDOMUnknownPropertyHook.js
+55 -46
@@ -7,7 +7,6 @@
7
8 import {
9 registrationNameModules,
10 - plugins,
10 possibleRegistrationNames,
11 } from 'events/EventPluginRegistry';
12 import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState';
@@ -30,51 +29,72 @@ function getStackAddendum() {
29 if (__DEV__) {
30 var warnedProperties = {};
31 var hasOwnProperty = Object.prototype.hasOwnProperty;
33 - var EVENT_NAME_REGEX = /^on[A-Z]/;
32 + var EVENT_NAME_REGEX = /^on./;
33 + var INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
34 var rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$');
35 var rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$');
36
37 - var validateProperty = function(tagName, name, value) {
37 + var validateProperty = function(tagName, name, value, canUseEventSystem) {
38 if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
39 return true;
40 }
41
42 - if (registrationNameModules.hasOwnProperty(name)) {
43 - return true;
44 - }
45 -
46 - if (plugins.length === 0 && EVENT_NAME_REGEX.test(name)) {
47 - // If no event plugins have been injected, we might be in a server environment.
48 - // Don't check events in this case.
49 - return true;
50 - }
51 -
42 var lowerCasedName = name.toLowerCase();
53 - var registrationName = possibleRegistrationNames.hasOwnProperty(
54 - lowerCasedName,
55 - )
56 - ? possibleRegistrationNames[lowerCasedName]
57 - : null;
58 -
59 - if (registrationName != null) {
43 + if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
44 warning(
45 false,
62 - 'Invalid event handler property `%s`. Did you mean `%s`?%s',
63 - name,
64 - registrationName,
65 - getStackAddendum(),
46 + 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
47 + 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
48 + 'are not needed/supported by React.',
49 );
50 warnedProperties[name] = true;
51 return true;
52 }
53
71 - if (lowerCasedName.indexOf('on') === 0 && lowerCasedName.length > 2) {
72 - warning(
73 - false,
74 - 'Unknown event handler property `%s`. It will be ignored.%s',
75 - name,
76 - getStackAddendum(),
77 - );
54 + // We can't rely on the event system being injected on the server.
55 + if (canUseEventSystem) {
56 + if (registrationNameModules.hasOwnProperty(name)) {
57 + return true;
58 + }
59 + var registrationName = possibleRegistrationNames.hasOwnProperty(
60 + lowerCasedName,
61 + )
62 + ? possibleRegistrationNames[lowerCasedName]
63 + : null;
64 + if (registrationName != null) {
65 + warning(
66 + false,
67 + 'Invalid event handler property `%s`. Did you mean `%s`?%s',
68 + name,
69 + registrationName,
70 + getStackAddendum(),
71 + );
72 + warnedProperties[name] = true;
73 + return true;
74 + }
75 + if (EVENT_NAME_REGEX.test(name)) {
76 + warning(
77 + false,
78 + 'Unknown event handler property `%s`. It will be ignored.%s',
79 + name,
80 + getStackAddendum(),
81 + );
82 + warnedProperties[name] = true;
83 + return true;
84 + }
85 + } else if (EVENT_NAME_REGEX.test(name)) {
86 + // If no event plugins have been injected, we are in a server environment.
87 + // So we can't tell if the event name is correct for sure, but we can filter
88 + // out known bad ones like `onclick`. We can't suggest a specific replacement though.
89 + if (INVALID_EVENT_NAME_REGEX.test(name)) {
90 + warning(
91 + false,
92 + 'Invalid event handler property `%s`. ' +
93 + 'React events use the camelCase naming convention, for example `onClick`.%s',
94 + name,
95 + getStackAddendum(),
96 + );
97 + }
98 warnedProperties[name] = true;
99 return true;
100 }
@@ -84,17 +104,6 @@ if (__DEV__) {
104 return true;
105 }
106
87 - if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
88 - warning(
89 - false,
90 - 'React uses onFocus and onBlur instead of onFocusIn and onFocusOut. ' +
91 - 'All React events are normalized to bubble, so onFocusIn and onFocusOut ' +
92 - 'are not needed/supported by React.',
93 - );
94 - warnedProperties[name] = true;
95 - return true;
96 - }
97 -
107 if (lowerCasedName === 'innerhtml') {
108 warning(
109 false,
@@ -233,10 +242,10 @@ if (__DEV__) {
242 };
243 }
244
236 -var warnUnknownProperties = function(type, props) {
245 +var warnUnknownProperties = function(type, props, canUseEventSystem) {
246 var unknownProps = [];
247 for (var key in props) {
239 - var isValid = validateProperty(type, key, props[key]);
248 + var isValid = validateProperty(type, key, props[key], canUseEventSystem);
249 if (!isValid) {
250 unknownProps.push(key);
251 }
@@ -266,9 +275,9 @@ var warnUnknownProperties = function(type, props) {
275 }
276 };
277
269 -export function validateProperties(type, props) {
278 +export function validateProperties(type, props, canUseEventSystem) {
279 if (isCustomComponent(type, props)) {
280 return;
281 }
273 - warnUnknownProperties(type, props);
282 + warnUnknownProperties(type, props, canUseEventSystem);
283 }
packages/react-native-renderer/src/__tests__/ReactNativeEvents-test.internal.js renamed
packages/react-native-renderer/src/__tests__/ReactNativeMount-test.internal.js renamed
packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeEvents-test.internal.js.snap renamed
packages/react-native-renderer/src/__tests__/__snapshots__/ReactNativeMount-test.internal.js.snap renamed
packages/react-native-renderer/src/__tests__/createReactNativeComponentClass-test.internal.js renamed
packages/react-noop-renderer/npm/index.js
+5 -1
@@ -1,3 +1,7 @@
1 'use strict';
2
3 -module.exports = require('./cjs/react-noop-renderer.development.js');
3 +if (process.env.NODE_ENV === 'production') {
4 + module.exports = require('./cjs/react-noop-renderer.production.min.js');
5 +} else {
6 + module.exports = require('./cjs/react-noop-renderer.development.js');
7 +}
packages/react-noop-renderer/package.json
+2 -1
@@ -9,7 +9,8 @@
9 "dependencies": {
10 "fbjs": "^0.8.16",
11 "object-assign": "^4.1.1",
12 - "prop-types": "^15.6.0"
12 + "prop-types": "^15.6.0",
13 + "regenerator-runtime": "^0.11.0"
14 },
15 "peerDependencies": {
16 "react": "^16.0.0"
packages/react-reconciler/src/__tests__/ReactFragment-test.internal.js renamed
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.js
-254
@@ -21,10 +21,6 @@ describe('ReactIncrementalErrorHandling', () => {
21 ReactNoop = require('react-noop-renderer');
22 });
23
24 - afterEach(() => {
25 - jest.unmock('../ReactFiberErrorLogger');
26 - });
27 -
24 function div(...children) {
25 children = children.map(c => (typeof c === 'string' ? {text: c} : c));
26 return {type: 'div', children, prop: undefined};
@@ -954,254 +950,4 @@ describe('ReactIncrementalErrorHandling', () => {
950 });
951 expect(() => ReactNoop.flush()).toThrow('Error!');
952 });
957 -
958 - describe('ReactFiberErrorLogger', () => {
959 - function normalizeCodeLocInfo(str) {
960 - return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
961 - }
962 -
963 - it('should log errors that occur during the begin phase', () => {
964 - // Intentionally spy in production too.
965 - spyOn(console, 'error');
966 -
967 - class ErrorThrowingComponent extends React.Component {
968 - componentWillMount() {
969 - const error = new Error('componentWillMount error');
970 - // Note: it's `true` on the Error prototype our test environment.
971 - // That lets us avoid asserting on warnings for each expected error.
972 - // Here we intentionally shadow it to test logging, like in real apps.
973 - error.suppressReactErrorLogging = undefined;
974 - throw error;
975 - }
976 - render() {
977 - return <div />;
978 - }
979 - }
980 -
981 - try {
982 - ReactNoop.render(
983 - <div>
984 - <span>
985 - <ErrorThrowingComponent />
986 - </span>
987 - </div>,
988 - );
989 - ReactNoop.flushDeferredPri();
990 - } catch (error) {}
991 -
992 - expect(console.error.calls.count()).toBe(1);
993 - const errorMessage = console.error.calls.argsFor(0)[0];
994 - if (__DEV__) {
995 - expect(normalizeCodeLocInfo(errorMessage)).toContain(
996 - 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
997 - ' in ErrorThrowingComponent (at **)\n' +
998 - ' in span (at **)\n' +
999 - ' in div (at **)',
1000 - );
1001 - expect(errorMessage).toContain(
1002 - 'Consider adding an error boundary to your tree to customize error handling behavior.',
1003 - );
1004 - } else {
1005 - expect(errorMessage.message).toContain('componentWillMount error');
1006 - }
1007 - });
1008 -
1009 - it('should log errors that occur during the commit phase', () => {
1010 - // Intentionally spy in production too.
1011 - spyOn(console, 'error');
1012 -
1013 - class ErrorThrowingComponent extends React.Component {
1014 - componentDidMount() {
1015 - const error = new Error('componentDidMount error');
1016 - // Note: it's `true` on the Error prototype our test environment.
1017 - // That lets us avoid asserting on warnings for each expected error.
1018 - // Here we intentionally shadow it to test logging, like in real apps.
1019 - error.suppressReactErrorLogging = undefined;
1020 - throw error;
1021 - }
1022 - render() {
1023 - return <div />;
1024 - }
1025 - }
1026 -
1027 - try {
1028 - ReactNoop.render(
1029 - <div>
1030 - <span>
1031 - <ErrorThrowingComponent />
1032 - </span>
1033 - </div>,
1034 - );
1035 - ReactNoop.flushDeferredPri();
1036 - } catch (error) {}
1037 -
1038 - expect(console.error.calls.count()).toBe(1);
1039 - const errorMessage = console.error.calls.argsFor(0)[0];
1040 - if (__DEV__) {
1041 - expect(normalizeCodeLocInfo(errorMessage)).toContain(
1042 - 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
1043 - ' in ErrorThrowingComponent (at **)\n' +
1044 - ' in span (at **)\n' +
1045 - ' in div (at **)',
1046 - );
1047 - expect(errorMessage).toContain(
1048 - 'Consider adding an error boundary to your tree to customize error handling behavior.',
1049 - );
1050 - } else {
1051 - expect(errorMessage.message).toBe('componentDidMount error');
1052 - }
1053 - });
1054 -
1055 - it('should ignore errors thrown in log method to prevent cycle', () => {
1056 - jest.resetModules();
1057 - jest.mock('../ReactFiberErrorLogger');
1058 - React = require('react');
1059 - ReactNoop = require('react-noop-renderer');
1060 - // Intentionally spy in production too.
1061 - spyOn(console, 'error');
1062 -
1063 - class ErrorThrowingComponent extends React.Component {
1064 - render() {
1065 - throw new Error('render error');
1066 - }
1067 - }
1068 -
1069 - const logCapturedErrorCalls = [];
1070 -
1071 - const ReactFiberErrorLogger = require('../ReactFiberErrorLogger');
1072 - ReactFiberErrorLogger.logCapturedError.mockImplementation(
1073 - capturedError => {
1074 - logCapturedErrorCalls.push(capturedError);
1075 - const error = new Error('logCapturedError error');
1076 - // Note: it's `true` on the Error prototype our test environment.
1077 - // That lets us avoid asserting on warnings for each expected error.
1078 - // Here we intentionally shadow it to test logging, like in real apps.
1079 - error.suppressReactErrorLogging = undefined;
1080 - throw error;
1081 - },
1082 - );
1083 -
1084 - try {
1085 - ReactNoop.render(
1086 - <div>
1087 - <span>
1088 - <ErrorThrowingComponent />
1089 - </span>
1090 - </div>,
1091 - );
1092 - ReactNoop.flushDeferredPri();
1093 - } catch (error) {}
1094 -
1095 - expect(logCapturedErrorCalls.length).toBe(1);
1096 -
1097 - // The error thrown in logCapturedError should also be logged
1098 - expect(console.error.calls.count()).toBe(1);
1099 - expect(console.error.calls.argsFor(0)[0].message).toContain(
1100 - 'logCapturedError error',
1101 - );
1102 - });
1103 -
1104 - it('should relay info about error boundary and retry attempts if applicable', () => {
1105 - // Intentionally spy in production too.
1106 - spyOn(console, 'error');
1107 -
1108 - class ParentComponent extends React.Component {
1109 - render() {
1110 - return <ErrorBoundaryComponent />;
1111 - }
1112 - }
1113 -
1114 - let handleErrorCalls = [];
1115 - let renderAttempts = 0;
1116 -
1117 - class ErrorBoundaryComponent extends React.Component {
1118 - componentDidCatch(error) {
1119 - handleErrorCalls.push(error);
1120 - this.setState({}); // Render again
1121 - }
1122 - render() {
1123 - return <ErrorThrowingComponent />;
1124 - }
1125 - }
1126 -
1127 - class ErrorThrowingComponent extends React.Component {
1128 - componentDidMount() {
1129 - const error = new Error('componentDidMount error');
1130 - // Note: it's `true` on the Error prototype our test environment.
1131 - // That lets us avoid asserting on warnings for each expected error.
1132 - // Here we intentionally shadow it to test logging, like in real apps.
1133 - error.suppressReactErrorLogging = undefined;
1134 - throw error;
1135 - }
1136 - render() {
1137 - renderAttempts++;
1138 - return <div />;
1139 - }
1140 - }
1141 -
1142 - try {
1143 - ReactNoop.render(<ParentComponent />);
1144 - ReactNoop.flush();
1145 - } catch (error) {}
1146 -
1147 - expect(renderAttempts).toBe(2);
1148 - expect(handleErrorCalls.length).toBe(1);
1149 - expect(console.error.calls.count()).toBe(2);
1150 - if (__DEV__) {
1151 - expect(console.error.calls.argsFor(0)[0]).toContain(
1152 - 'The above error occurred in the <ErrorThrowingComponent> component:',
1153 - );
1154 - expect(console.error.calls.argsFor(0)[0]).toContain(
1155 - 'React will try to recreate this component tree from scratch ' +
1156 - 'using the error boundary you provided, ErrorBoundaryComponent.',
1157 - );
1158 - expect(console.error.calls.argsFor(1)[0]).toContain(
1159 - 'The above error occurred in the <ErrorThrowingComponent> component:',
1160 - );
1161 - expect(console.error.calls.argsFor(1)[0]).toContain(
1162 - 'This error was initially handled by the error boundary ErrorBoundaryComponent.\n' +
1163 - 'Recreating the tree from scratch failed so React will unmount the tree.',
1164 - );
1165 - }
1166 - });
1167 - });
1168 -
1169 - it('resets instance variables before unmounting failed node', () => {
1170 - class ErrorBoundary extends React.Component {
1171 - state = {error: null};
1172 - componentDidCatch(error) {
1173 - this.setState({error});
1174 - }
1175 - render() {
1176 - return this.state.error ? null : this.props.children;
1177 - }
1178 - }
1179 - class Foo extends React.Component {
1180 - state = {step: 0};
1181 - componentDidMount() {
1182 - this.setState({step: 1});
1183 - }
1184 - componentWillUnmount() {
1185 - ReactNoop.yield('componentWillUnmount: ' + this.state.step);
1186 - }
1187 - render() {
1188 - ReactNoop.yield('render: ' + this.state.step);
1189 - if (this.state.step > 0) {
1190 - throw new Error('oops');
1191 - }
1192 - return null;
1193 - }
1194 - }
1195 -
1196 - ReactNoop.render(
1197 - <ErrorBoundary>
1198 - <Foo />
1199 - </ErrorBoundary>,
1200 - );
1201 - expect(ReactNoop.flush()).toEqual([
1202 - 'render: 0',
1203 - 'render: 1',
1204 - 'componentWillUnmount: 0',
1205 - ]);
1206 - });
953 });
packages/react-reconciler/src/__tests__/ReactIncrementalErrorLogging-test.internal.js new
+1207
@@ -0,0 +1,1207 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +var PropTypes;
13 +var React;
14 +var ReactNoop;
15 +
16 +describe('ReactIncrementalErrorLogging', () => {
17 + beforeEach(() => {
18 + jest.resetModules();
19 + PropTypes = require('prop-types');
20 + React = require('react');
21 + ReactNoop = require('react-noop-renderer');
22 + });
23 +
24 + afterEach(() => {
25 + jest.unmock('../ReactFiberErrorLogger');
26 + });
27 +
28 + function div(...children) {
29 + children = children.map(c => (typeof c === 'string' ? {text: c} : c));
30 + return {type: 'div', children, prop: undefined};
31 + }
32 +
33 + function span(prop) {
34 + return {type: 'span', children: [], prop};
35 + }
36 +
37 + it('catches render error in a boundary during full deferred mounting', () => {
38 + class ErrorBoundary extends React.Component {
39 + state = {error: null};
40 + componentDidCatch(error) {
41 + this.setState({error});
42 + }
43 + render() {
44 + if (this.state.error) {
45 + return (
46 + <span prop={`Caught an error: ${this.state.error.message}.`} />
47 + );
48 + }
49 + return this.props.children;
50 + }
51 + }
52 +
53 + function BrokenRender(props) {
54 + throw new Error('Hello');
55 + }
56 +
57 + ReactNoop.render(
58 + <ErrorBoundary>
59 + <BrokenRender />
60 + </ErrorBoundary>,
61 + );
62 + ReactNoop.flushDeferredPri();
63 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
64 + });
65 +
66 + it('catches render error in a boundary during partial deferred mounting', () => {
67 + var ops = [];
68 + class ErrorBoundary extends React.Component {
69 + state = {error: null};
70 + componentDidCatch(error) {
71 + ops.push('ErrorBoundary componentDidCatch');
72 + this.setState({error});
73 + }
74 + render() {
75 + if (this.state.error) {
76 + ops.push('ErrorBoundary render error');
77 + return (
78 + <span prop={`Caught an error: ${this.state.error.message}.`} />
79 + );
80 + }
81 + ops.push('ErrorBoundary render success');
82 + return this.props.children;
83 + }
84 + }
85 +
86 + function BrokenRender(props) {
87 + ops.push('BrokenRender');
88 + throw new Error('Hello');
89 + }
90 +
91 + ReactNoop.render(
92 + <ErrorBoundary>
93 + <BrokenRender />
94 + </ErrorBoundary>,
95 + );
96 +
97 + ReactNoop.flushDeferredPri(15);
98 + expect(ops).toEqual(['ErrorBoundary render success']);
99 + expect(ReactNoop.getChildren()).toEqual([]);
100 +
101 + ops.length = 0;
102 + ReactNoop.flushDeferredPri(30);
103 + expect(ops).toEqual([
104 + 'BrokenRender',
105 + 'ErrorBoundary componentDidCatch',
106 + 'ErrorBoundary render error',
107 + ]);
108 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
109 + });
110 +
111 + it('catches render error in a boundary during synchronous mounting', () => {
112 + var ops = [];
113 + class ErrorBoundary extends React.Component {
114 + state = {error: null};
115 + componentDidCatch(error) {
116 + ops.push('ErrorBoundary componentDidCatch');
117 + this.setState({error});
118 + }
119 + render() {
120 + if (this.state.error) {
121 + ops.push('ErrorBoundary render error');
122 + return (
123 + <span prop={`Caught an error: ${this.state.error.message}.`} />
124 + );
125 + }
126 + ops.push('ErrorBoundary render success');
127 + return this.props.children;
128 + }
129 + }
130 +
131 + function BrokenRender(props) {
132 + ops.push('BrokenRender');
133 + throw new Error('Hello');
134 + }
135 +
136 + ReactNoop.flushSync(() => {
137 + ReactNoop.render(
138 + <ErrorBoundary>
139 + <BrokenRender />
140 + </ErrorBoundary>,
141 + );
142 + });
143 +
144 + expect(ops).toEqual([
145 + 'ErrorBoundary render success',
146 + 'BrokenRender',
147 + 'ErrorBoundary componentDidCatch',
148 + 'ErrorBoundary render error',
149 + ]);
150 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
151 + });
152 +
153 + it('catches render error in a boundary during batched mounting', () => {
154 + var ops = [];
155 + class ErrorBoundary extends React.Component {
156 + state = {error: null};
157 + componentDidCatch(error) {
158 + ops.push('ErrorBoundary componentDidCatch');
159 + this.setState({error});
160 + }
161 + render() {
162 + if (this.state.error) {
163 + ops.push('ErrorBoundary render error');
164 + return (
165 + <span prop={`Caught an error: ${this.state.error.message}.`} />
166 + );
167 + }
168 + ops.push('ErrorBoundary render success');
169 + return this.props.children;
170 + }
171 + }
172 +
173 + function BrokenRender(props) {
174 + ops.push('BrokenRender');
175 + throw new Error('Hello');
176 + }
177 +
178 + ReactNoop.flushSync(() => {
179 + ReactNoop.render(<ErrorBoundary>Before the storm.</ErrorBoundary>);
180 + ReactNoop.render(
181 + <ErrorBoundary>
182 + <BrokenRender />
183 + </ErrorBoundary>,
184 + );
185 + });
186 +
187 + expect(ops).toEqual([
188 + 'ErrorBoundary render success',
189 + 'BrokenRender',
190 + 'ErrorBoundary componentDidCatch',
191 + 'ErrorBoundary render error',
192 + ]);
193 + expect(ReactNoop.getChildren()).toEqual([span('Caught an error: Hello.')]);
194 + });
195 +
196 + it('propagates an error from a noop error boundary during full deferred mounting', () => {
197 + var ops = [];
198 + class RethrowErrorBoundary extends React.Component {
199 + componentDidCatch(error) {
200 + ops.push('RethrowErrorBoundary componentDidCatch');
201 + throw error;
202 + }
203 + render() {
204 + ops.push('RethrowErrorBoundary render');
205 + return this.props.children;
206 + }
207 + }
208 +
209 + function BrokenRender() {
210 + ops.push('BrokenRender');
211 + throw new Error('Hello');
212 + }
213 +
214 + ReactNoop.render(
215 + <RethrowErrorBoundary>
216 + <BrokenRender />
217 + </RethrowErrorBoundary>,
218 + );
219 +
220 + expect(() => {
221 + ReactNoop.flush();
222 + }).toThrow('Hello');
223 + expect(ops).toEqual([
224 + 'RethrowErrorBoundary render',
225 + 'BrokenRender',
226 + 'RethrowErrorBoundary componentDidCatch',
227 + ]);
228 + expect(ReactNoop.getChildren()).toEqual([]);
229 + });
230 +
231 + it('propagates an error from a noop error boundary during partial deferred mounting', () => {
232 + var ops = [];
233 + class RethrowErrorBoundary extends React.Component {
234 + componentDidCatch(error) {
235 + ops.push('RethrowErrorBoundary componentDidCatch');
236 + throw error;
237 + }
238 + render() {
239 + ops.push('RethrowErrorBoundary render');
240 + return this.props.children;
241 + }
242 + }
243 +
244 + function BrokenRender() {
245 + ops.push('BrokenRender');
246 + throw new Error('Hello');
247 + }
248 +
249 + ReactNoop.render(
250 + <RethrowErrorBoundary>
251 + <BrokenRender />
252 + </RethrowErrorBoundary>,
253 + );
254 +
255 + ReactNoop.flushDeferredPri(15);
256 + expect(ops).toEqual(['RethrowErrorBoundary render']);
257 +
258 + ops.length = 0;
259 + expect(() => {
260 + ReactNoop.flush();
261 + }).toThrow('Hello');
262 + expect(ops).toEqual([
263 + 'BrokenRender',
264 + 'RethrowErrorBoundary componentDidCatch',
265 + ]);
266 + expect(ReactNoop.getChildren()).toEqual([]);
267 + });
268 +
269 + it('propagates an error from a noop error boundary during synchronous mounting', () => {
270 + var ops = [];
271 + class RethrowErrorBoundary extends React.Component {
272 + componentDidCatch(error) {
273 + ops.push('RethrowErrorBoundary componentDidCatch');
274 + throw error;
275 + }
276 + render() {
277 + ops.push('RethrowErrorBoundary render');
278 + return this.props.children;
279 + }
280 + }
281 +
282 + function BrokenRender() {
283 + ops.push('BrokenRender');
284 + throw new Error('Hello');
285 + }
286 +
287 + expect(() => {
288 + ReactNoop.flushSync(() => {
289 + ReactNoop.render(
290 + <RethrowErrorBoundary>
291 + <BrokenRender />
292 + </RethrowErrorBoundary>,
293 + );
294 + });
295 + }).toThrow('Hello');
296 + expect(ops).toEqual([
297 + 'RethrowErrorBoundary render',
298 + 'BrokenRender',
299 + 'RethrowErrorBoundary componentDidCatch',
300 + ]);
301 + expect(ReactNoop.getChildren()).toEqual([]);
302 + });
303 +
304 + it('propagates an error from a noop error boundary during batched mounting', () => {
305 + var ops = [];
306 + class RethrowErrorBoundary extends React.Component {
307 + componentDidCatch(error) {
308 + ops.push('RethrowErrorBoundary componentDidCatch');
309 + throw error;
310 + }
311 + render() {
312 + ops.push('RethrowErrorBoundary render');
313 + return this.props.children;
314 + }
315 + }
316 +
317 + function BrokenRender() {
318 + ops.push('BrokenRender');
319 + throw new Error('Hello');
320 + }
321 +
322 + expect(() => {
323 + ReactNoop.flushSync(() => {
324 + ReactNoop.render(
325 + <RethrowErrorBoundary>Before the storm.</RethrowErrorBoundary>,
326 + );
327 + ReactNoop.render(
328 + <RethrowErrorBoundary>
329 + <BrokenRender />
330 + </RethrowErrorBoundary>,
331 + );
332 + });
333 + }).toThrow('Hello');
334 + expect(ops).toEqual([
335 + 'RethrowErrorBoundary render',
336 + 'BrokenRender',
337 + 'RethrowErrorBoundary componentDidCatch',
338 + ]);
339 + expect(ReactNoop.getChildren()).toEqual([]);
340 + });
341 +
342 + it('applies batched updates regardless despite errors in scheduling', () => {
343 + ReactNoop.render(<span prop="a:1" />);
344 + expect(() => {
345 + ReactNoop.batchedUpdates(() => {
346 + ReactNoop.render(<span prop="a:2" />);
347 + ReactNoop.render(<span prop="a:3" />);
348 + throw new Error('Hello');
349 + });
350 + }).toThrow('Hello');
351 + ReactNoop.flush();
352 + expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
353 + });
354 +
355 + it('applies nested batched updates despite errors in scheduling', () => {
356 + ReactNoop.render(<span prop="a:1" />);
357 + expect(() => {
358 + ReactNoop.batchedUpdates(() => {
359 + ReactNoop.render(<span prop="a:2" />);
360 + ReactNoop.render(<span prop="a:3" />);
361 + ReactNoop.batchedUpdates(() => {
362 + ReactNoop.render(<span prop="a:4" />);
363 + ReactNoop.render(<span prop="a:5" />);
364 + throw new Error('Hello');
365 + });
366 + });
367 + }).toThrow('Hello');
368 + ReactNoop.flush();
369 + expect(ReactNoop.getChildren()).toEqual([span('a:5')]);
370 + });
371 +
372 + it('applies sync updates regardless despite errors in scheduling', () => {
373 + ReactNoop.render(<span prop="a:1" />);
374 + expect(() => {
375 + ReactNoop.flushSync(() => {
376 + ReactNoop.batchedUpdates(() => {
377 + ReactNoop.render(<span prop="a:2" />);
378 + ReactNoop.render(<span prop="a:3" />);
379 + throw new Error('Hello');
380 + });
381 + });
382 + }).toThrow('Hello');
383 + expect(ReactNoop.getChildren()).toEqual([span('a:3')]);
384 + });
385 +
386 + it('can schedule updates after uncaught error in render on mount', () => {
387 + var ops = [];
388 +
389 + function BrokenRender() {
390 + ops.push('BrokenRender');
391 + throw new Error('Hello');
392 + }
393 +
394 + function Foo() {
395 + ops.push('Foo');
396 + return null;
397 + }
398 +
399 + ReactNoop.render(<BrokenRender />);
400 + expect(() => {
401 + ReactNoop.flush();
402 + }).toThrow('Hello');
403 + expect(ops).toEqual(['BrokenRender']);
404 +
405 + ops = [];
406 + ReactNoop.render(<Foo />);
407 + ReactNoop.flush();
408 + expect(ops).toEqual(['Foo']);
409 + });
410 +
411 + it('can schedule updates after uncaught error in render on update', () => {
412 + var ops = [];
413 +
414 + function BrokenRender(props) {
415 + ops.push('BrokenRender');
416 + if (props.throw) {
417 + throw new Error('Hello');
418 + }
419 + return null;
420 + }
421 +
422 + function Foo() {
423 + ops.push('Foo');
424 + return null;
425 + }
426 +
427 + ReactNoop.render(<BrokenRender throw={false} />);
428 + ReactNoop.flush();
429 + ops = [];
430 +
431 + expect(() => {
432 + ReactNoop.render(<BrokenRender throw={true} />);
433 + ReactNoop.flush();
434 + }).toThrow('Hello');
435 + expect(ops).toEqual(['BrokenRender']);
436 +
437 + ops = [];
438 + ReactNoop.render(<Foo />);
439 + ReactNoop.flush();
440 + expect(ops).toEqual(['Foo']);
441 + });
442 +
443 + it('can schedule updates after uncaught error during umounting', () => {
444 + var ops = [];
445 +
446 + class BrokenComponentWillUnmount extends React.Component {
447 + render() {
448 + return <div />;
449 + }
450 + componentWillUnmount() {
451 + throw new Error('Hello');
452 + }
453 + }
454 +
455 + function Foo() {
456 + ops.push('Foo');
457 + return null;
458 + }
459 +
460 + ReactNoop.render(<BrokenComponentWillUnmount />);
461 + ReactNoop.flush();
462 +
463 + expect(() => {
464 + ReactNoop.render(<div />);
465 + ReactNoop.flush();
466 + }).toThrow('Hello');
467 +
468 + ops = [];
469 + ReactNoop.render(<Foo />);
470 + ReactNoop.flush();
471 + expect(ops).toEqual(['Foo']);
472 + });
473 +
474 + it('should not attempt to recover an unmounting error boundary', () => {
475 + class Parent extends React.Component {
476 + componentWillUnmount() {
477 + ReactNoop.yield('Parent componentWillUnmount');
478 + }
479 + render() {
480 + return <Boundary />;
481 + }
482 + }
483 +
484 + class Boundary extends React.Component {
485 + componentDidCatch(e) {
486 + ReactNoop.yield(`Caught error: ${e.message}`);
487 + }
488 + render() {
489 + return <ThrowsOnUnmount />;
490 + }
491 + }
492 +
493 + class ThrowsOnUnmount extends React.Component {
494 + componentWillUnmount() {
495 + ReactNoop.yield('ThrowsOnUnmount componentWillUnmount');
496 + throw new Error('unmount error');
497 + }
498 + render() {
499 + return null;
500 + }
501 + }
502 +
503 + ReactNoop.render(<Parent />);
504 + ReactNoop.flush();
505 + ReactNoop.render(null);
506 + expect(ReactNoop.flush()).toEqual([
507 + // Parent unmounts before the error is thrown.
508 + 'Parent componentWillUnmount',
509 + 'ThrowsOnUnmount componentWillUnmount',
510 + ]);
511 + ReactNoop.render(<Parent />);
512 + });
513 +
514 + it('can unmount an error boundary before it is handled', () => {
515 + let parent;
516 +
517 + class Parent extends React.Component {
518 + state = {step: 0};
519 + render() {
520 + parent = this;
521 + return this.state.step === 0 ? <Boundary /> : null;
522 + }
523 + }
524 +
525 + class Boundary extends React.Component {
526 + componentDidCatch() {}
527 + render() {
528 + return <Child />;
529 + }
530 + }
531 +
532 + class Child extends React.Component {
533 + componentDidUpdate() {
534 + parent.setState({step: 1});
535 + throw new Error('update error');
536 + }
537 + render() {
538 + return null;
539 + }
540 + }
541 +
542 + ReactNoop.render(<Parent />);
543 + ReactNoop.flush();
544 +
545 + ReactNoop.flushSync(() => {
546 + ReactNoop.render(<Parent />);
547 + });
548 + });
549 +
550 + it('continues work on other roots despite caught errors', () => {
551 + class ErrorBoundary extends React.Component {
552 + state = {error: null};
553 + componentDidCatch(error) {
554 + this.setState({error});
555 + }
556 + render() {
557 + if (this.state.error) {
558 + return (
559 + <span prop={`Caught an error: ${this.state.error.message}.`} />
560 + );
561 + }
562 + return this.props.children;
563 + }
564 + }
565 +
566 + function BrokenRender(props) {
567 + throw new Error('Hello');
568 + }
569 +
570 + ReactNoop.renderToRootWithID(
571 + <ErrorBoundary>
572 + <BrokenRender />
573 + </ErrorBoundary>,
574 + 'a',
575 + );
576 + ReactNoop.renderToRootWithID(<span prop="b:1" />, 'b');
577 + ReactNoop.flush();
578 + expect(ReactNoop.getChildren('a')).toEqual([
579 + span('Caught an error: Hello.'),
580 + ]);
581 + expect(ReactNoop.getChildren('b')).toEqual([span('b:1')]);
582 + });
583 +
584 + it('continues work on other roots despite uncaught errors', () => {
585 + function BrokenRender(props) {
586 + throw new Error('Hello');
587 + }
588 +
589 + ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
590 + expect(() => {
591 + ReactNoop.flush();
592 + }).toThrow('Hello');
593 + expect(ReactNoop.getChildren('a')).toEqual([]);
594 +
595 + ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
596 + ReactNoop.renderToRootWithID(<span prop="b:2" />, 'b');
597 + expect(() => {
598 + ReactNoop.flush();
599 + }).toThrow('Hello');
600 +
601 + expect(ReactNoop.getChildren('a')).toEqual([]);
602 + expect(ReactNoop.getChildren('b')).toEqual([span('b:2')]);
603 +
604 + ReactNoop.renderToRootWithID(<span prop="a:3" />, 'a');
605 + ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
606 + expect(() => {
607 + ReactNoop.flush();
608 + }).toThrow('Hello');
609 + expect(ReactNoop.getChildren('a')).toEqual([span('a:3')]);
610 + expect(ReactNoop.getChildren('b')).toEqual([]);
611 +
612 + ReactNoop.renderToRootWithID(<span prop="a:4" />, 'a');
613 + ReactNoop.renderToRootWithID(<BrokenRender />, 'b');
614 + ReactNoop.renderToRootWithID(<span prop="c:4" />, 'c');
615 + expect(() => {
616 + ReactNoop.flush();
617 + }).toThrow('Hello');
618 + expect(ReactNoop.getChildren('a')).toEqual([span('a:4')]);
619 + expect(ReactNoop.getChildren('b')).toEqual([]);
620 + expect(ReactNoop.getChildren('c')).toEqual([span('c:4')]);
621 +
622 + ReactNoop.renderToRootWithID(<span prop="a:5" />, 'a');
623 + ReactNoop.renderToRootWithID(<span prop="b:5" />, 'b');
624 + ReactNoop.renderToRootWithID(<span prop="c:5" />, 'c');
625 + ReactNoop.renderToRootWithID(<span prop="d:5" />, 'd');
626 + ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
627 + expect(() => {
628 + ReactNoop.flush();
629 + }).toThrow('Hello');
630 + expect(ReactNoop.getChildren('a')).toEqual([span('a:5')]);
631 + expect(ReactNoop.getChildren('b')).toEqual([span('b:5')]);
632 + expect(ReactNoop.getChildren('c')).toEqual([span('c:5')]);
633 + expect(ReactNoop.getChildren('d')).toEqual([span('d:5')]);
634 + expect(ReactNoop.getChildren('e')).toEqual([]);
635 +
636 + ReactNoop.renderToRootWithID(<BrokenRender />, 'a');
637 + ReactNoop.renderToRootWithID(<span prop="b:6" />, 'b');
638 + ReactNoop.renderToRootWithID(<BrokenRender />, 'c');
639 + ReactNoop.renderToRootWithID(<span prop="d:6" />, 'd');
640 + ReactNoop.renderToRootWithID(<BrokenRender />, 'e');
641 + ReactNoop.renderToRootWithID(<span prop="f:6" />, 'f');
642 + expect(() => {
643 + ReactNoop.flush();
644 + }).toThrow('Hello');
645 + expect(ReactNoop.getChildren('a')).toEqual([]);
646 + expect(ReactNoop.getChildren('b')).toEqual([span('b:6')]);
647 + expect(ReactNoop.getChildren('c')).toEqual([]);
648 + expect(ReactNoop.getChildren('d')).toEqual([span('d:6')]);
649 + expect(ReactNoop.getChildren('e')).toEqual([]);
650 + expect(ReactNoop.getChildren('f')).toEqual([span('f:6')]);
651 +
652 + ReactNoop.unmountRootWithID('a');
653 + ReactNoop.unmountRootWithID('b');
654 + ReactNoop.unmountRootWithID('c');
655 + ReactNoop.unmountRootWithID('d');
656 + ReactNoop.unmountRootWithID('e');
657 + ReactNoop.unmountRootWithID('f');
658 + ReactNoop.flush();
659 + expect(ReactNoop.getChildren('a')).toEqual(null);
660 + expect(ReactNoop.getChildren('b')).toEqual(null);
661 + expect(ReactNoop.getChildren('c')).toEqual(null);
662 + expect(ReactNoop.getChildren('d')).toEqual(null);
663 + expect(ReactNoop.getChildren('e')).toEqual(null);
664 + expect(ReactNoop.getChildren('f')).toEqual(null);
665 + });
666 +
667 + it('unwinds the context stack correctly on error', () => {
668 + class Provider extends React.Component {
669 + static childContextTypes = {message: PropTypes.string};
670 + static contextTypes = {message: PropTypes.string};
671 + getChildContext() {
672 + return {
673 + message: (this.context.message || '') + this.props.message,
674 + };
675 + }
676 + render() {
677 + return this.props.children;
678 + }
679 + }
680 +
681 + function Connector(props, context) {
682 + return <span prop={context.message} />;
683 + }
684 +
685 + Connector.contextTypes = {
686 + message: PropTypes.string,
687 + };
688 +
689 + function BadRender() {
690 + throw new Error('render error');
691 + }
692 +
693 + class Boundary extends React.Component {
694 + state = {error: null};
695 + componentDidCatch(error) {
696 + this.setState({error});
697 + }
698 + render() {
699 + return (
700 + <Provider message="b">
701 + <Provider message="c">
702 + <Provider message="d">
703 + <Provider message="e">
704 + {!this.state.error && <BadRender />}
705 + </Provider>
706 + </Provider>
707 + </Provider>
708 + </Provider>
709 + );
710 + }
711 + }
712 +
713 + ReactNoop.render(
714 + <Provider message="a">
715 + <Boundary />
716 + <Connector />
717 + </Provider>,
718 + );
719 + ReactNoop.flush();
720 +
721 + // If the context stack does not unwind, span will get 'abcde'
722 + expect(ReactNoop.getChildren()).toEqual([span('a')]);
723 + });
724 +
725 + it('catches reconciler errors in a boundary during mounting', () => {
726 + spyOnDev(console, 'error');
727 +
728 + class ErrorBoundary extends React.Component {
729 + state = {error: null};
730 + componentDidCatch(error) {
731 + this.setState({error});
732 + }
733 + render() {
734 + if (this.state.error) {
735 + return <span prop={this.state.error.message} />;
736 + }
737 + return this.props.children;
738 + }
739 + }
740 + const InvalidType = undefined;
741 + function BrokenRender(props) {
742 + return <InvalidType />;
743 + }
744 +
745 + ReactNoop.render(
746 + <ErrorBoundary>
747 + <BrokenRender />
748 + </ErrorBoundary>,
749 + );
750 + ReactNoop.flush();
751 + expect(ReactNoop.getChildren()).toEqual([
752 + span(
753 + 'Element type is invalid: expected a string (for built-in components) or ' +
754 + 'a class/function (for composite components) but got: undefined.' +
755 + (__DEV__
756 + ? " You likely forgot to export your component from the file it's " +
757 + 'defined in, or you might have mixed up default and named imports.' +
758 + '\n\nCheck the render method of `BrokenRender`.'
759 + : ''),
760 + ),
761 + ]);
762 + if (__DEV__) {
763 + expect(console.error.calls.count()).toBe(1);
764 + expect(console.error.calls.argsFor(0)[0]).toContain(
765 + 'Warning: React.createElement: type is invalid -- expected a string',
766 + );
767 + }
768 + });
769 +
770 + it('catches reconciler errors in a boundary during update', () => {
771 + spyOnDev(console, 'error');
772 +
773 + class ErrorBoundary extends React.Component {
774 + state = {error: null};
775 + componentDidCatch(error) {
776 + this.setState({error});
777 + }
778 + render() {
779 + if (this.state.error) {
780 + return <span prop={this.state.error.message} />;
781 + }
782 + return this.props.children;
783 + }
784 + }
785 +
786 + const InvalidType = undefined;
787 + function BrokenRender(props) {
788 + return props.fail ? <InvalidType /> : <span />;
789 + }
790 +
791 + ReactNoop.render(
792 + <ErrorBoundary>
793 + <BrokenRender fail={false} />
794 + </ErrorBoundary>,
795 + );
796 + ReactNoop.flush();
797 +
798 + ReactNoop.render(
799 + <ErrorBoundary>
800 + <BrokenRender fail={true} />
801 + </ErrorBoundary>,
802 + );
803 + ReactNoop.flush();
804 + expect(ReactNoop.getChildren()).toEqual([
805 + span(
806 + 'Element type is invalid: expected a string (for built-in components) or ' +
807 + 'a class/function (for composite components) but got: undefined.' +
808 + (__DEV__
809 + ? " You likely forgot to export your component from the file it's " +
810 + 'defined in, or you might have mixed up default and named imports.' +
811 + '\n\nCheck the render method of `BrokenRender`.'
812 + : ''),
813 + ),
814 + ]);
815 + if (__DEV__) {
816 + expect(console.error.calls.count()).toBe(1);
817 + expect(console.error.calls.argsFor(0)[0]).toContain(
818 + 'Warning: React.createElement: type is invalid -- expected a string',
819 + );
820 + }
821 + });
822 +
823 + it('recovers from uncaught reconciler errors', () => {
824 + spyOnDev(console, 'error');
825 + const InvalidType = undefined;
826 + ReactNoop.render(<InvalidType />);
827 + expect(() => {
828 + ReactNoop.flush();
829 + }).toThrowError(
830 + 'Element type is invalid: expected a string (for built-in components) or ' +
831 + 'a class/function (for composite components) but got: undefined.' +
832 + (__DEV__
833 + ? " You likely forgot to export your component from the file it's " +
834 + 'defined in, or you might have mixed up default and named imports.'
835 + : ''),
836 + );
837 +
838 + ReactNoop.render(<span prop="hi" />);
839 + ReactNoop.flush();
840 + expect(ReactNoop.getChildren()).toEqual([span('hi')]);
841 + if (__DEV__) {
842 + expect(console.error.calls.count()).toBe(1);
843 + expect(console.error.calls.argsFor(0)[0]).toContain(
844 + 'Warning: React.createElement: type is invalid -- expected a string',
845 + );
846 + }
847 + });
848 +
849 + it('unmounts components with uncaught errors', () => {
850 + const ops = [];
851 + let inst;
852 +
853 + class BrokenRenderAndUnmount extends React.Component {
854 + state = {fail: false};
855 + componentWillUnmount() {
856 + ops.push('BrokenRenderAndUnmount componentWillUnmount');
857 + }
858 + render() {
859 + inst = this;
860 + if (this.state.fail) {
861 + throw new Error('Hello.');
862 + }
863 + return null;
864 + }
865 + }
866 +
867 + class Parent extends React.Component {
868 + componentWillUnmount() {
869 + ops.push('Parent componentWillUnmount [!]');
870 + throw new Error('One does not simply unmount me.');
871 + }
872 + render() {
873 + return this.props.children;
874 + }
875 + }
876 +
877 + ReactNoop.render(
878 + <Parent>
879 + <Parent>
880 + <BrokenRenderAndUnmount />
881 + </Parent>
882 + </Parent>,
883 + );
884 + ReactNoop.flush();
885 +
886 + inst.setState({fail: true});
887 + expect(() => {
888 + ReactNoop.flush();
889 + }).toThrowError('Hello.');
890 +
891 + expect(ops).toEqual([
892 + // Attempt to clean up.
893 + // Errors in parents shouldn't stop children from unmounting.
894 + 'Parent componentWillUnmount [!]',
895 + 'Parent componentWillUnmount [!]',
896 + 'BrokenRenderAndUnmount componentWillUnmount',
897 + ]);
898 + expect(ReactNoop.getChildren()).toEqual([]);
899 + });
900 +
901 + it('does not interrupt unmounting if detaching a ref throws', () => {
902 + var ops = [];
903 +
904 + class Bar extends React.Component {
905 + componentWillUnmount() {
906 + ops.push('Bar unmount');
907 + }
908 + render() {
909 + return <span prop="Bar" />;
910 + }
911 + }
912 +
913 + function barRef(inst) {
914 + if (inst === null) {
915 + ops.push('barRef detach');
916 + throw new Error('Detach error');
917 + }
918 + ops.push('barRef attach');
919 + }
920 +
921 + function Foo(props) {
922 + return <div>{props.hide ? null : <Bar ref={barRef} />}</div>;
923 + }
924 +
925 + ReactNoop.render(<Foo />);
926 + ReactNoop.flush();
927 + expect(ops).toEqual(['barRef attach']);
928 + expect(ReactNoop.getChildren()).toEqual([div(span('Bar'))]);
929 +
930 + ops = [];
931 +
932 + // Unmount
933 + ReactNoop.render(<Foo hide={true} />);
934 + expect(() => ReactNoop.flush()).toThrow('Detach error');
935 + expect(ops).toEqual([
936 + 'barRef detach',
937 + // Bar should unmount even though its ref threw an error while detaching
938 + 'Bar unmount',
939 + ]);
940 + // Because there was an error, entire tree should unmount
941 + expect(ReactNoop.getChildren()).toEqual([]);
942 + });
943 +
944 + it('handles error thrown by host config while working on failed root', () => {
945 + ReactNoop.simulateErrorInHostConfig(() => {
946 + ReactNoop.render(<span />);
947 + expect(() => ReactNoop.flush()).toThrow('Error in host config.');
948 + });
949 + });
950 +
951 + it('handles error thrown by top-level callback', () => {
952 + ReactNoop.render(<div />, () => {
953 + throw new Error('Error!');
954 + });
955 + expect(() => ReactNoop.flush()).toThrow('Error!');
956 + });
957 +
958 + describe('ReactFiberErrorLogger', () => {
959 + function normalizeCodeLocInfo(str) {
960 + return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
961 + }
962 +
963 + it('should log errors that occur during the begin phase', () => {
964 + // Intentionally spy in production too.
965 + spyOn(console, 'error');
966 +
967 + class ErrorThrowingComponent extends React.Component {
968 + componentWillMount() {
969 + const error = new Error('componentWillMount error');
970 + // Note: it's `true` on the Error prototype our test environment.
971 + // That lets us avoid asserting on warnings for each expected error.
972 + // Here we intentionally shadow it to test logging, like in real apps.
973 + error.suppressReactErrorLogging = undefined;
974 + throw error;
975 + }
976 + render() {
977 + return <div />;
978 + }
979 + }
980 +
981 + try {
982 + ReactNoop.render(
983 + <div>
984 + <span>
985 + <ErrorThrowingComponent />
986 + </span>
987 + </div>,
988 + );
989 + ReactNoop.flushDeferredPri();
990 + } catch (error) {}
991 +
992 + expect(console.error.calls.count()).toBe(1);
993 + const errorMessage = console.error.calls.argsFor(0)[0];
994 + if (__DEV__) {
995 + expect(normalizeCodeLocInfo(errorMessage)).toContain(
996 + 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
997 + ' in ErrorThrowingComponent (at **)\n' +
998 + ' in span (at **)\n' +
999 + ' in div (at **)',
1000 + );
1001 + expect(errorMessage).toContain(
1002 + 'Consider adding an error boundary to your tree to customize error handling behavior.',
1003 + );
1004 + } else {
1005 + expect(errorMessage.message).toContain('componentWillMount error');
1006 + }
1007 + });
1008 +
1009 + it('should log errors that occur during the commit phase', () => {
1010 + // Intentionally spy in production too.
1011 + spyOn(console, 'error');
1012 +
1013 + class ErrorThrowingComponent extends React.Component {
1014 + componentDidMount() {
1015 + const error = new Error('componentDidMount error');
1016 + // Note: it's `true` on the Error prototype our test environment.
1017 + // That lets us avoid asserting on warnings for each expected error.
1018 + // Here we intentionally shadow it to test logging, like in real apps.
1019 + error.suppressReactErrorLogging = undefined;
1020 + throw error;
1021 + }
1022 + render() {
1023 + return <div />;
1024 + }
1025 + }
1026 +
1027 + try {
1028 + ReactNoop.render(
1029 + <div>
1030 + <span>
1031 + <ErrorThrowingComponent />
1032 + </span>
1033 + </div>,
1034 + );
1035 + ReactNoop.flushDeferredPri();
1036 + } catch (error) {}
1037 +
1038 + expect(console.error.calls.count()).toBe(1);
1039 + const errorMessage = console.error.calls.argsFor(0)[0];
1040 + if (__DEV__) {
1041 + expect(normalizeCodeLocInfo(errorMessage)).toContain(
1042 + 'The above error occurred in the <ErrorThrowingComponent> component:\n' +
1043 + ' in ErrorThrowingComponent (at **)\n' +
1044 + ' in span (at **)\n' +
1045 + ' in div (at **)',
1046 + );
1047 + expect(errorMessage).toContain(
1048 + 'Consider adding an error boundary to your tree to customize error handling behavior.',
1049 + );
1050 + } else {
1051 + expect(errorMessage.message).toBe('componentDidMount error');
1052 + }
1053 + });
1054 +
1055 + it('should ignore errors thrown in log method to prevent cycle', () => {
1056 + jest.resetModules();
1057 + jest.mock('../ReactFiberErrorLogger');
1058 + React = require('react');
1059 + ReactNoop = require('react-noop-renderer');
1060 + // Intentionally spy in production too.
1061 + spyOn(console, 'error');
1062 +
1063 + class ErrorThrowingComponent extends React.Component {
1064 + render() {
1065 + throw new Error('render error');
1066 + }
1067 + }
1068 +
1069 + const logCapturedErrorCalls = [];
1070 +
1071 + const ReactFiberErrorLogger = require('../ReactFiberErrorLogger');
1072 + ReactFiberErrorLogger.logCapturedError.mockImplementation(
1073 + capturedError => {
1074 + logCapturedErrorCalls.push(capturedError);
1075 + const error = new Error('logCapturedError error');
1076 + // Note: it's `true` on the Error prototype our test environment.
1077 + // That lets us avoid asserting on warnings for each expected error.
1078 + // Here we intentionally shadow it to test logging, like in real apps.
1079 + error.suppressReactErrorLogging = undefined;
1080 + throw error;
1081 + },
1082 + );
1083 +
1084 + try {
1085 + ReactNoop.render(
1086 + <div>
1087 + <span>
1088 + <ErrorThrowingComponent />
1089 + </span>
1090 + </div>,
1091 + );
1092 + ReactNoop.flushDeferredPri();
1093 + } catch (error) {}
1094 +
1095 + expect(logCapturedErrorCalls.length).toBe(1);
1096 +
1097 + // The error thrown in logCapturedError should also be logged
1098 + expect(console.error.calls.count()).toBe(1);
1099 + expect(console.error.calls.argsFor(0)[0].message).toContain(
1100 + 'logCapturedError error',
1101 + );
1102 + });
1103 +
1104 + it('should relay info about error boundary and retry attempts if applicable', () => {
1105 + // Intentionally spy in production too.
1106 + spyOn(console, 'error');
1107 +
1108 + class ParentComponent extends React.Component {
1109 + render() {
1110 + return <ErrorBoundaryComponent />;
1111 + }
1112 + }
1113 +
1114 + let handleErrorCalls = [];
1115 + let renderAttempts = 0;
1116 +
1117 + class ErrorBoundaryComponent extends React.Component {
1118 + componentDidCatch(error) {
1119 + handleErrorCalls.push(error);
1120 + this.setState({}); // Render again
1121 + }
1122 + render() {
1123 + return <ErrorThrowingComponent />;
1124 + }
1125 + }
1126 +
1127 + class ErrorThrowingComponent extends React.Component {
1128 + componentDidMount() {
1129 + const error = new Error('componentDidMount error');
1130 + // Note: it's `true` on the Error prototype our test environment.
1131 + // That lets us avoid asserting on warnings for each expected error.
1132 + // Here we intentionally shadow it to test logging, like in real apps.
1133 + error.suppressReactErrorLogging = undefined;
1134 + throw error;
1135 + }
1136 + render() {
1137 + renderAttempts++;
1138 + return <div />;
1139 + }
1140 + }
1141 +
1142 + try {
1143 + ReactNoop.render(<ParentComponent />);
1144 + ReactNoop.flush();
1145 + } catch (error) {}
1146 +
1147 + expect(renderAttempts).toBe(2);
1148 + expect(handleErrorCalls.length).toBe(1);
1149 + expect(console.error.calls.count()).toBe(2);
1150 + if (__DEV__) {
1151 + expect(console.error.calls.argsFor(0)[0]).toContain(
1152 + 'The above error occurred in the <ErrorThrowingComponent> component:',
1153 + );
1154 + expect(console.error.calls.argsFor(0)[0]).toContain(
1155 + 'React will try to recreate this component tree from scratch ' +
1156 + 'using the error boundary you provided, ErrorBoundaryComponent.',
1157 + );
1158 + expect(console.error.calls.argsFor(1)[0]).toContain(
1159 + 'The above error occurred in the <ErrorThrowingComponent> component:',
1160 + );
1161 + expect(console.error.calls.argsFor(1)[0]).toContain(
1162 + 'This error was initially handled by the error boundary ErrorBoundaryComponent.\n' +
1163 + 'Recreating the tree from scratch failed so React will unmount the tree.',
1164 + );
1165 + }
1166 + });
1167 + });
1168 +
1169 + it('resets instance variables before unmounting failed node', () => {
1170 + class ErrorBoundary extends React.Component {
1171 + state = {error: null};
1172 + componentDidCatch(error) {
1173 + this.setState({error});
1174 + }
1175 + render() {
1176 + return this.state.error ? null : this.props.children;
1177 + }
1178 + }
1179 + class Foo extends React.Component {
1180 + state = {step: 0};
1181 + componentDidMount() {
1182 + this.setState({step: 1});
1183 + }
1184 + componentWillUnmount() {
1185 + ReactNoop.yield('componentWillUnmount: ' + this.state.step);
1186 + }
1187 + render() {
1188 + ReactNoop.yield('render: ' + this.state.step);
1189 + if (this.state.step > 0) {
1190 + throw new Error('oops');
1191 + }
1192 + return null;
1193 + }
1194 + }
1195 +
1196 + ReactNoop.render(
1197 + <ErrorBoundary>
1198 + <Foo />
1199 + </ErrorBoundary>,
1200 + );
1201 + expect(ReactNoop.flush()).toEqual([
1202 + 'render: 0',
1203 + 'render: 1',
1204 + 'componentWillUnmount: 0',
1205 + ]);
1206 + });
1207 +});
packages/react-reconciler/src/__tests__/ReactIncrementalPerf-test.internal.js renamed
packages/react-reconciler/src/__tests__/ReactPersistent-test.internal.js renamed
packages/react-reconciler/src/__tests__/__snapshots__/ReactIncrementalPerf-test.internal.js.snap renamed
packages/react-rt-renderer/src/__tests__/ReactNativeRT-test.internal.js renamed
packages/react/src/__tests__/ReactAsyncClassComponent-test.internal.js renamed
packages/react/src/__tests__/ReactJSXElementValidator-test.internal.js new
+104
@@ -0,0 +1,104 @@
1 +/**
2 + * Copyright (c) 2013-present, Facebook, Inc.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @emails react-core
8 + */
9 +
10 +'use strict';
11 +
12 +var React;
13 +var ReactTestUtils;
14 +
15 +describe('ReactJSXElementValidator', () => {
16 + beforeEach(() => {
17 + jest.resetModules();
18 +
19 + const ReactFeatureFlags = require('shared/ReactFeatureFlags');
20 + ReactFeatureFlags.enableReactFragment = true;
21 +
22 + React = require('react');
23 + ReactTestUtils = require('react-dom/test-utils');
24 + });
25 +
26 + it('warns for fragments with illegal attributes', () => {
27 + spyOnDev(console, 'error');
28 +
29 + class Foo extends React.Component {
30 + render() {
31 + return (
32 + <React.Fragment a={1} b={2}>
33 + hello
34 + </React.Fragment>
35 + );
36 + }
37 + }
38 +
39 + ReactTestUtils.renderIntoDocument(<Foo />);
40 +
41 + if (__DEV__) {
42 + expect(console.error.calls.count()).toBe(1);
43 + expect(console.error.calls.argsFor(0)[0]).toContain('Invalid prop `');
44 + expect(console.error.calls.argsFor(0)[0]).toContain(
45 + '` supplied to `React.Fragment`. React.Fragment ' +
46 + 'can only have `key` and `children` props.',
47 + );
48 + }
49 + });
50 +
51 + it('warns for fragments with refs', () => {
52 + spyOnDev(console, 'error');
53 +
54 + class Foo extends React.Component {
55 + render() {
56 + return (
57 + <React.Fragment
58 + ref={bar => {
59 + this.foo = bar;
60 + }}>
61 + hello
62 + </React.Fragment>
63 + );
64 + }
65 + }
66 +
67 + ReactTestUtils.renderIntoDocument(<Foo />);
68 +
69 + if (__DEV__) {
70 + expect(console.error.calls.count()).toBe(1);
71 + expect(console.error.calls.argsFor(0)[0]).toContain(
72 + 'Invalid attribute `ref` supplied to `React.Fragment`.',
73 + );
74 + }
75 + });
76 +
77 + it('does not warn for fragments of multiple elements without keys', () => {
78 + ReactTestUtils.renderIntoDocument(
79 + <React.Fragment>
80 + <span>1</span>
81 + <span>2</span>
82 + </React.Fragment>,
83 + );
84 + });
85 +
86 + it('warns for fragments of multiple elements with same key', () => {
87 + spyOnDev(console, 'error');
88 +
89 + ReactTestUtils.renderIntoDocument(
90 + <React.Fragment>
91 + <span key="a">1</span>
92 + <span key="a">2</span>
93 + <span key="b">3</span>
94 + </React.Fragment>,
95 + );
96 +
97 + if (__DEV__) {
98 + expect(console.error.calls.count()).toBe(1);
99 + expect(console.error.calls.argsFor(0)[0]).toContain(
100 + 'Encountered two children with the same key, `a`.',
101 + );
102 + }
103 + });
104 +});
packages/react/src/__tests__/ReactJSXElementValidator-test.js
-82
@@ -27,9 +27,6 @@ describe('ReactJSXElementValidator', () => {
27 beforeEach(() => {
28 jest.resetModules();
29
30 - const ReactFeatureFlags = require('shared/ReactFeatureFlags');
31 - ReactFeatureFlags.enableReactFragment = true;
32 -
30 PropTypes = require('prop-types');
31 React = require('react');
32 ReactDOM = require('react-dom');
@@ -92,57 +89,6 @@ describe('ReactJSXElementValidator', () => {
89 }
90 });
91
95 - it('warns for fragments with illegal attributes', () => {
96 - spyOnDev(console, 'error');
97 -
98 - class Foo extends React.Component {
99 - render() {
100 - return (
101 - <React.Fragment a={1} b={2}>
102 - hello
103 - </React.Fragment>
104 - );
105 - }
106 - }
107 -
108 - ReactTestUtils.renderIntoDocument(<Foo />);
109 -
110 - if (__DEV__) {
111 - expect(console.error.calls.count()).toBe(1);
112 - expect(console.error.calls.argsFor(0)[0]).toContain('Invalid prop `');
113 - expect(console.error.calls.argsFor(0)[0]).toContain(
114 - '` supplied to `React.Fragment`. React.Fragment ' +
115 - 'can only have `key` and `children` props.',
116 - );
117 - }
118 - });
119 -
120 - it('warns for fragments with refs', () => {
121 - spyOnDev(console, 'error');
122 -
123 - class Foo extends React.Component {
124 - render() {
125 - return (
126 - <React.Fragment
127 - ref={bar => {
128 - this.foo = bar;
129 - }}>
130 - hello
131 - </React.Fragment>
132 - );
133 - }
134 - }
135 -
136 - ReactTestUtils.renderIntoDocument(<Foo />);
137 -
138 - if (__DEV__) {
139 - expect(console.error.calls.count()).toBe(1);
140 - expect(console.error.calls.argsFor(0)[0]).toContain(
141 - 'Invalid attribute `ref` supplied to `React.Fragment`.',
142 - );
143 - }
144 - });
145 -
92 it('warns for keys for iterables of elements in rest args', () => {
93 spyOnDev(console, 'error');
94
@@ -168,34 +114,6 @@ describe('ReactJSXElementValidator', () => {
114 }
115 });
116
171 - it('does not warn for fragments of multiple elements without keys', () => {
172 - ReactTestUtils.renderIntoDocument(
173 - <React.Fragment>
174 - <span>1</span>
175 - <span>2</span>
176 - </React.Fragment>,
177 - );
178 - });
179 -
180 - it('warns for fragments of multiple elements with same key', () => {
181 - spyOnDev(console, 'error');
182 -
183 - ReactTestUtils.renderIntoDocument(
184 - <React.Fragment>
185 - <span key="a">1</span>
186 - <span key="a">2</span>
187 - <span key="b">3</span>
188 - </React.Fragment>,
189 - );
190 -
191 - if (__DEV__) {
192 - expect(console.error.calls.count()).toBe(1);
193 - expect(console.error.calls.argsFor(0)[0]).toContain(
194 - 'Encountered two children with the same key, `a`.',
195 - );
196 - }
197 - });
198 -
117 it('does not warn for arrays of elements with keys', () => {
118 ReactTestUtils.renderIntoDocument(
119 <Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>,
packages/shared/__tests__/ReactErrorUtils-test.internal.js renamed
packages/shared/__tests__/reactProdInvariant-test.internal.js renamed
scripts/circleci/test_entry_point.sh
+15 -15
@@ -4,29 +4,29 @@ set -e
4
5 COMMANDS_TO_RUN=()
6
7 -if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
8 - COMMANDS_TO_RUN+=('./scripts/circleci/test_coverage.sh')
9 -fi
10 -
11 -if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
12 - COMMANDS_TO_RUN+=('yarn test-prod --runInBand')
13 -fi
14 -
15 -if [ $((3 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
16 - COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
17 -fi
18 -
19 -# These seem out of order but extract-errors must be run after jest.
7 if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
8 COMMANDS_TO_RUN+=('node ./scripts/prettier/index')
9 COMMANDS_TO_RUN+=('node ./scripts/tasks/flow')
10 + COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
11 COMMANDS_TO_RUN+=('yarn test --runInBand')
24 - COMMANDS_TO_RUN+=('./scripts/circleci/build.sh')
12 COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh')
13 COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh')
14 COMMANDS_TO_RUN+=('./scripts/circleci/test_print_warnings.sh')
15 COMMANDS_TO_RUN+=('./scripts/circleci/track_stats.sh')
29 - # COMMANDS_TO_RUN+=('./scripts/circleci/bench.sh')
16 +fi
17 +
18 +if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
19 + COMMANDS_TO_RUN+=('yarn test-prod --runInBand')
20 +fi
21 +
22 +if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
23 + COMMANDS_TO_RUN+=('./scripts/circleci/build.sh')
24 + COMMANDS_TO_RUN+=('yarn test-build --runInBand')
25 + COMMANDS_TO_RUN+=('yarn test-build-prod --runInBand')
26 +fi
27 +
28 +if [ $((3 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
29 + COMMANDS_TO_RUN+=('./scripts/circleci/test_coverage.sh')
30 fi
31
32 RETURN_CODES=()
scripts/eslint-rules/__tests__/no-primitive-constructors-test.internal.js renamed
scripts/eslint-rules/__tests__/warning-and-invariant-args-test.internal.js renamed
scripts/jest/config.build.js new
+32
@@ -0,0 +1,32 @@
1 +'use strict';
2 +
3 +const {readdirSync, statSync} = require('fs');
4 +const {join} = require('path');
5 +const sourceConfig = require('./config.source');
6 +
7 +// Find all folders in packages/* with package.json
8 +const packagesRoot = join(__dirname, '..', '..', 'packages');
9 +const packages = readdirSync(packagesRoot).filter(dir => {
10 + if (dir.charAt(0) === '.') {
11 + return false;
12 + }
13 + const packagePath = join(packagesRoot, dir, 'package.json');
14 + return statSync(packagePath).isFile();
15 +});
16 +// Create a module map to point React packages to the build output
17 +const moduleNameMapper = {};
18 +packages.forEach(name => {
19 + // Root entry point
20 + moduleNameMapper[`^${name}$`] = `<rootDir>/build/packages/${name}`;
21 + // Named entry points
22 + moduleNameMapper[`^${name}/(.*)$`] = `<rootDir>/build/packages/${name}/$1`;
23 +});
24 +
25 +module.exports = Object.assign({}, sourceConfig, {
26 + // Redirect imports to the compiled bundles
27 + moduleNameMapper,
28 + // Don't run bundle tests on blacklisted -test.internal.* files
29 + testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'],
30 + // Exclude the build output from transforms
31 + transformIgnorePatterns: ['/node_modules/', '<rootDir>/build/'],
32 +});
scripts/jest/config.source.js new
+20
@@ -0,0 +1,20 @@
1 +'use strict';
2 +
3 +module.exports = {
4 + modulePathIgnorePatterns: [
5 + '<rootDir>/scripts/rollup/shims/',
6 + '<rootDir>/scripts/bench/',
7 + ],
8 + transform: {
9 + '.*': require.resolve('./preprocessor.js'),
10 + },
11 + setupFiles: [require.resolve('./setupEnvironment.js')],
12 + setupTestFrameworkScriptFile: require.resolve('./setupTests.js'),
13 + // Only include files directly in __tests__, not in nested folders.
14 + testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$',
15 + moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'],
16 + rootDir: process.cwd(),
17 + roots: ['<rootDir>/packages', '<rootDir>/scripts'],
18 + collectCoverageFrom: ['packages/**/*.js'],
19 + timers: 'fake',
20 +};
scripts/jest/preprocessor.js
+1 -1
@@ -5,7 +5,7 @@ var path = require('path');
5 var babel = require('babel-core');
6 var coffee = require('coffee-script');
7
8 -var tsPreprocessor = require('./ts-preprocessor');
8 +var tsPreprocessor = require('./typescript/preprocessor');
9 var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction');
10
11 // Use require.resolve to be resilient to file moves, npm updates, etc
scripts/jest/setupEnvironment.js renamed
scripts/jest/setupTests.js renamed
+6 -1
@@ -3,7 +3,7 @@
3 if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
4 // Inside the class equivalence tester, we have a custom environment, let's
5 // require that instead.
6 - require('./setupSpecEquivalenceReporter.js');
6 + require('./spec-equivalence-reporter/setupTests.js');
7 } else {
8 var env = jasmine.getEnv();
9 var errorMap = require('../error-codes/codes.json');
@@ -75,6 +75,11 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
75 };
76 const OriginalError = global.Error;
77 const ErrorProxy = new Proxy(OriginalError, {
78 + apply(target, thisArg, argumentsList) {
79 + const error = Reflect.apply(target, thisArg, argumentsList);
80 + error.message = decodeErrorMessage(error.message);
81 + return error;
82 + },
83 construct(target, argumentsList, newTarget) {
84 const error = Reflect.construct(target, argumentsList, newTarget);
85 error.message = decodeErrorMessage(error.message);
scripts/jest/spec-equivalence-reporter/setupTests.js renamed
scripts/jest/typescript/jest.d.ts renamed
scripts/jest/typescript/preprocessor.js renamed
scripts/rollup/bundles.js
+15 -2
@@ -178,14 +178,27 @@ const bundles = [
178 externals: ['react'],
179 },
180
181 - /******* React Noop Renderer (used only for fixtures/fiber-debugger) *******/
181 + /******* React Noop Renderer (used for tests) *******/
182 {
183 label: 'noop',
184 - bundleTypes: [NODE_DEV],
184 + bundleTypes: [NODE_DEV, NODE_PROD],
185 moduleType: RENDERER,
186 entry: 'react-noop-renderer',
187 global: 'ReactNoopRenderer',
188 externals: ['react', 'expect'],
189 + // React Noop uses generators. However GCC currently
190 + // breaks when we attempt to use them in the output.
191 + // So we precompile them with regenerator, and include
192 + // it as a runtime dependency of React Noop. In practice
193 + // this isn't an issue because React Noop is only used
194 + // in our tests. We wouldn't want to do this for any
195 + // public package though.
196 + babel: opts =>
197 + Object.assign({}, opts, {
198 + plugins: opts.plugins.concat([
199 + require.resolve('babel-plugin-transform-regenerator'),
200 + ]),
201 + }),
202 },
203
204 /******* React Reconciler *******/
scripts/rollup/results.json
+49 -45
@@ -9,12 +9,12 @@
9 "gzip": 2820
10 },
11 "react.development.js (NODE_DEV)": {
12 - "size": 45226,
13 - "gzip": 12653
12 + "size": 45228,
13 + "gzip": 12654
14 },
15 "react.production.min.js (NODE_PROD)": {
16 - "size": 5398,
17 - "gzip": 2384
16 + "size": 5399,
17 + "gzip": 2385
18 },
19 "React-dev.js (FB_DEV)": {
20 "size": 44902,
@@ -33,12 +33,12 @@
33 "gzip": 30731
34 },
35 "react-dom.development.js (NODE_DEV)": {
36 - "size": 550734,
37 - "gzip": 128057
36 + "size": 550933,
37 + "gzip": 128104
38 },
39 "react-dom.production.min.js (NODE_PROD)": {
40 - "size": 93027,
41 - "gzip": 29952
40 + "size": 93102,
41 + "gzip": 29993
42 },
43 "ReactDOM-dev.js (FB_DEV)": {
44 "size": 575937,
@@ -49,12 +49,12 @@
49 "gzip": 51733
50 },
51 "react-dom-test-utils.development.js (NODE_DEV)": {
52 - "size": 36191,
53 - "gzip": 10293
52 + "size": 36193,
53 + "gzip": 10294
54 },
55 "react-dom-test-utils.production.min.js (NODE_PROD)": {
56 - "size": 10188,
57 - "gzip": 3835
56 + "size": 10189,
57 + "gzip": 3836
58 },
59 "ReactTestUtils-dev.js (FB_DEV)": {
60 "size": 36913,
@@ -69,12 +69,12 @@
69 "gzip": 3901
70 },
71 "react-dom-unstable-native-dependencies.development.js (NODE_DEV)": {
72 - "size": 58979,
73 - "gzip": 15256
72 + "size": 58981,
73 + "gzip": 15257
74 },
75 "react-dom-unstable-native-dependencies.production.min.js (NODE_PROD)": {
76 - "size": 10909,
77 - "gzip": 3772
76 + "size": 10910,
77 + "gzip": 3773
78 },
79 "ReactDOMUnstableNativeDependencies-dev.js (FB_DEV)": {
80 "size": 60056,
@@ -93,12 +93,12 @@
93 "gzip": 5865
94 },
95 "react-dom-server.browser.development.js (NODE_DEV)": {
96 - "size": 83887,
97 - "gzip": 22828
96 + "size": 83924,
97 + "gzip": 22831
98 },
99 "react-dom-server.browser.production.min.js (NODE_PROD)": {
100 - "size": 14263,
101 - "gzip": 5748
100 + "size": 14235,
101 + "gzip": 5744
102 },
103 "ReactDOMServer-dev.js (FB_DEV)": {
104 "size": 87596,
@@ -109,11 +109,11 @@
109 "gzip": 8156
110 },
111 "react-dom-server.node.development.js (NODE_DEV)": {
112 - "size": 85855,
113 - "gzip": 23337
112 + "size": 85892,
113 + "gzip": 23339
114 },
115 "react-dom-server.node.production.min.js (NODE_PROD)": {
116 - "size": 15088,
116 + "size": 15060,
117 "gzip": 6052
118 },
119 "react-art.development.js (UMD_DEV)": {
@@ -125,12 +125,12 @@
125 "gzip": 25785
126 },
127 "react-art.development.js (NODE_DEV)": {
128 - "size": 283633,
129 - "gzip": 60512
128 + "size": 283832,
129 + "gzip": 60561
130 },
131 "react-art.production.min.js (NODE_PROD)": {
132 - "size": 47631,
133 - "gzip": 14903
132 + "size": 47706,
133 + "gzip": 14932
134 },
135 "ReactART-dev.js (FB_DEV)": {
136 "size": 294853,
@@ -165,48 +165,48 @@
165 "gzip": 21823
166 },
167 "react-test-renderer.development.js (NODE_DEV)": {
168 - "size": 280201,
169 - "gzip": 59318
168 + "size": 280400,
169 + "gzip": 59367
170 },
171 "react-test-renderer.production.min.js (NODE_PROD)": {
172 - "size": 45987,
173 - "gzip": 14196
172 + "size": 46062,
173 + "gzip": 14232
174 },
175 "ReactTestRenderer-dev.js (FB_DEV)": {
176 "size": 291517,
177 "gzip": 61172
178 },
179 "react-test-renderer-shallow.development.js (NODE_DEV)": {
180 - "size": 9686,
181 - "gzip": 2628
180 + "size": 10233,
181 + "gzip": 2761
182 },
183 "react-test-renderer-shallow.production.min.js (NODE_PROD)": {
184 - "size": 4865,
185 - "gzip": 1795
184 + "size": 5159,
185 + "gzip": 1880
186 },
187 "ReactShallowRenderer-dev.js (FB_DEV)": {
188 "size": 9917,
189 "gzip": 2596
190 },
191 "react-noop-renderer.development.js (NODE_DEV)": {
192 - "size": 276892,
193 - "gzip": 58307
192 + "size": 277870,
193 + "gzip": 58556
194 },
195 "react-reconciler.development.js (NODE_DEV)": {
196 - "size": 262062,
197 - "gzip": 54933
196 + "size": 262259,
197 + "gzip": 54981
198 },
199 "react-reconciler.production.min.js (NODE_PROD)": {
200 - "size": 39304,
201 - "gzip": 12212
200 + "size": 39382,
201 + "gzip": 12251
202 },
203 "react-call-return.development.js (NODE_DEV)": {
204 - "size": 2463,
205 - "gzip": 918
204 + "size": 2465,
205 + "gzip": 919
206 },
207 "react-call-return.production.min.js (NODE_PROD)": {
208 - "size": 880,
209 - "gzip": 499
208 + "size": 881,
209 + "gzip": 500
210 },
211 "react-dom-test-utils.development.js (UMD_DEV)": {
212 "size": 41456,
@@ -215,6 +215,10 @@
215 "react-dom-test-utils.production.min.js (UMD_PROD)": {
216 "size": 10617,
217 "gzip": 3917
218 + },
219 + "react-noop-renderer.production.min.js (NODE_PROD)": {
220 + "size": 45070,
221 + "gzip": 14103
222 }
223 }
224 }
\ No newline at end of file
scripts/rollup/wrappers.js
+17
@@ -53,6 +53,15 @@ ${license}
53
54 'use strict';
55
56 +${
57 + globalName === 'ReactNoopRenderer'
58 + ? // React Noop needs regenerator runtime because it uses
59 + // generators but GCC doesn't handle them in the output.
60 + // So we use Babel for them.
61 + `const regeneratorRuntime = require("regenerator-runtime");`
62 + : ``
63 + }
64 +
65 if (process.env.NODE_ENV !== "production") {
66 (function() {
67 ${source}
@@ -67,6 +76,14 @@ ${source}
76 *
77 ${license}
78 */
79 +${
80 + globalName === 'ReactNoopRenderer'
81 + ? // React Noop needs regenerator runtime because it uses
82 + // generators but GCC doesn't handle them in the output.
83 + // So we use Babel for them.
84 + `const regeneratorRuntime = require("regenerator-runtime");`
85 + : ``
86 + }
87 ${source}`;
88 },
89
yarn.lock
+39 -1
@@ -757,6 +757,12 @@ babel-plugin-transform-react-jsx@^6.24.1:
757 babel-plugin-syntax-jsx "^6.8.0"
758 babel-runtime "^6.22.0"
759
760 +babel-plugin-transform-regenerator@^6.26.0:
761 + version "6.26.0"
762 + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
763 + dependencies:
764 + regenerator-transform "^0.10.0"
765 +
766 babel-plugin-transform-strict-mode@^6.24.1:
767 version "6.24.1"
768 resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758"
@@ -818,6 +824,13 @@ babel-register@^6.24.1:
824 mkdirp "^0.5.1"
825 source-map-support "^0.4.2"
826
827 +babel-runtime@^6.18.0, babel-runtime@^6.26.0:
828 + version "6.26.0"
829 + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
830 + dependencies:
831 + core-js "^2.4.0"
832 + regenerator-runtime "^0.11.0"
833 +
834 babel-runtime@^6.22.0:
835 version "6.23.0"
836 resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.23.0.tgz#0a9489f144de70efb3ce4300accdb329e2fc543b"
@@ -858,6 +871,15 @@ babel-types@^6.18.0, babel-types@^6.23.0, babel-types@^6.24.1:
871 lodash "^4.2.0"
872 to-fast-properties "^1.0.1"
873
874 +babel-types@^6.19.0:
875 + version "6.26.0"
876 + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
877 + dependencies:
878 + babel-runtime "^6.26.0"
879 + esutils "^2.0.2"
880 + lodash "^4.17.4"
881 + to-fast-properties "^1.0.3"
882 +
883 babel@^5.4.7:
884 version "5.8.38"
885 resolved "https://registry.yarnpkg.com/babel/-/babel-5.8.38.tgz#dfb087c22894917c576fb67ce9cf328d458629fb"
@@ -3202,7 +3224,7 @@ lodash@^3.10.0, lodash@^3.2.0, lodash@^3.9.3:
3224 version "3.10.1"
3225 resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
3226
3205 -lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.2.0, lodash@^4.3.0:
3227 +lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
3228 version "4.17.4"
3229 resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
3230
@@ -3906,6 +3928,18 @@ regenerator-runtime@^0.10.0:
3928 version "0.10.4"
3929 resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93"
3930
3931 +regenerator-runtime@^0.11.0:
3932 + version "0.11.0"
3933 + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
3934 +
3935 +regenerator-transform@^0.10.0:
3936 + version "0.10.1"
3937 + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
3938 + dependencies:
3939 + babel-runtime "^6.18.0"
3940 + babel-types "^6.19.0"
3941 + private "^0.1.6"
3942 +
3943 regenerator@0.8.40:
3944 version "0.8.40"
3945 resolved "https://registry.yarnpkg.com/regenerator/-/regenerator-0.8.40.tgz#a0e457c58ebdbae575c9f8cd75127e93756435d8"
@@ -4592,6 +4626,10 @@ to-fast-properties@^1.0.0, to-fast-properties@^1.0.1:
4626 version "1.0.2"
4627 resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320"
4628
4629 +to-fast-properties@^1.0.3:
4630 + version "1.0.3"
4631 + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
4632 +
4633 tough-cookie@^2.3.2, tough-cookie@~2.3.0:
4634 version "2.3.2"
4635 resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"