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).
This commit is contained in:
Dan Abramov
2017-11-23 17:44:58 +00:00
committed by GitHub
parent e949d57508
commit fa7a97fc46
65 changed files with 5086 additions and 3686 deletions
+6 -31
View File
@@ -35,6 +35,7 @@
"babel-plugin-transform-es3-property-literals": "^6.5.0",
"babel-plugin-transform-object-rest-spread": "^6.6.5",
"babel-plugin-transform-react-jsx-source": "^6.8.0",
"babel-plugin-transform-regenerator": "^6.26.0",
"babel-preset-react": "^6.5.0",
"babel-traverse": "^6.9.0",
"babylon": "6.15.0",
@@ -103,40 +104,14 @@
"linc": "node ./scripts/tasks/linc.js",
"lint": "node ./scripts/tasks/eslint.js",
"postinstall": "node node_modules/fbjs-scripts/node/check-dev-engines.js package.json",
"test": "cross-env NODE_ENV=development jest",
"test-prod": "cross-env NODE_ENV=production jest",
"test": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.source.js",
"test-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.source.js",
"test-prod-build": "yarn test-build-prod",
"test-build": "cross-env NODE_ENV=development jest --config ./scripts/jest/config.build.js",
"test-build-prod": "cross-env NODE_ENV=production jest --config ./scripts/jest/config.build.js",
"flow": "node ./scripts/tasks/flow.js",
"prettier": "node ./scripts/prettier/index.js write-changed",
"prettier-all": "node ./scripts/prettier/index.js write",
"version-check": "node ./scripts/tasks/version-check.js"
},
"jest": {
"modulePathIgnorePatterns": [
"<rootDir>/scripts/rollup/shims/",
"<rootDir>/scripts/bench/"
],
"transform": {
".*": "./scripts/jest/preprocessor.js"
},
"setupFiles": [
"./scripts/jest/environment.js"
],
"setupTestFrameworkScriptFile": "./scripts/jest/test-framework-setup.js",
"testRegex": "/__tests__/.*(\\.js|\\.coffee|[^d]\\.ts)$",
"moduleFileExtensions": [
"js",
"json",
"node",
"coffee",
"ts"
],
"roots": [
"<rootDir>/packages",
"<rootDir>/scripts"
],
"collectCoverageFrom": [
"packages/**/*.js"
],
"timers": "fake"
}
}
+24 -9
View File
@@ -1709,15 +1709,25 @@ describe('ReactDOMComponent', () => {
it('should warn about incorrect casing on event handlers (ssr)', () => {
spyOnDev(console, 'error');
ReactDOMServer.renderToString(
React.createElement('input', {type: 'text', onclick: '1'}),
React.createElement('input', {type: 'text', oninput: '1'}),
);
ReactDOMServer.renderToString(
React.createElement('input', {type: 'text', onKeydown: '1'}),
);
if (__DEV__) {
expect(console.error.calls.count()).toBe(2);
expect(console.error.calls.argsFor(0)[0]).toContain('onClick');
expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown');
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Invalid event handler property `oninput`. ' +
'React events use the camelCase naming convention, ' +
// Note: we don't know the right event name so we
// use a generic one (onClick) as a suggestion.
// This is because we don't bundle the event system
// on the server.
'for example `onClick`.',
);
// We can't warn for `onKeydown` on the server because
// there is no way tell if this is a valid event or not
// without access to the event system (which we don't bundle).
}
});
@@ -1735,14 +1745,14 @@ describe('ReactDOMComponent', () => {
it('should warn about incorrect casing on event handlers', () => {
spyOnDev(console, 'error');
ReactTestUtils.renderIntoDocument(
React.createElement('input', {type: 'text', onclick: '1'}),
React.createElement('input', {type: 'text', oninput: '1'}),
);
ReactTestUtils.renderIntoDocument(
React.createElement('input', {type: 'text', onKeydown: '1'}),
);
if (__DEV__) {
expect(console.error.calls.count()).toBe(2);
expect(console.error.calls.argsFor(0)[0]).toContain('onClick');
expect(console.error.calls.argsFor(0)[0]).toContain('onInput');
expect(console.error.calls.argsFor(1)[0]).toContain('onKeyDown');
}
});
@@ -1860,15 +1870,20 @@ describe('ReactDOMComponent', () => {
it('gives source code refs for unknown prop warning (ssr)', () => {
spyOnDev(console, 'error');
ReactDOMServer.renderToString(<div class="paladin" />);
ReactDOMServer.renderToString(<input type="text" onclick="1" />);
ReactDOMServer.renderToString(<input type="text" oninput="1" />);
if (__DEV__) {
expect(console.error.calls.count()).toBe(2);
expect(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe(
'Warning: Invalid DOM property `class`. Did you mean `className`?\n in div (at **)',
);
expect(normalizeCodeLocInfo(console.error.calls.argsFor(1)[0])).toBe(
'Warning: Invalid event handler property `onclick`. Did you mean ' +
'`onClick`?\n in input (at **)',
'Warning: Invalid event handler property `oninput`. ' +
// Note: we don't know the right event name so we
// use a generic one (onClick) as a suggestion.
// This is because we don't bundle the event system
// on the server.
'React events use the camelCase naming convention, for example `onClick`.' +
'\n in input (at **)',
);
}
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,597 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('property to attribute mapping', function() {
describe('string properties', function() {
itRenders('simple numbers', async render => {
const e = await render(<div width={30} />);
expect(e.getAttribute('width')).toBe('30');
});
itRenders('simple strings', async render => {
const e = await render(<div width={'30'} />);
expect(e.getAttribute('width')).toBe('30');
});
itRenders('no string prop with true value', async render => {
const e = await render(<a href={true} />, 1);
expect(e.hasAttribute('href')).toBe(false);
});
itRenders('no string prop with false value', async render => {
const e = await render(<a href={false} />, 1);
expect(e.hasAttribute('href')).toBe(false);
});
itRenders('no string prop with null value', async render => {
const e = await render(<div width={null} />);
expect(e.hasAttribute('width')).toBe(false);
});
});
describe('boolean properties', function() {
itRenders('boolean prop with true value', async render => {
const e = await render(<div hidden={true} />);
expect(e.getAttribute('hidden')).toBe('');
});
itRenders('boolean prop with false value', async render => {
const e = await render(<div hidden={false} />);
expect(e.getAttribute('hidden')).toBe(null);
});
itRenders('boolean prop with self value', async render => {
const e = await render(<div hidden="hidden" />);
expect(e.getAttribute('hidden')).toBe('');
});
// this does not seem like correct behavior, since hidden="" in HTML indicates
// that the boolean property is present. however, it is how the current code
// behaves, so the test is included here.
itRenders('boolean prop with "" value', async render => {
const e = await render(<div hidden="" />);
expect(e.getAttribute('hidden')).toBe(null);
});
// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with string value', async render => {
const e = await render(<div hidden="foo" />);
expect(e.getAttribute('hidden')).toBe('');
});
// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with array value', async render => {
const e = await render(<div hidden={['foo', 'bar']} />);
expect(e.getAttribute('hidden')).toBe('');
});
// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with object value', async render => {
const e = await render(<div hidden={{foo: 'bar'}} />);
expect(e.getAttribute('hidden')).toBe('');
});
// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with non-zero number value', async render => {
const e = await render(<div hidden={10} />);
expect(e.getAttribute('hidden')).toBe('');
});
// this seems like it might mask programmer error, but it's existing behavior.
itRenders('boolean prop with zero value', async render => {
const e = await render(<div hidden={0} />);
expect(e.getAttribute('hidden')).toBe(null);
});
itRenders('no boolean prop with null value', async render => {
const e = await render(<div hidden={null} />);
expect(e.hasAttribute('hidden')).toBe(false);
});
});
describe('download property (combined boolean/string attribute)', function() {
itRenders('download prop with true value', async render => {
const e = await render(<a download={true} />);
expect(e.getAttribute('download')).toBe('');
});
itRenders('download prop with false value', async render => {
const e = await render(<a download={false} />);
expect(e.getAttribute('download')).toBe(null);
});
itRenders('download prop with string value', async render => {
const e = await render(<a download="myfile" />);
expect(e.getAttribute('download')).toBe('myfile');
});
itRenders('download prop with string "false" value', async render => {
const e = await render(<a download="false" />);
expect(e.getAttribute('download')).toBe('false');
});
itRenders('download prop with string "true" value', async render => {
const e = await render(<a download={'true'} />);
expect(e.getAttribute('download')).toBe('true');
});
itRenders('download prop with number 0 value', async render => {
const e = await render(<a download={0} />);
expect(e.getAttribute('download')).toBe('0');
});
itRenders('no download prop with null value', async render => {
const e = await render(<div download={null} />);
expect(e.hasAttribute('download')).toBe(false);
});
itRenders('no download prop with undefined value', async render => {
const e = await render(<div download={undefined} />);
expect(e.hasAttribute('download')).toBe(false);
});
});
describe('className property', function() {
itRenders('className prop with string value', async render => {
const e = await render(<div className="myClassName" />);
expect(e.getAttribute('class')).toBe('myClassName');
});
itRenders('className prop with empty string value', async render => {
const e = await render(<div className="" />);
expect(e.getAttribute('class')).toBe('');
});
itRenders('no className prop with true value', async render => {
const e = await render(<div className={true} />, 1);
expect(e.hasAttribute('class')).toBe(false);
});
itRenders('no className prop with false value', async render => {
const e = await render(<div className={false} />, 1);
expect(e.hasAttribute('class')).toBe(false);
});
itRenders('no className prop with null value', async render => {
const e = await render(<div className={null} />);
expect(e.hasAttribute('className')).toBe(false);
});
itRenders('badly cased className with a warning', async render => {
const e = await render(<div classname="test" />, 1);
expect(e.hasAttribute('class')).toBe(false);
expect(e.hasAttribute('classname')).toBe(true);
});
itRenders(
'className prop when given the alias with a warning',
async render => {
const e = await render(<div class="test" />, 1);
expect(e.className).toBe('test');
},
);
itRenders(
'className prop when given a badly cased alias',
async render => {
const e = await render(<div cLASs="test" />, 1);
expect(e.className).toBe('test');
},
);
itRenders('class for custom elements', async render => {
const e = await render(<div is="custom-element" class="test" />, 0);
expect(e.getAttribute('class')).toBe('test');
});
itRenders('className for custom elements', async render => {
const e = await render(<div is="custom-element" className="test" />, 0);
expect(e.getAttribute('className')).toBe('test');
});
});
describe('htmlFor property', function() {
itRenders('htmlFor with string value', async render => {
const e = await render(<div htmlFor="myFor" />);
expect(e.getAttribute('for')).toBe('myFor');
});
itRenders('no badly cased htmlfor', async render => {
const e = await render(<div htmlfor="myFor" />, 1);
expect(e.hasAttribute('for')).toBe(false);
expect(e.getAttribute('htmlfor')).toBe('myFor');
});
itRenders('htmlFor with an empty string', async render => {
const e = await render(<div htmlFor="" />);
expect(e.getAttribute('for')).toBe('');
});
itRenders('no htmlFor prop with true value', async render => {
const e = await render(<div htmlFor={true} />, 1);
expect(e.hasAttribute('for')).toBe(false);
});
itRenders('no htmlFor prop with false value', async render => {
const e = await render(<div htmlFor={false} />, 1);
expect(e.hasAttribute('for')).toBe(false);
});
itRenders('no htmlFor prop with null value', async render => {
const e = await render(<div htmlFor={null} />);
expect(e.hasAttribute('htmlFor')).toBe(false);
});
itRenders('htmlFor attribute on custom elements', async render => {
const e = await render(<div is="custom-element" htmlFor="test" />);
expect(e.getAttribute('htmlFor')).toBe('test');
});
itRenders('for attribute on custom elements', async render => {
const e = await render(<div is="custom-element" for="test" />);
expect(e.getAttribute('for')).toBe('test');
});
});
describe('numeric properties', function() {
itRenders(
'positive numeric property with positive value',
async render => {
const e = await render(<input size={2} />);
expect(e.getAttribute('size')).toBe('2');
},
);
itRenders(
'no positive numeric property with zero value',
async render => {
const e = await render(<input size={0} />);
expect(e.hasAttribute('size')).toBe(false);
},
);
itRenders('numeric property with zero value', async render => {
const e = await render(<ol start={0} />);
expect(e.getAttribute('start')).toBe('0');
});
});
describe('props with special meaning in React', function() {
itRenders('no ref attribute', async render => {
class RefComponent extends React.Component {
render() {
return <div ref="foo" />;
}
}
const e = await render(<RefComponent />);
expect(e.getAttribute('ref')).toBe(null);
});
itRenders('no children attribute', async render => {
const e = await render(React.createElement('div', {}, 'foo'));
expect(e.getAttribute('children')).toBe(null);
});
itRenders('no key attribute', async render => {
const e = await render(<div key="foo" />);
expect(e.getAttribute('key')).toBe(null);
});
itRenders('no dangerouslySetInnerHTML attribute', async render => {
const e = await render(
<div dangerouslySetInnerHTML={{__html: '<foo />'}} />,
);
expect(e.getAttribute('dangerouslySetInnerHTML')).toBe(null);
});
itRenders('no suppressContentEditableWarning attribute', async render => {
const e = await render(<div suppressContentEditableWarning={true} />);
expect(e.getAttribute('suppressContentEditableWarning')).toBe(null);
});
itRenders('no suppressHydrationWarning attribute', async render => {
const e = await render(<span suppressHydrationWarning={true} />);
expect(e.getAttribute('suppressHydrationWarning')).toBe(null);
});
});
describe('inline styles', function() {
itRenders('simple styles', async render => {
const e = await render(<div style={{color: 'red', width: '30px'}} />);
expect(e.style.color).toBe('red');
expect(e.style.width).toBe('30px');
});
itRenders('relevant styles with px', async render => {
const e = await render(
<div
style={{
left: 0,
margin: 16,
opacity: 0.5,
padding: '4px',
}}
/>,
);
expect(e.style.left).toBe('0px');
expect(e.style.margin).toBe('16px');
expect(e.style.opacity).toBe('0.5');
expect(e.style.padding).toBe('4px');
});
itRenders('custom properties', async render => {
const e = await render(<div style={{'--foo': 5}} />);
// This seems like an odd way computed properties are exposed in jsdom.
// In a real browser we'd read it with e.style.getPropertyValue('--foo')
expect(e.style.Foo).toBe('5');
});
itRenders('no undefined styles', async render => {
const e = await render(
<div style={{color: undefined, width: '30px'}} />,
);
expect(e.style.color).toBe('');
expect(e.style.width).toBe('30px');
});
itRenders('no null styles', async render => {
const e = await render(<div style={{color: null, width: '30px'}} />);
expect(e.style.color).toBe('');
expect(e.style.width).toBe('30px');
});
itRenders('no empty styles', async render => {
const e = await render(<div style={{color: null, width: null}} />);
expect(e.style.color).toBe('');
expect(e.style.width).toBe('');
expect(e.hasAttribute('style')).toBe(false);
});
itRenders('unitless-number rules with prefixes', async render => {
const {style} = await render(
<div
style={{
lineClamp: 10,
WebkitLineClamp: 10,
MozFlexGrow: 10,
msFlexGrow: 10,
msGridRow: 10,
msGridRowEnd: 10,
msGridRowSpan: 10,
msGridRowStart: 10,
msGridColumn: 10,
msGridColumnEnd: 10,
msGridColumnSpan: 10,
msGridColumnStart: 10,
}}
/>,
);
expect(style.lineClamp).toBe('10');
expect(style.WebkitLineClamp).toBe('10');
expect(style.MozFlexGrow).toBe('10');
// jsdom is inconsistent in the style property name
// it uses on the client and when processing server markup.
// But it should be there either way.
expect(style.MsFlexGrow || style.msFlexGrow).toBe('10');
expect(style.MsGridRow || style.msGridRow).toBe('10');
expect(style.MsGridRowEnd || style.msGridRowEnd).toBe('10');
expect(style.MsGridRowSpan || style.msGridRowSpan).toBe('10');
expect(style.MsGridRowStart || style.msGridRowStart).toBe('10');
expect(style.MsGridColumn || style.msGridColumn).toBe('10');
expect(style.MsGridColumnEnd || style.msGridColumnEnd).toBe('10');
expect(style.MsGridColumnSpan || style.msGridColumnSpan).toBe('10');
expect(style.MsGridColumnStart || style.msGridColumnStart).toBe('10');
});
});
describe('aria attributes', function() {
itRenders('simple strings', async render => {
const e = await render(<div aria-label="hello" />);
expect(e.getAttribute('aria-label')).toBe('hello');
});
// this probably is just masking programmer error, but it is existing behavior.
itRenders('aria string prop with false value', async render => {
const e = await render(<div aria-label={false} />);
expect(e.getAttribute('aria-label')).toBe('false');
});
itRenders('no aria prop with null value', async render => {
const e = await render(<div aria-label={null} />);
expect(e.hasAttribute('aria-label')).toBe(false);
});
itRenders('"aria" attribute with a warning', async render => {
// Reserved for future use.
const e = await render(<div aria="hello" />, 1);
expect(e.getAttribute('aria')).toBe('hello');
});
});
describe('cased attributes', function() {
itRenders(
'badly cased aliased HTML attribute with a warning',
async render => {
const e = await render(<meta httpequiv="refresh" />, 1);
expect(e.hasAttribute('http-equiv')).toBe(false);
expect(e.getAttribute('httpequiv')).toBe('refresh');
},
);
itRenders('badly cased SVG attribute with a warning', async render => {
const e = await render(<text textlength="10" />, 1);
expect(e.getAttribute('textLength')).toBe('10');
});
itRenders('no badly cased aliased SVG attribute alias', async render => {
const e = await render(<text strokedasharray="10 10" />, 1);
expect(e.hasAttribute('stroke-dasharray')).toBe(false);
expect(e.getAttribute('strokedasharray')).toBe('10 10');
});
itRenders(
'no badly cased original SVG attribute that is aliased',
async render => {
const e = await render(<text stroke-dasharray="10 10" />, 1);
expect(e.getAttribute('stroke-dasharray')).toBe('10 10');
},
);
});
describe('unknown attributes', function() {
itRenders('unknown attributes', async render => {
const e = await render(<div foo="bar" />);
expect(e.getAttribute('foo')).toBe('bar');
});
itRenders('unknown data- attributes', async render => {
const e = await render(<div data-foo="bar" />);
expect(e.getAttribute('data-foo')).toBe('bar');
});
itRenders('badly cased reserved attributes', async render => {
const e = await render(<div CHILDREN="5" />, 1);
expect(e.getAttribute('CHILDREN')).toBe('5');
});
itRenders('"data" attribute', async render => {
// For `<object />` acts as `src`.
const e = await render(<object data="hello" />);
expect(e.getAttribute('data')).toBe('hello');
});
itRenders('no unknown data- attributes with null value', async render => {
const e = await render(<div data-foo={null} />);
expect(e.hasAttribute('data-foo')).toBe(false);
});
itRenders('unknown data- attributes with casing', async render => {
const e = await render(<div data-fooBar="true" />, 1);
expect(e.getAttribute('data-foobar')).toBe('true');
});
itRenders('unknown data- attributes with boolean true', async render => {
const e = await render(<div data-foobar={true} />);
expect(e.getAttribute('data-foobar')).toBe('true');
});
itRenders('unknown data- attributes with boolean false', async render => {
const e = await render(<div data-foobar={false} />);
expect(e.getAttribute('data-foobar')).toBe('false');
});
itRenders(
'no unknown data- attributes with casing and null value',
async render => {
const e = await render(<div data-fooBar={null} />, 1);
expect(e.hasAttribute('data-foobar')).toBe(false);
},
);
itRenders('custom attributes for non-standard elements', async render => {
const e = await render(<nonstandard foo="bar" />);
expect(e.getAttribute('foo')).toBe('bar');
});
itRenders('unknown attributes for custom elements', async render => {
const e = await render(<custom-element foo="bar" />);
expect(e.getAttribute('foo')).toBe('bar');
});
itRenders(
'no unknown attributes for custom elements with null value',
async render => {
const e = await render(<custom-element foo={null} />);
expect(e.hasAttribute('foo')).toBe(false);
},
);
itRenders(
'unknown attributes for custom elements using is',
async render => {
const e = await render(<div is="custom-element" foo="bar" />);
expect(e.getAttribute('foo')).toBe('bar');
},
);
itRenders(
'no unknown attributes for custom elements using is with null value',
async render => {
const e = await render(<div is="custom-element" foo={null} />);
expect(e.hasAttribute('foo')).toBe(false);
},
);
itRenders('SVG tags with dashes in them', async render => {
const e = await render(
<svg>
<font-face accentHeight={10} />
</svg>,
);
expect(e.firstChild.hasAttribute('accentHeight')).toBe(false);
expect(e.firstChild.getAttribute('accent-height')).toBe('10');
});
itRenders('cased custom attributes', async render => {
const e = await render(<div fooBar="test" />, 1);
expect(e.getAttribute('foobar')).toBe('test');
});
});
itRenders('no HTML events', async render => {
const e = await render(<div onClick={() => {}} />);
expect(e.getAttribute('onClick')).toBe(null);
expect(e.getAttribute('onClick')).toBe(null);
expect(e.getAttribute('click')).toBe(null);
});
itRenders('no unknown events', async render => {
const e = await render(
<div onunknownevent="alert(&quot;hack&quot;)" />,
1,
);
expect(e.getAttribute('onunknownevent')).toBe(null);
});
itRenders('custom attribute named `on`', async render => {
const e = await render(<div on="tap:do-something" />);
expect(e.getAttribute('on')).toEqual('tap:do-something');
});
});
});
@@ -0,0 +1,152 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
const TEXT_NODE_TYPE = 3;
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('basic rendering', function() {
itRenders('a blank div', async render => {
const e = await render(<div />);
expect(e.tagName).toBe('DIV');
});
itRenders('a self-closing tag', async render => {
const e = await render(<br />);
expect(e.tagName).toBe('BR');
});
itRenders('a self-closing tag as a child', async render => {
const e = await render(
<div>
<br />
</div>,
);
expect(e.childNodes.length).toBe(1);
expect(e.firstChild.tagName).toBe('BR');
});
itRenders('a string', async render => {
let e = await render('Hello');
expect(e.nodeType).toBe(3);
expect(e.nodeValue).toMatch('Hello');
});
itRenders('a number', async render => {
let e = await render(42);
expect(e.nodeType).toBe(3);
expect(e.nodeValue).toMatch('42');
});
itRenders('an array with one child', async render => {
let e = await render([<div key={1}>text1</div>]);
let parent = e.parentNode;
expect(parent.childNodes[0].tagName).toBe('DIV');
});
itRenders('an array with several children', async render => {
let Header = props => {
return <p>header</p>;
};
let Footer = props => {
return [<h2 key={1}>footer</h2>, <h3 key={2}>about</h3>];
};
let e = await render([
<div key={1}>text1</div>,
<span key={2}>text2</span>,
<Header key={3} />,
<Footer key={4} />,
]);
let parent = e.parentNode;
expect(parent.childNodes[0].tagName).toBe('DIV');
expect(parent.childNodes[1].tagName).toBe('SPAN');
expect(parent.childNodes[2].tagName).toBe('P');
expect(parent.childNodes[3].tagName).toBe('H2');
expect(parent.childNodes[4].tagName).toBe('H3');
});
itRenders('a nested array', async render => {
let e = await render([
[<div key={1}>text1</div>],
<span key={1}>text2</span>,
[[[null, <p key={1} />], false]],
]);
let parent = e.parentNode;
expect(parent.childNodes[0].tagName).toBe('DIV');
expect(parent.childNodes[1].tagName).toBe('SPAN');
expect(parent.childNodes[2].tagName).toBe('P');
});
itRenders('an iterable', async render => {
const threeDivIterable = {
'@@iterator': function() {
var i = 0;
return {
next: function() {
if (i++ < 3) {
return {value: <div key={i} />, done: false};
} else {
return {value: undefined, done: true};
}
},
};
},
};
let e = await render(threeDivIterable);
let parent = e.parentNode;
expect(parent.childNodes.length).toBe(3);
expect(parent.childNodes[0].tagName).toBe('DIV');
expect(parent.childNodes[1].tagName).toBe('DIV');
expect(parent.childNodes[2].tagName).toBe('DIV');
});
itRenders('emptyish values', async render => {
let e = await render(0);
expect(e.nodeType).toBe(TEXT_NODE_TYPE);
expect(e.nodeValue).toMatch('0');
// Empty string is special because client renders a node
// but server returns empty HTML. So we compare parent text.
expect((await render(<div>{''}</div>)).textContent).toBe('');
expect(await render([])).toBe(null);
expect(await render(false)).toBe(null);
expect(await render(true)).toBe(null);
expect(await render(undefined)).toBe(null);
expect(await render([[[false]], undefined])).toBe(null);
});
});
});
@@ -0,0 +1,292 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let PropTypes;
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
PropTypes = require('prop-types');
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {
resetModules,
itRenders,
itThrowsWhenRendering,
} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('context', function() {
let PurpleContext, RedContext;
beforeEach(() => {
class Parent extends React.Component {
getChildContext() {
return {text: this.props.text};
}
render() {
return this.props.children;
}
}
Parent.childContextTypes = {text: PropTypes.string};
PurpleContext = props => <Parent text="purple">{props.children}</Parent>;
RedContext = props => <Parent text="red">{props.children}</Parent>;
});
itRenders('class child with context', async render => {
class ClassChildWithContext extends React.Component {
render() {
return <div>{this.context.text}</div>;
}
}
ClassChildWithContext.contextTypes = {text: PropTypes.string};
const e = await render(
<PurpleContext>
<ClassChildWithContext />
</PurpleContext>,
);
expect(e.textContent).toBe('purple');
});
itRenders('stateless child with context', async render => {
function StatelessChildWithContext(props, context) {
return <div>{context.text}</div>;
}
StatelessChildWithContext.contextTypes = {text: PropTypes.string};
const e = await render(
<PurpleContext>
<StatelessChildWithContext />
</PurpleContext>,
);
expect(e.textContent).toBe('purple');
});
itRenders('class child without context', async render => {
class ClassChildWithoutContext extends React.Component {
render() {
// this should render blank; context isn't passed to this component.
return <div>{this.context.text}</div>;
}
}
const e = await render(
<PurpleContext>
<ClassChildWithoutContext />
</PurpleContext>,
);
expect(e.textContent).toBe('');
});
itRenders('stateless child without context', async render => {
function StatelessChildWithoutContext(props, context) {
// this should render blank; context isn't passed to this component.
return <div>{context.text}</div>;
}
const e = await render(
<PurpleContext>
<StatelessChildWithoutContext />
</PurpleContext>,
);
expect(e.textContent).toBe('');
});
itRenders('class child with wrong context', async render => {
class ClassChildWithWrongContext extends React.Component {
render() {
// this should render blank; context.text isn't passed to this component.
return <div id="classWrongChild">{this.context.text}</div>;
}
}
ClassChildWithWrongContext.contextTypes = {foo: PropTypes.string};
const e = await render(
<PurpleContext>
<ClassChildWithWrongContext />
</PurpleContext>,
);
expect(e.textContent).toBe('');
});
itRenders('stateless child with wrong context', async render => {
function StatelessChildWithWrongContext(props, context) {
// this should render blank; context.text isn't passed to this component.
return <div id="statelessWrongChild">{context.text}</div>;
}
StatelessChildWithWrongContext.contextTypes = {
foo: PropTypes.string,
};
const e = await render(
<PurpleContext>
<StatelessChildWithWrongContext />
</PurpleContext>,
);
expect(e.textContent).toBe('');
});
itRenders('with context passed through to a grandchild', async render => {
function Grandchild(props, context) {
return <div>{context.text}</div>;
}
Grandchild.contextTypes = {text: PropTypes.string};
const Child = props => <Grandchild />;
const e = await render(
<PurpleContext>
<Child />
</PurpleContext>,
);
expect(e.textContent).toBe('purple');
});
itRenders('a child context overriding a parent context', async render => {
const Grandchild = (props, context) => {
return <div>{context.text}</div>;
};
Grandchild.contextTypes = {text: PropTypes.string};
const e = await render(
<PurpleContext>
<RedContext>
<Grandchild />
</RedContext>
</PurpleContext>,
);
expect(e.textContent).toBe('red');
});
itRenders('a child context merged with a parent context', async render => {
class Parent extends React.Component {
getChildContext() {
return {text1: 'purple'};
}
render() {
return <Child />;
}
}
Parent.childContextTypes = {text1: PropTypes.string};
class Child extends React.Component {
getChildContext() {
return {text2: 'red'};
}
render() {
return <Grandchild />;
}
}
Child.childContextTypes = {text2: PropTypes.string};
const Grandchild = (props, context) => {
return (
<div>
<div id="first">{context.text1}</div>
<div id="second">{context.text2}</div>
</div>
);
};
Grandchild.contextTypes = {
text1: PropTypes.string,
text2: PropTypes.string,
};
const e = await render(<Parent />);
expect(e.querySelector('#first').textContent).toBe('purple');
expect(e.querySelector('#second').textContent).toBe('red');
});
itRenders(
'with a call to componentWillMount before getChildContext',
async render => {
class WillMountContext extends React.Component {
getChildContext() {
return {text: this.state.text};
}
componentWillMount() {
this.setState({text: 'foo'});
}
render() {
return <Child />;
}
}
WillMountContext.childContextTypes = {text: PropTypes.string};
const Child = (props, context) => {
return <div>{context.text}</div>;
};
Child.contextTypes = {text: PropTypes.string};
const e = await render(<WillMountContext />);
expect(e.textContent).toBe('foo');
},
);
itRenders(
'if getChildContext exists but childContextTypes is missing with a warning',
async render => {
function HopefulChild(props, context) {
return context.foo || 'nope';
}
HopefulChild.contextTypes = {
foo: PropTypes.string,
};
class ForgetfulParent extends React.Component {
render() {
return <HopefulChild />;
}
getChildContext() {
return {foo: 'bar'};
}
}
const e = await render(<ForgetfulParent />, 1);
expect(e.textContent).toBe('nope');
},
);
itThrowsWhenRendering(
'if getChildContext returns a value not in childContextTypes',
render => {
class MyComponent extends React.Component {
render() {
return <div />;
}
getChildContext() {
return {value1: 'foo', value2: 'bar'};
}
}
MyComponent.childContextTypes = {value1: PropTypes.string};
return render(<MyComponent />);
},
'MyComponent.getChildContext(): key "value2" is not defined in childContextTypes.',
);
});
});
@@ -0,0 +1,865 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
const TEXT_NODE_TYPE = 3;
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {
resetModules,
itRenders,
itThrowsWhenRendering,
serverRender,
streamRender,
clientRenderOnServerString,
} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('elements and children', function() {
function expectNode(node, type, value) {
expect(node).not.toBe(null);
expect(node.nodeType).toBe(type);
expect(node.nodeValue).toMatch(value);
}
function expectTextNode(node, text) {
expectNode(node, TEXT_NODE_TYPE, text);
}
describe('text children', function() {
itRenders('a div with text', async render => {
const e = await render(<div>Text</div>);
expect(e.tagName).toBe('DIV');
expect(e.childNodes.length).toBe(1);
expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
});
itRenders('a div with text with flanking whitespace', async render => {
// prettier-ignore
const e = await render(<div> Text </div>);
expect(e.childNodes.length).toBe(1);
expectNode(e.childNodes[0], TEXT_NODE_TYPE, ' Text ');
});
itRenders('a div with an empty text child', async render => {
const e = await render(<div>{''}</div>);
expect(e.childNodes.length).toBe(0);
});
itRenders('a div with multiple empty text children', async render => {
const e = await render(
<div>
{''}
{''}
{''}
</div>,
);
if (render === serverRender || render === streamRender) {
// For plain server markup result we should have no text nodes if
// they're all empty.
expect(e.childNodes.length).toBe(0);
expect(e.textContent).toBe('');
} else {
expect(e.childNodes.length).toBe(3);
expectTextNode(e.childNodes[0], '');
expectTextNode(e.childNodes[1], '');
expectTextNode(e.childNodes[2], '');
}
});
itRenders('a div with multiple whitespace children', async render => {
// prettier-ignore
const e = await render(<div>{' '}{' '}{' '}</div>);
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
// For plain server markup result we have comments between.
// If we're able to hydrate, they remain.
expect(e.childNodes.length).toBe(5);
expectTextNode(e.childNodes[0], ' ');
expectTextNode(e.childNodes[2], ' ');
expectTextNode(e.childNodes[4], ' ');
} else {
expect(e.childNodes.length).toBe(3);
expectTextNode(e.childNodes[0], ' ');
expectTextNode(e.childNodes[1], ' ');
expectTextNode(e.childNodes[2], ' ');
}
});
itRenders('a div with text sibling to a node', async render => {
const e = await render(
<div>
Text<span>More Text</span>
</div>,
);
let spanNode;
expect(e.childNodes.length).toBe(2);
spanNode = e.childNodes[1];
expectTextNode(e.childNodes[0], 'Text');
expect(spanNode.tagName).toBe('SPAN');
expect(spanNode.childNodes.length).toBe(1);
expectNode(spanNode.firstChild, TEXT_NODE_TYPE, 'More Text');
});
itRenders('a non-standard element with text', async render => {
const e = await render(<nonstandard>Text</nonstandard>);
expect(e.tagName).toBe('NONSTANDARD');
expect(e.childNodes.length).toBe(1);
expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
});
itRenders('a custom element with text', async render => {
const e = await render(<custom-element>Text</custom-element>);
expect(e.tagName).toBe('CUSTOM-ELEMENT');
expect(e.childNodes.length).toBe(1);
expectNode(e.firstChild, TEXT_NODE_TYPE, 'Text');
});
itRenders('a leading blank child with a text sibling', async render => {
const e = await render(<div>{''}foo</div>);
if (render === serverRender || render === streamRender) {
expect(e.childNodes.length).toBe(1);
expectTextNode(e.childNodes[0], 'foo');
} else {
expect(e.childNodes.length).toBe(2);
expectTextNode(e.childNodes[0], '');
expectTextNode(e.childNodes[1], 'foo');
}
});
itRenders('a trailing blank child with a text sibling', async render => {
const e = await render(<div>foo{''}</div>);
// with Fiber, there are just two text nodes.
if (render === serverRender || render === streamRender) {
expect(e.childNodes.length).toBe(1);
expectTextNode(e.childNodes[0], 'foo');
} else {
expect(e.childNodes.length).toBe(2);
expectTextNode(e.childNodes[0], 'foo');
expectTextNode(e.childNodes[1], '');
}
});
itRenders('an element with two text children', async render => {
const e = await render(
<div>
{'foo'}
{'bar'}
</div>,
);
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
// In the server render output there's a comment between them.
expect(e.childNodes.length).toBe(3);
expectTextNode(e.childNodes[0], 'foo');
expectTextNode(e.childNodes[2], 'bar');
} else {
expect(e.childNodes.length).toBe(2);
expectTextNode(e.childNodes[0], 'foo');
expectTextNode(e.childNodes[1], 'bar');
}
});
itRenders(
'a component returning text node between two text nodes',
async render => {
const B = () => 'b';
const e = await render(
<div>
{'a'}
<B />
{'c'}
</div>,
);
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
// In the server render output there's a comment between them.
expect(e.childNodes.length).toBe(5);
expectTextNode(e.childNodes[0], 'a');
expectTextNode(e.childNodes[2], 'b');
expectTextNode(e.childNodes[4], 'c');
} else {
expect(e.childNodes.length).toBe(3);
expectTextNode(e.childNodes[0], 'a');
expectTextNode(e.childNodes[1], 'b');
expectTextNode(e.childNodes[2], 'c');
}
},
);
itRenders('a tree with sibling host and text nodes', async render => {
class X extends React.Component {
render() {
return [null, [<Y key="1" />], false];
}
}
function Y() {
return [<Z key="1" />, ['c']];
}
function Z() {
return null;
}
const e = await render(
<div>
{[['a'], 'b']}
<div>
<X key="1" />
d
</div>
e
</div>,
);
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
// In the server render output there's comments between text nodes.
expect(e.childNodes.length).toBe(5);
expectTextNode(e.childNodes[0], 'a');
expectTextNode(e.childNodes[2], 'b');
expect(e.childNodes[3].childNodes.length).toBe(3);
expectTextNode(e.childNodes[3].childNodes[0], 'c');
expectTextNode(e.childNodes[3].childNodes[2], 'd');
expectTextNode(e.childNodes[4], 'e');
} else {
expect(e.childNodes.length).toBe(4);
expectTextNode(e.childNodes[0], 'a');
expectTextNode(e.childNodes[1], 'b');
expect(e.childNodes[2].childNodes.length).toBe(2);
expectTextNode(e.childNodes[2].childNodes[0], 'c');
expectTextNode(e.childNodes[2].childNodes[1], 'd');
expectTextNode(e.childNodes[3], 'e');
}
});
});
describe('number children', function() {
itRenders('a number as single child', async render => {
const e = await render(<div>{3}</div>);
expect(e.textContent).toBe('3');
});
// zero is falsey, so it could look like no children if the code isn't careful.
itRenders('zero as single child', async render => {
const e = await render(<div>{0}</div>);
expect(e.textContent).toBe('0');
});
itRenders('an element with number and text children', async render => {
const e = await render(
<div>
{'foo'}
{40}
</div>,
);
// with Fiber, there are just two text nodes.
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
// In the server markup there's a comment between.
expect(e.childNodes.length).toBe(3);
expectTextNode(e.childNodes[0], 'foo');
expectTextNode(e.childNodes[2], '40');
} else {
expect(e.childNodes.length).toBe(2);
expectTextNode(e.childNodes[0], 'foo');
expectTextNode(e.childNodes[1], '40');
}
});
});
describe('null, false, and undefined children', function() {
itRenders('null single child as blank', async render => {
const e = await render(<div>{null}</div>);
expect(e.childNodes.length).toBe(0);
});
itRenders('false single child as blank', async render => {
const e = await render(<div>{false}</div>);
expect(e.childNodes.length).toBe(0);
});
itRenders('undefined single child as blank', async render => {
const e = await render(<div>{undefined}</div>);
expect(e.childNodes.length).toBe(0);
});
itRenders('a null component children as empty', async render => {
const NullComponent = () => null;
const e = await render(
<div>
<NullComponent />
</div>,
);
expect(e.childNodes.length).toBe(0);
});
itRenders('null children as blank', async render => {
const e = await render(<div>{null}foo</div>);
expect(e.childNodes.length).toBe(1);
expectTextNode(e.childNodes[0], 'foo');
});
itRenders('false children as blank', async render => {
const e = await render(<div>{false}foo</div>);
expect(e.childNodes.length).toBe(1);
expectTextNode(e.childNodes[0], 'foo');
});
itRenders('null and false children together as blank', async render => {
const e = await render(
<div>
{false}
{null}foo{null}
{false}
</div>,
);
expect(e.childNodes.length).toBe(1);
expectTextNode(e.childNodes[0], 'foo');
});
itRenders('only null and false children as blank', async render => {
const e = await render(
<div>
{false}
{null}
{null}
{false}
</div>,
);
expect(e.childNodes.length).toBe(0);
});
});
describe('elements with implicit namespaces', function() {
itRenders('an svg element', async render => {
const e = await render(<svg />);
expect(e.childNodes.length).toBe(0);
expect(e.tagName).toBe('svg');
expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
});
itRenders('svg child element with an attribute', async render => {
let e = await render(<svg viewBox="0 0 0 0" />);
expect(e.childNodes.length).toBe(0);
expect(e.tagName).toBe('svg');
expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
expect(e.getAttribute('viewBox')).toBe('0 0 0 0');
});
itRenders(
'svg child element with a namespace attribute',
async render => {
let e = await render(
<svg>
<image xlinkHref="http://i.imgur.com/w7GCRPb.png" />
</svg>,
);
e = e.firstChild;
expect(e.childNodes.length).toBe(0);
expect(e.tagName).toBe('image');
expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
expect(e.getAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
'http://i.imgur.com/w7GCRPb.png',
);
},
);
itRenders('svg child element with a badly cased alias', async render => {
let e = await render(
<svg>
<image xlinkhref="http://i.imgur.com/w7GCRPb.png" />
</svg>,
1,
);
e = e.firstChild;
expect(e.hasAttributeNS('http://www.w3.org/1999/xlink', 'href')).toBe(
false,
);
expect(e.getAttribute('xlinkhref')).toBe(
'http://i.imgur.com/w7GCRPb.png',
);
});
itRenders('svg element with a tabIndex attribute', async render => {
let e = await render(<svg tabIndex="1" />);
expect(e.tabIndex).toBe(1);
});
itRenders(
'svg element with a badly cased tabIndex attribute',
async render => {
let e = await render(<svg tabindex="1" />, 1);
expect(e.tabIndex).toBe(1);
},
);
itRenders('svg element with a mixed case name', async render => {
let e = await render(
<svg>
<filter>
<feMorphology />
</filter>
</svg>,
);
e = e.firstChild.firstChild;
expect(e.childNodes.length).toBe(0);
expect(e.tagName).toBe('feMorphology');
expect(e.namespaceURI).toBe('http://www.w3.org/2000/svg');
});
itRenders('a math element', async render => {
const e = await render(<math />);
expect(e.childNodes.length).toBe(0);
expect(e.tagName).toBe('math');
expect(e.namespaceURI).toBe('http://www.w3.org/1998/Math/MathML');
});
});
// specially wrapped components
// (see the big switch near the beginning ofReactDOMComponent.mountComponent)
itRenders('an img', async render => {
const e = await render(<img />);
expect(e.childNodes.length).toBe(0);
expect(e.nextSibling).toBe(null);
expect(e.tagName).toBe('IMG');
});
itRenders('a button', async render => {
const e = await render(<button />);
expect(e.childNodes.length).toBe(0);
expect(e.nextSibling).toBe(null);
expect(e.tagName).toBe('BUTTON');
});
itRenders('a div with dangerouslySetInnerHTML', async render => {
const e = await render(
<div dangerouslySetInnerHTML={{__html: "<span id='child'/>"}} />,
);
expect(e.childNodes.length).toBe(1);
expect(e.firstChild.tagName).toBe('SPAN');
expect(e.firstChild.getAttribute('id')).toBe('child');
expect(e.firstChild.childNodes.length).toBe(0);
});
describe('newline-eating elements', function() {
itRenders(
'a newline-eating tag with content not starting with \\n',
async render => {
const e = await render(<pre>Hello</pre>);
expect(e.textContent).toBe('Hello');
},
);
itRenders(
'a newline-eating tag with content starting with \\n',
async render => {
const e = await render(<pre>{'\nHello'}</pre>);
expect(e.textContent).toBe('\nHello');
},
);
itRenders('a normal tag with content starting with \\n', async render => {
const e = await render(<div>{'\nHello'}</div>);
expect(e.textContent).toBe('\nHello');
});
});
describe('different component implementations', function() {
function checkFooDiv(e) {
expect(e.childNodes.length).toBe(1);
expectNode(e.firstChild, TEXT_NODE_TYPE, 'foo');
}
itRenders('stateless components', async render => {
const StatelessComponent = () => <div>foo</div>;
checkFooDiv(await render(<StatelessComponent />));
});
itRenders('ES6 class components', async render => {
class ClassComponent extends React.Component {
render() {
return <div>foo</div>;
}
}
checkFooDiv(await render(<ClassComponent />));
});
itRenders('factory components', async render => {
const FactoryComponent = () => {
return {
render: function() {
return <div>foo</div>;
},
};
};
checkFooDiv(await render(<FactoryComponent />));
});
});
describe('component hierarchies', async function() {
itRenders('single child hierarchies of components', async render => {
const Component = props => <div>{props.children}</div>;
let e = await render(
<Component>
<Component>
<Component>
<Component />
</Component>
</Component>
</Component>,
);
for (var i = 0; i < 3; i++) {
expect(e.tagName).toBe('DIV');
expect(e.childNodes.length).toBe(1);
e = e.firstChild;
}
expect(e.tagName).toBe('DIV');
expect(e.childNodes.length).toBe(0);
});
itRenders('multi-child hierarchies of components', async render => {
const Component = props => <div>{props.children}</div>;
const e = await render(
<Component>
<Component>
<Component />
<Component />
</Component>
<Component>
<Component />
<Component />
</Component>
</Component>,
);
expect(e.tagName).toBe('DIV');
expect(e.childNodes.length).toBe(2);
for (var i = 0; i < 2; i++) {
var child = e.childNodes[i];
expect(child.tagName).toBe('DIV');
expect(child.childNodes.length).toBe(2);
for (var j = 0; j < 2; j++) {
var grandchild = child.childNodes[j];
expect(grandchild.tagName).toBe('DIV');
expect(grandchild.childNodes.length).toBe(0);
}
}
});
itRenders('a div with a child', async render => {
const e = await render(
<div id="parent">
<div id="child" />
</div>,
);
expect(e.id).toBe('parent');
expect(e.childNodes.length).toBe(1);
expect(e.childNodes[0].id).toBe('child');
expect(e.childNodes[0].childNodes.length).toBe(0);
});
itRenders('a div with multiple children', async render => {
const e = await render(
<div id="parent">
<div id="child1" />
<div id="child2" />
</div>,
);
expect(e.id).toBe('parent');
expect(e.childNodes.length).toBe(2);
expect(e.childNodes[0].id).toBe('child1');
expect(e.childNodes[0].childNodes.length).toBe(0);
expect(e.childNodes[1].id).toBe('child2');
expect(e.childNodes[1].childNodes.length).toBe(0);
});
itRenders(
'a div with multiple children separated by whitespace',
async render => {
const e = await render(
<div id="parent">
<div id="child1" /> <div id="child2" />
</div>,
);
expect(e.id).toBe('parent');
let child1, child2, textNode;
expect(e.childNodes.length).toBe(3);
child1 = e.childNodes[0];
textNode = e.childNodes[1];
child2 = e.childNodes[2];
expect(child1.id).toBe('child1');
expect(child1.childNodes.length).toBe(0);
expectTextNode(textNode, ' ');
expect(child2.id).toBe('child2');
expect(child2.childNodes.length).toBe(0);
},
);
itRenders(
'a div with a single child surrounded by whitespace',
async render => {
// prettier-ignore
const e = await render(<div id="parent"> <div id="child" /> </div>); // eslint-disable-line no-multi-spaces
let textNode1, child, textNode2;
expect(e.childNodes.length).toBe(3);
textNode1 = e.childNodes[0];
child = e.childNodes[1];
textNode2 = e.childNodes[2];
expect(e.id).toBe('parent');
expectTextNode(textNode1, ' ');
expect(child.id).toBe('child');
expect(child.childNodes.length).toBe(0);
expectTextNode(textNode2, ' ');
},
);
itRenders('a composite with multiple children', async render => {
const Component = props => props.children;
const e = await render(
<Component>{['a', 'b', [undefined], [[false, 'c']]]}</Component>,
);
let parent = e.parentNode;
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
// For plain server markup result we have comments between.
// If we're able to hydrate, they remain.
expect(parent.childNodes.length).toBe(5);
expectTextNode(parent.childNodes[0], 'a');
expectTextNode(parent.childNodes[2], 'b');
expectTextNode(parent.childNodes[4], 'c');
} else {
expect(parent.childNodes.length).toBe(3);
expectTextNode(parent.childNodes[0], 'a');
expectTextNode(parent.childNodes[1], 'b');
expectTextNode(parent.childNodes[2], 'c');
}
});
});
describe('escaping >, <, and &', function() {
itRenders('>,<, and & as single child', async render => {
const e = await render(<div>{'<span>Text&quot;</span>'}</div>);
expect(e.childNodes.length).toBe(1);
expectNode(e.firstChild, TEXT_NODE_TYPE, '<span>Text&quot;</span>');
});
itRenders('>,<, and & as multiple children', async render => {
const e = await render(
<div>
{'<span>Text1&quot;</span>'}
{'<span>Text2&quot;</span>'}
</div>,
);
if (
render === serverRender ||
render === clientRenderOnServerString ||
render === streamRender
) {
expect(e.childNodes.length).toBe(3);
expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
expectTextNode(e.childNodes[2], '<span>Text2&quot;</span>');
} else {
expect(e.childNodes.length).toBe(2);
expectTextNode(e.childNodes[0], '<span>Text1&quot;</span>');
expectTextNode(e.childNodes[1], '<span>Text2&quot;</span>');
}
});
});
describe('carriage return and null character', () => {
// HTML parsing normalizes CR and CRLF to LF.
// It also ignores null character.
// https://www.w3.org/TR/html5/single-page.html#preprocessing-the-input-stream
// If we have a mismatch, it might be caused by that (and should not be reported).
// We won't be patching up in this case as that matches our past behavior.
itRenders(
'an element with one text child with special characters',
async render => {
const e = await render(<div>{'foo\rbar\r\nbaz\nqux\u0000'}</div>);
if (render === serverRender || render === streamRender) {
expect(e.childNodes.length).toBe(1);
// Everything becomes LF when parsed from server HTML.
// Null character is ignored.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar\nbaz\nqux');
} else {
expect(e.childNodes.length).toBe(1);
// Client rendering (or hydration) uses JS value with CR.
// Null character stays.
expectNode(
e.childNodes[0],
TEXT_NODE_TYPE,
'foo\rbar\r\nbaz\nqux\u0000',
);
}
},
);
itRenders(
'an element with two text children with special characters',
async render => {
const e = await render(
<div>
{'foo\rbar'}
{'\r\nbaz\nqux\u0000'}
</div>,
);
if (render === serverRender || render === streamRender) {
// We have three nodes because there is a comment between them.
expect(e.childNodes.length).toBe(3);
// Everything becomes LF when parsed from server HTML.
// Null character is ignored.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\nbar');
expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\nbaz\nqux');
} else if (render === clientRenderOnServerString) {
// We have three nodes because there is a comment between them.
expect(e.childNodes.length).toBe(3);
// Hydration uses JS value with CR and null character.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
expectNode(e.childNodes[2], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
} else {
expect(e.childNodes.length).toBe(2);
// Client rendering uses JS value with CR and null character.
expectNode(e.childNodes[0], TEXT_NODE_TYPE, 'foo\rbar');
expectNode(e.childNodes[1], TEXT_NODE_TYPE, '\r\nbaz\nqux\u0000');
}
},
);
itRenders(
'an element with an attribute value with special characters',
async render => {
const e = await render(<a title={'foo\rbar\r\nbaz\nqux\u0000'} />);
if (
render === serverRender ||
render === streamRender ||
render === clientRenderOnServerString
) {
// Everything becomes LF when parsed from server HTML.
// Null character in an attribute becomes the replacement character.
// Hydration also ends up with LF because we don't patch up attributes.
expect(e.title).toBe('foo\nbar\nbaz\nqux\uFFFD');
} else {
// Client rendering uses JS value with CR and null character.
expect(e.title).toBe('foo\rbar\r\nbaz\nqux\u0000');
}
},
);
});
describe('components that throw errors', function() {
itThrowsWhenRendering(
'a function returning undefined',
async render => {
const UndefinedComponent = () => undefined;
await render(<UndefinedComponent />, 1);
},
'UndefinedComponent(...): Nothing was returned from render. ' +
'This usually means a return statement is missing. Or, to ' +
'render nothing, return null.',
);
itThrowsWhenRendering(
'a class returning undefined',
async render => {
class UndefinedComponent extends React.Component {
render() {
return undefined;
}
}
await render(<UndefinedComponent />, 1);
},
'UndefinedComponent(...): Nothing was returned from render. ' +
'This usually means a return statement is missing. Or, to ' +
'render nothing, return null.',
);
itThrowsWhenRendering(
'a function returning an object',
async render => {
const ObjectComponent = () => ({x: 123});
await render(<ObjectComponent />, 1);
},
'Objects are not valid as a React child (found: object with keys {x}).' +
(__DEV__
? ' If you meant to render a collection of children, use ' +
'an array instead.'
: ''),
);
itThrowsWhenRendering(
'a class returning an object',
async render => {
class ObjectComponent extends React.Component {
render() {
return {x: 123};
}
}
await render(<ObjectComponent />, 1);
},
'Objects are not valid as a React child (found: object with keys {x}).' +
(__DEV__
? ' If you meant to render a collection of children, use ' +
'an array instead.'
: ''),
);
itThrowsWhenRendering(
'top-level object',
async render => {
await render({x: 123});
},
'Objects are not valid as a React child (found: object with keys {x}).' +
(__DEV__
? ' If you meant to render a collection of children, use ' +
'an array instead.'
: ''),
);
});
});
});
@@ -0,0 +1,612 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOM;
let ReactDOMServer;
let ReactTestUtils;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {
resetModules,
itRenders,
itClientRenders,
renderIntoDom,
serverRender,
} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('form controls', function() {
describe('inputs', function() {
itRenders('an input with a value and an onChange', async render => {
const e = await render(<input value="foo" onChange={() => {}} />);
expect(e.value).toBe('foo');
});
itRenders('an input with a value and readOnly', async render => {
const e = await render(<input value="foo" readOnly={true} />);
expect(e.value).toBe('foo');
});
itRenders(
'an input with a value and no onChange/readOnly',
async render => {
// this configuration should raise a dev warning that value without
// onChange or readOnly is a mistake.
const e = await render(<input value="foo" />, 1);
expect(e.value).toBe('foo');
expect(e.getAttribute('value')).toBe('foo');
},
);
itRenders('an input with a defaultValue', async render => {
const e = await render(<input defaultValue="foo" />);
expect(e.value).toBe('foo');
expect(e.getAttribute('value')).toBe('foo');
expect(e.getAttribute('defaultValue')).toBe(null);
});
itRenders('an input value overriding defaultValue', async render => {
const e = await render(
<input value="foo" defaultValue="bar" readOnly={true} />,
1,
);
expect(e.value).toBe('foo');
expect(e.getAttribute('value')).toBe('foo');
expect(e.getAttribute('defaultValue')).toBe(null);
});
itRenders(
'an input value overriding defaultValue no matter the prop order',
async render => {
const e = await render(
<input defaultValue="bar" value="foo" readOnly={true} />,
1,
);
expect(e.value).toBe('foo');
expect(e.getAttribute('value')).toBe('foo');
expect(e.getAttribute('defaultValue')).toBe(null);
},
);
});
describe('checkboxes', function() {
itRenders('a checkbox that is checked with an onChange', async render => {
const e = await render(
<input type="checkbox" checked={true} onChange={() => {}} />,
);
expect(e.checked).toBe(true);
});
itRenders('a checkbox that is checked with readOnly', async render => {
const e = await render(
<input type="checkbox" checked={true} readOnly={true} />,
);
expect(e.checked).toBe(true);
});
itRenders(
'a checkbox that is checked and no onChange/readOnly',
async render => {
// this configuration should raise a dev warning that checked without
// onChange or readOnly is a mistake.
const e = await render(<input type="checkbox" checked={true} />, 1);
expect(e.checked).toBe(true);
},
);
itRenders('a checkbox with defaultChecked', async render => {
const e = await render(<input type="checkbox" defaultChecked={true} />);
expect(e.checked).toBe(true);
expect(e.getAttribute('defaultChecked')).toBe(null);
});
itRenders(
'a checkbox checked overriding defaultChecked',
async render => {
const e = await render(
<input
type="checkbox"
checked={true}
defaultChecked={false}
readOnly={true}
/>,
1,
);
expect(e.checked).toBe(true);
expect(e.getAttribute('defaultChecked')).toBe(null);
},
);
itRenders(
'a checkbox checked overriding defaultChecked no matter the prop order',
async render => {
const e = await render(
<input
type="checkbox"
defaultChecked={false}
checked={true}
readOnly={true}
/>,
1,
);
expect(e.checked).toBe(true);
expect(e.getAttribute('defaultChecked')).toBe(null);
},
);
});
describe('textareas', function() {
// textareas
// ---------
itRenders('a textarea with a value and an onChange', async render => {
const e = await render(<textarea value="foo" onChange={() => {}} />);
// textarea DOM elements don't have a value **attribute**, the text is
// a child of the element and accessible via the .value **property**.
expect(e.getAttribute('value')).toBe(null);
expect(e.value).toBe('foo');
});
itRenders('a textarea with a value and readOnly', async render => {
const e = await render(<textarea value="foo" readOnly={true} />);
// textarea DOM elements don't have a value **attribute**, the text is
// a child of the element and accessible via the .value **property**.
expect(e.getAttribute('value')).toBe(null);
expect(e.value).toBe('foo');
});
itRenders(
'a textarea with a value and no onChange/readOnly',
async render => {
// this configuration should raise a dev warning that value without
// onChange or readOnly is a mistake.
const e = await render(<textarea value="foo" />, 1);
expect(e.getAttribute('value')).toBe(null);
expect(e.value).toBe('foo');
},
);
itRenders('a textarea with a defaultValue', async render => {
const e = await render(<textarea defaultValue="foo" />);
expect(e.getAttribute('value')).toBe(null);
expect(e.getAttribute('defaultValue')).toBe(null);
expect(e.value).toBe('foo');
});
itRenders('a textarea value overriding defaultValue', async render => {
const e = await render(
<textarea value="foo" defaultValue="bar" readOnly={true} />,
1,
);
expect(e.getAttribute('value')).toBe(null);
expect(e.getAttribute('defaultValue')).toBe(null);
expect(e.value).toBe('foo');
});
itRenders(
'a textarea value overriding defaultValue no matter the prop order',
async render => {
const e = await render(
<textarea defaultValue="bar" value="foo" readOnly={true} />,
1,
);
expect(e.getAttribute('value')).toBe(null);
expect(e.getAttribute('defaultValue')).toBe(null);
expect(e.value).toBe('foo');
},
);
});
describe('selects', function() {
var options;
beforeEach(function() {
options = [
<option key={1} value="foo" id="foo">
Foo
</option>,
<option key={2} value="bar" id="bar">
Bar
</option>,
<option key={3} value="baz" id="baz">
Baz
</option>,
];
});
// a helper function to test the selected value of a <select> element.
// takes in a <select> DOM element (element) and a value or array of
// values that should be selected (selected).
const expectSelectValue = (element, selected) => {
if (!Array.isArray(selected)) {
selected = [selected];
}
// the select DOM element shouldn't ever have a value or defaultValue
// attribute; that is not how select values are expressed in the DOM.
expect(element.getAttribute('value')).toBe(null);
expect(element.getAttribute('defaultValue')).toBe(null);
['foo', 'bar', 'baz'].forEach(value => {
const expectedValue = selected.indexOf(value) !== -1;
var option = element.querySelector(`#${value}`);
expect(option.selected).toBe(expectedValue);
});
};
itRenders('a select with a value and an onChange', async render => {
const e = await render(
<select value="bar" onChange={() => {}}>
{options}
</select>,
);
expectSelectValue(e, 'bar');
});
itRenders('a select with a value and readOnly', async render => {
const e = await render(
<select value="bar" readOnly={true}>
{options}
</select>,
);
expectSelectValue(e, 'bar');
});
itRenders(
'a select with a multiple values and an onChange',
async render => {
const e = await render(
<select value={['bar', 'baz']} multiple={true} onChange={() => {}}>
{options}
</select>,
);
expectSelectValue(e, ['bar', 'baz']);
},
);
itRenders(
'a select with a multiple values and readOnly',
async render => {
const e = await render(
<select value={['bar', 'baz']} multiple={true} readOnly={true}>
{options}
</select>,
);
expectSelectValue(e, ['bar', 'baz']);
},
);
itRenders(
'a select with a value and no onChange/readOnly',
async render => {
// this configuration should raise a dev warning that value without
// onChange or readOnly is a mistake.
const e = await render(<select value="bar">{options}</select>, 1);
expectSelectValue(e, 'bar');
},
);
itRenders('a select with a defaultValue', async render => {
const e = await render(<select defaultValue="bar">{options}</select>);
expectSelectValue(e, 'bar');
});
itRenders('a select value overriding defaultValue', async render => {
const e = await render(
<select value="bar" defaultValue="baz" readOnly={true}>
{options}
</select>,
1,
);
expectSelectValue(e, 'bar');
});
itRenders(
'a select value overriding defaultValue no matter the prop order',
async render => {
const e = await render(
<select defaultValue="baz" value="bar" readOnly={true}>
{options}
</select>,
1,
);
expectSelectValue(e, 'bar');
},
);
});
describe('user interaction', function() {
let ControlledInput,
ControlledTextArea,
ControlledCheckbox,
ControlledSelect;
beforeEach(() => {
ControlledInput = class extends React.Component {
constructor() {
super();
this.state = {value: 'Hello'};
}
handleChange(event) {
if (this.props.onChange) {
this.props.onChange(event);
}
this.setState({value: event.target.value});
}
render() {
return (
<input
value={this.state.value}
onChange={this.handleChange.bind(this)}
/>
);
}
};
ControlledTextArea = class extends React.Component {
constructor() {
super();
this.state = {value: 'Hello'};
}
handleChange(event) {
if (this.props.onChange) {
this.props.onChange(event);
}
this.setState({value: event.target.value});
}
render() {
return (
<textarea
value={this.state.value}
onChange={this.handleChange.bind(this)}
/>
);
}
};
ControlledCheckbox = class extends React.Component {
constructor() {
super();
this.state = {value: true};
}
handleChange(event) {
if (this.props.onChange) {
this.props.onChange(event);
}
this.setState({value: event.target.checked});
}
render() {
return (
<input
type="checkbox"
checked={this.state.value}
onChange={this.handleChange.bind(this)}
/>
);
}
};
ControlledSelect = class extends React.Component {
constructor() {
super();
this.state = {value: 'Hello'};
}
handleChange(event) {
if (this.props.onChange) {
this.props.onChange(event);
}
this.setState({value: event.target.value});
}
render() {
return (
<select
value={this.state.value}
onChange={this.handleChange.bind(this)}>
<option key="1" value="Hello">
Hello
</option>
<option key="2" value="Goodbye">
Goodbye
</option>
</select>
);
}
};
});
describe('user interaction with controlled inputs', function() {
itClientRenders('a controlled text input', async render => {
let changeCount = 0;
const e = await render(
<ControlledInput onChange={() => changeCount++} />,
);
expect(changeCount).toBe(0);
expect(e.value).toBe('Hello');
// simulate a user typing.
e.value = 'Goodbye';
ReactTestUtils.Simulate.change(e);
expect(changeCount).toBe(1);
expect(e.value).toBe('Goodbye');
});
itClientRenders('a controlled textarea', async render => {
let changeCount = 0;
const e = await render(
<ControlledTextArea onChange={() => changeCount++} />,
);
expect(changeCount).toBe(0);
expect(e.value).toBe('Hello');
// simulate a user typing.
e.value = 'Goodbye';
ReactTestUtils.Simulate.change(e);
expect(changeCount).toBe(1);
expect(e.value).toBe('Goodbye');
});
itClientRenders('a controlled checkbox', async render => {
let changeCount = 0;
const e = await render(
<ControlledCheckbox onChange={() => changeCount++} />,
);
expect(changeCount).toBe(0);
expect(e.checked).toBe(true);
// simulate a user typing.
e.checked = false;
ReactTestUtils.Simulate.change(e);
expect(changeCount).toBe(1);
expect(e.checked).toBe(false);
});
itClientRenders('a controlled select', async render => {
let changeCount = 0;
const e = await render(
<ControlledSelect onChange={() => changeCount++} />,
);
expect(changeCount).toBe(0);
expect(e.value).toBe('Hello');
// simulate a user typing.
e.value = 'Goodbye';
ReactTestUtils.Simulate.change(e);
expect(changeCount).toBe(1);
expect(e.value).toBe('Goodbye');
});
});
describe('user interaction with inputs before client render', function() {
// renders the element and changes the value **before** the client
// code has a chance to render; this simulates what happens when a
// user starts to interact with a server-rendered form before
// ReactDOM.render is called. the client render should NOT blow away
// the changes the user has made.
const testUserInteractionBeforeClientRender = async (
element,
initialValue = 'Hello',
changedValue = 'Goodbye',
valueKey = 'value',
) => {
const field = await serverRender(element);
expect(field[valueKey]).toBe(initialValue);
// simulate a user typing in the field **before** client-side reconnect happens.
field[valueKey] = changedValue;
resetModules();
// client render on top of the server markup.
const clientField = await renderIntoDom(
element,
field.parentNode,
true,
);
// verify that the input field was not replaced.
// Note that we cannot use expect(clientField).toBe(field) because
// of jest bug #1772
expect(clientField === field).toBe(true);
// confirm that the client render has not changed what the user typed.
expect(clientField[valueKey]).toBe(changedValue);
};
it('should not blow away user-entered text on successful reconnect to an uncontrolled input', () =>
testUserInteractionBeforeClientRender(
<input defaultValue="Hello" />,
));
it('should not blow away user-entered text on successful reconnect to a controlled input', async () => {
let changeCount = 0;
await testUserInteractionBeforeClientRender(
<ControlledInput onChange={() => changeCount++} />,
);
// note that there's a strong argument to be made that the DOM revival
// algorithm should notice that the user has changed the value and fire
// an onChange. however, it does not now, so that's what this tests.
expect(changeCount).toBe(0);
});
it('should not blow away user-entered text on successful reconnect to an uncontrolled checkbox', () =>
testUserInteractionBeforeClientRender(
<input type="checkbox" defaultChecked={true} />,
true,
false,
'checked',
));
it('should not blow away user-entered text on successful reconnect to a controlled checkbox', async () => {
let changeCount = 0;
await testUserInteractionBeforeClientRender(
<ControlledCheckbox onChange={() => changeCount++} />,
true,
false,
'checked',
);
expect(changeCount).toBe(0);
});
// skipping this test because React 15 does the wrong thing. it blows
// away the user's typing in the textarea.
xit('should not blow away user-entered text on successful reconnect to an uncontrolled textarea', () =>
testUserInteractionBeforeClientRender(
<textarea defaultValue="Hello" />,
));
// skipping this test because React 15 does the wrong thing. it blows
// away the user's typing in the textarea.
xit('should not blow away user-entered text on successful reconnect to a controlled textarea', async () => {
let changeCount = 0;
await testUserInteractionBeforeClientRender(
<ControlledTextArea onChange={() => changeCount++} />,
);
expect(changeCount).toBe(0);
});
it('should not blow away user-selected value on successful reconnect to an uncontrolled select', () =>
testUserInteractionBeforeClientRender(
<select defaultValue="Hello">
<option key="1" value="Hello">
Hello
</option>
<option key="2" value="Goodbye">
Goodbye
</option>
</select>,
));
it('should not blow away user-selected value on successful reconnect to an controlled select', async () => {
let changeCount = 0;
await testUserInteractionBeforeClientRender(
<ControlledSelect onChange={() => changeCount++} />,
);
expect(changeCount).toBe(0);
});
});
});
});
});
@@ -0,0 +1,107 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
require('shared/ReactFeatureFlags').enableReactFragment = true;
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {resetModules, itRenders} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('React.Fragment', () => {
itRenders('a fragment with one child', async render => {
let e = await render(
<React.Fragment>
<div>text1</div>
</React.Fragment>,
);
let parent = e.parentNode;
expect(parent.childNodes[0].tagName).toBe('DIV');
});
itRenders('a fragment with several children', async render => {
let Header = props => {
return <p>header</p>;
};
let Footer = props => {
return (
<React.Fragment>
<h2>footer</h2>
<h3>about</h3>
</React.Fragment>
);
};
let e = await render(
<React.Fragment>
<div>text1</div>
<span>text2</span>
<Header />
<Footer />
</React.Fragment>,
);
let parent = e.parentNode;
expect(parent.childNodes[0].tagName).toBe('DIV');
expect(parent.childNodes[1].tagName).toBe('SPAN');
expect(parent.childNodes[2].tagName).toBe('P');
expect(parent.childNodes[3].tagName).toBe('H2');
expect(parent.childNodes[4].tagName).toBe('H3');
});
itRenders('a nested fragment', async render => {
let e = await render(
<React.Fragment>
<React.Fragment>
<div>text1</div>
</React.Fragment>
<span>text2</span>
<React.Fragment>
<React.Fragment>
<React.Fragment>
{null}
<p />
</React.Fragment>
{false}
</React.Fragment>
</React.Fragment>
</React.Fragment>,
);
let parent = e.parentNode;
expect(parent.childNodes[0].tagName).toBe('DIV');
expect(parent.childNodes[1].tagName).toBe('SPAN');
expect(parent.childNodes[2].tagName).toBe('P');
});
itRenders('an empty framgent', async render => {
expect(await render(<React.Fragment />)).toBe(null);
});
});
});
@@ -0,0 +1,424 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {
resetModules,
expectMarkupMismatch,
expectMarkupMatch,
} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('reconnecting to server markup', function() {
var EmptyComponent;
beforeEach(() => {
EmptyComponent = class extends React.Component {
render() {
return null;
}
};
});
describe('elements', function() {
describe('reconnecting different component implementations', function() {
let ES6ClassComponent, PureComponent, bareElement;
beforeEach(() => {
// try each type of component on client and server.
ES6ClassComponent = class extends React.Component {
render() {
return <div id={this.props.id} />;
}
};
PureComponent = props => <div id={props.id} />;
bareElement = <div id="foobarbaz" />;
});
it('should reconnect ES6 Class to ES6 Class', () =>
expectMarkupMatch(
<ES6ClassComponent id="foobarbaz" />,
<ES6ClassComponent id="foobarbaz" />,
));
it('should reconnect Pure Component to ES6 Class', () =>
expectMarkupMatch(
<ES6ClassComponent id="foobarbaz" />,
<PureComponent id="foobarbaz" />,
));
it('should reconnect Bare Element to ES6 Class', () =>
expectMarkupMatch(<ES6ClassComponent id="foobarbaz" />, bareElement));
it('should reconnect ES6 Class to Pure Component', () =>
expectMarkupMatch(
<PureComponent id="foobarbaz" />,
<ES6ClassComponent id="foobarbaz" />,
));
it('should reconnect Pure Component to Pure Component', () =>
expectMarkupMatch(
<PureComponent id="foobarbaz" />,
<PureComponent id="foobarbaz" />,
));
it('should reconnect Bare Element to Pure Component', () =>
expectMarkupMatch(<PureComponent id="foobarbaz" />, bareElement));
it('should reconnect ES6 Class to Bare Element', () =>
expectMarkupMatch(bareElement, <ES6ClassComponent id="foobarbaz" />));
it('should reconnect Pure Component to Bare Element', () =>
expectMarkupMatch(bareElement, <PureComponent id="foobarbaz" />));
it('should reconnect Bare Element to Bare Element', () =>
expectMarkupMatch(bareElement, bareElement));
});
it('should error reconnecting different element types', () =>
expectMarkupMismatch(<div />, <span />));
it('should error reconnecting fewer root children', () =>
expectMarkupMismatch(<span key="a" />, [
<span key="a" />,
<span key="b" />,
]));
it('should error reconnecting missing attributes', () =>
expectMarkupMismatch(<div id="foo" />, <div />));
it('should error reconnecting added attributes', () =>
expectMarkupMismatch(<div />, <div id="foo" />));
it('should error reconnecting different attribute values', () =>
expectMarkupMismatch(<div id="foo" />, <div id="bar" />));
it('can explicitly ignore errors reconnecting different element types of children', () =>
expectMarkupMatch(
<div>
<div />
</div>,
<div suppressHydrationWarning={true}>
<span />
</div>,
));
it('can explicitly ignore errors reconnecting missing attributes', () =>
expectMarkupMatch(
<div id="foo" />,
<div suppressHydrationWarning={true} />,
));
it('can explicitly ignore errors reconnecting added attributes', () =>
expectMarkupMatch(
<div />,
<div id="foo" suppressHydrationWarning={true} />,
));
it('can explicitly ignore errors reconnecting different attribute values', () =>
expectMarkupMatch(
<div id="foo" />,
<div id="bar" suppressHydrationWarning={true} />,
));
it('can not deeply ignore errors reconnecting different attribute values', () =>
expectMarkupMismatch(
<div>
<div id="foo" />
</div>,
<div suppressHydrationWarning={true}>
<div id="bar" />
</div>,
));
});
describe('inline styles', function() {
it('should error reconnecting missing style attribute', () =>
expectMarkupMismatch(<div style={{width: '1px'}} />, <div />));
it('should error reconnecting added style attribute', () =>
expectMarkupMismatch(<div />, <div style={{width: '1px'}} />));
it('should error reconnecting empty style attribute', () =>
expectMarkupMismatch(
<div style={{width: '1px'}} />,
<div style={{}} />,
));
it('should error reconnecting added style values', () =>
expectMarkupMismatch(
<div style={{}} />,
<div style={{width: '1px'}} />,
));
it('should error reconnecting different style values', () =>
expectMarkupMismatch(
<div style={{width: '1px'}} />,
<div style={{width: '2px'}} />,
));
it('should reconnect number and string versions of a number', () =>
expectMarkupMatch(
<div style={{width: '1px', height: 2}} />,
<div style={{width: 1, height: '2px'}} />,
));
it('should error reconnecting reordered style values', () =>
expectMarkupMismatch(
<div style={{width: '1px', fontSize: '2px'}} />,
<div style={{fontSize: '2px', width: '1px'}} />,
));
it('can explicitly ignore errors reconnecting added style values', () =>
expectMarkupMatch(
<div style={{}} />,
<div style={{width: '1px'}} suppressHydrationWarning={true} />,
));
it('can explicitly ignore reconnecting different style values', () =>
expectMarkupMatch(
<div style={{width: '1px'}} />,
<div style={{width: '2px'}} suppressHydrationWarning={true} />,
));
});
describe('text nodes', function() {
it('should error reconnecting different text', () =>
expectMarkupMismatch(<div>Text</div>, <div>Other Text</div>));
it('should reconnect a div with a number and string version of number', () =>
expectMarkupMatch(<div>{2}</div>, <div>2</div>));
it('should error reconnecting different numbers', () =>
expectMarkupMismatch(<div>{2}</div>, <div>{3}</div>));
it('should error reconnecting different number from text', () =>
expectMarkupMismatch(<div>{2}</div>, <div>3</div>));
it('should error reconnecting different text in two code blocks', () =>
expectMarkupMismatch(
<div>
{'Text1'}
{'Text2'}
</div>,
<div>
{'Text1'}
{'Text3'}
</div>,
));
it('can explicitly ignore reconnecting different text', () =>
expectMarkupMatch(
<div>Text</div>,
<div suppressHydrationWarning={true}>Other Text</div>,
));
it('can explicitly ignore reconnecting different text in two code blocks', () =>
expectMarkupMatch(
<div suppressHydrationWarning={true}>
{'Text1'}
{'Text2'}
</div>,
<div suppressHydrationWarning={true}>
{'Text1'}
{'Text3'}
</div>,
));
});
describe('element trees and children', function() {
it('should error reconnecting missing children', () =>
expectMarkupMismatch(
<div>
<div />
</div>,
<div />,
));
it('should error reconnecting added children', () =>
expectMarkupMismatch(
<div />,
<div>
<div />
</div>,
));
it('should error reconnecting more children', () =>
expectMarkupMismatch(
<div>
<div />
</div>,
<div>
<div />
<div />
</div>,
));
it('should error reconnecting fewer children', () =>
expectMarkupMismatch(
<div>
<div />
<div />
</div>,
<div>
<div />
</div>,
));
it('should error reconnecting reordered children', () =>
expectMarkupMismatch(
<div>
<div />
<span />
</div>,
<div>
<span />
<div />
</div>,
));
it('should error reconnecting a div with children separated by whitespace on the client', () =>
expectMarkupMismatch(
<div id="parent">
<div id="child1" />
<div id="child2" />
</div>,
// prettier-ignore
<div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
));
it('should error reconnecting a div with children separated by different whitespace on the server', () =>
expectMarkupMismatch(
// prettier-ignore
<div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
<div id="parent">
<div id="child1" />
<div id="child2" />
</div>,
));
it('should error reconnecting a div with children separated by different whitespace', () =>
expectMarkupMismatch(
<div id="parent">
<div id="child1" /> <div id="child2" />
</div>,
// prettier-ignore
<div id="parent"><div id="child1" /> <div id="child2" /></div>, // eslint-disable-line no-multi-spaces
));
it('can distinguish an empty component from a dom node', () =>
expectMarkupMismatch(
<div>
<span />
</div>,
<div>
<EmptyComponent />
</div>,
));
it('can distinguish an empty component from an empty text component', () =>
expectMarkupMatch(
<div>
<EmptyComponent />
</div>,
<div>{''}</div>,
));
it('can explicitly ignore reconnecting more children', () =>
expectMarkupMatch(
<div>
<div />
</div>,
<div suppressHydrationWarning={true}>
<div />
<div />
</div>,
));
it('can explicitly ignore reconnecting fewer children', () =>
expectMarkupMatch(
<div>
<div />
<div />
</div>,
<div suppressHydrationWarning={true}>
<div />
</div>,
));
it('can explicitly ignore reconnecting reordered children', () =>
expectMarkupMatch(
<div suppressHydrationWarning={true}>
<div />
<span />
</div>,
<div suppressHydrationWarning={true}>
<span />
<div />
</div>,
));
it('can not deeply ignore reconnecting reordered children', () =>
expectMarkupMismatch(
<div suppressHydrationWarning={true}>
<div>
<div />
<span />
</div>
</div>,
<div suppressHydrationWarning={true}>
<div>
<span />
<div />
</div>
</div>,
));
});
// Markup Mismatches: misc
it('should error reconnecting a div with different dangerouslySetInnerHTML', () =>
expectMarkupMismatch(
<div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
<div dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}} />,
));
it('can explicitly ignore reconnecting a div with different dangerouslySetInnerHTML', () =>
expectMarkupMatch(
<div dangerouslySetInnerHTML={{__html: "<span id='child1'/>"}} />,
<div
dangerouslySetInnerHTML={{__html: "<span id='child2'/>"}}
suppressHydrationWarning={true}
/>,
));
});
});
@@ -0,0 +1,99 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOM;
let ReactDOMServer;
function initModules() {
// Reset warning cache.
jest.resetModuleRegistry();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
};
}
const {
resetModules,
asyncReactDOMRender,
clientRenderOnServerString,
expectMarkupMatch,
} = ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerIntegration', () => {
beforeEach(() => {
resetModules();
});
describe('refs', function() {
it('should not run ref code on server', async () => {
let refCount = 0;
class RefsComponent extends React.Component {
render() {
return <div ref={e => refCount++} />;
}
}
await expectMarkupMatch(<RefsComponent />, <div />);
expect(refCount).toBe(0);
});
it('should run ref code on client', async () => {
let refCount = 0;
class RefsComponent extends React.Component {
render() {
return <div ref={e => refCount++} />;
}
}
await expectMarkupMatch(<div />, <RefsComponent />);
expect(refCount).toBe(1);
});
it('should send the correct element to ref functions on client', async () => {
let refElement = null;
class RefsComponent extends React.Component {
render() {
return <div ref={e => (refElement = e)} />;
}
}
const e = await clientRenderOnServerString(<RefsComponent />);
expect(refElement).not.toBe(null);
expect(refElement).toBe(e);
});
it('should have string refs on client when rendered over server markup', async () => {
class RefsComponent extends React.Component {
render() {
return <div ref="myDiv" />;
}
}
const markup = ReactDOMServer.renderToString(<RefsComponent />);
const root = document.createElement('div');
root.innerHTML = markup;
let component = null;
resetModules();
await asyncReactDOMRender(
<RefsComponent ref={e => (component = e)} />,
root,
true,
);
expect(component.refs.myDiv).toBe(root.firstChild);
});
});
});
+1 -3
View File
@@ -11,8 +11,6 @@
const {COMMENT_NODE} = require('../shared/HTMLNodeType');
const invariant = require('invariant');
var React;
var ReactDOM;
var ReactDOMServer;
@@ -351,7 +349,7 @@ describe('ReactMount', () => {
containerDiv = document.createElement('div');
containerDiv.innerHTML = 'A<!-- react-mount-point-unstable -->B';
mountPoint = containerDiv.childNodes[1];
invariant(mountPoint.nodeType === COMMENT_NODE, 'Expected comment');
expect(mountPoint.nodeType).toBe(COMMENT_NODE);
});
it('renders at a comment node', () => {
@@ -9,15 +9,12 @@
'use strict';
var ExecutionEnvironment;
var React;
var ReactDOM;
var ReactDOMServer;
var ReactTestUtils;
var PropTypes;
var ROOT_ATTRIBUTE_NAME;
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}
@@ -29,39 +26,26 @@ describe('ReactDOMServer', () => {
ReactDOM = require('react-dom');
ReactTestUtils = require('react-dom/test-utils');
PropTypes = require('prop-types');
ExecutionEnvironment = require('fbjs/lib/ExecutionEnvironment');
ExecutionEnvironment.canUseDOM = false;
ReactDOMServer = require('react-dom/server');
// TODO: can we express this test with only public API?
var DOMProperty = require('../shared/DOMProperty');
ROOT_ATTRIBUTE_NAME = DOMProperty.ROOT_ATTRIBUTE_NAME;
});
describe('renderToString', () => {
it('should generate simple markup', () => {
var response = ReactDOMServer.renderToString(<span>hello world</span>);
expect(response).toMatch(
new RegExp(
'<span ' + ROOT_ATTRIBUTE_NAME + '=""' + '>hello world</span>',
),
new RegExp('<span data-reactroot=""' + '>hello world</span>'),
);
});
it('should generate simple markup for self-closing tags', () => {
var response = ReactDOMServer.renderToString(<img />);
expect(response).toMatch(
new RegExp('<img ' + ROOT_ATTRIBUTE_NAME + '=""' + '/>'),
);
expect(response).toMatch(new RegExp('<img data-reactroot=""' + '/>'));
});
it('should generate simple markup for attribute with `>` symbol', () => {
var response = ReactDOMServer.renderToString(<img data-attr=">" />);
expect(response).toMatch(
new RegExp(
'<img data-attr="&gt;" ' + ROOT_ATTRIBUTE_NAME + '=""' + '/>',
),
new RegExp('<img data-attr="&gt;" data-reactroot=""' + '/>'),
);
});
@@ -99,7 +83,7 @@ describe('ReactDOMServer', () => {
expect(response).toMatch(
new RegExp(
'<div ' +
ROOT_ATTRIBUTE_NAME +
'data-reactroot' +
'=""' +
'>' +
'<span' +
@@ -161,7 +145,7 @@ describe('ReactDOMServer', () => {
expect(response).toMatch(
new RegExp(
'<span ' +
ROOT_ATTRIBUTE_NAME +
'data-reactroot' +
'=""' +
'>' +
'Component name: <!-- -->TestComponent' +
@@ -176,17 +160,11 @@ describe('ReactDOMServer', () => {
}
runTest();
// This should work the same regardless of whether you can use DOM or not.
ExecutionEnvironment.canUseDOM = true;
runTest();
});
it('should have the correct mounting behavior (old hydrate API)', () => {
spyOnDev(console, 'warn');
spyOnDev(console, 'error');
// This test is testing client-side behavior.
ExecutionEnvironment.canUseDOM = true;
var mountCount = 0;
var numClicks = 0;
@@ -234,9 +212,7 @@ describe('ReactDOMServer', () => {
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
ExecutionEnvironment.canUseDOM = false;
lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
ExecutionEnvironment.canUseDOM = true;
element.innerHTML = lastMarkup;
var instance = ReactDOM.render(<TestComponent name="x" />, element);
@@ -287,8 +263,6 @@ describe('ReactDOMServer', () => {
it('should have the correct mounting behavior (new hydrate API)', () => {
spyOnDev(console, 'error');
// This test is testing client-side behavior.
ExecutionEnvironment.canUseDOM = true;
var mountCount = 0;
var numClicks = 0;
@@ -336,9 +310,7 @@ describe('ReactDOMServer', () => {
ReactDOM.unmountComponentAtNode(element);
expect(element.innerHTML).toEqual('');
ExecutionEnvironment.canUseDOM = false;
lastMarkup = ReactDOMServer.renderToString(<TestComponent name="x" />);
ExecutionEnvironment.canUseDOM = true;
element.innerHTML = lastMarkup;
var instance = ReactDOM.hydrate(<TestComponent name="x" />, element);
@@ -536,10 +508,6 @@ describe('ReactDOMServer', () => {
}
runTest();
// This should work the same regardless of whether you can use DOM or not.
ExecutionEnvironment.canUseDOM = true;
runTest();
});
it('should throw with silly args', () => {
@@ -0,0 +1,328 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
const stream = require('stream');
module.exports = function(initModules) {
let ReactDOM;
let ReactDOMServer;
function resetModules() {
({ReactDOM, ReactDOMServer} = initModules());
}
// Helper functions for rendering tests
// ====================================
// promisified version of ReactDOM.render()
function asyncReactDOMRender(reactElement, domElement, forceHydrate) {
return new Promise(resolve => {
if (forceHydrate) {
ReactDOM.hydrate(reactElement, domElement);
} else {
ReactDOM.render(reactElement, domElement);
}
// We can't use the callback for resolution because that will not catch
// errors. They're thrown.
resolve();
});
}
// performs fn asynchronously and expects count errors logged to console.error.
// will fail the test if the count of errors logged is not equal to count.
async function expectErrors(fn, count) {
if (console.error.calls && console.error.calls.reset) {
console.error.calls.reset();
} else {
spyOnDev(console, 'error');
}
const result = await fn();
if (
console.error.calls &&
console.error.calls.count() !== count &&
console.error.calls.count() !== 0
) {
console.log(
`We expected ${
count
} warning(s), but saw ${console.error.calls.count()} warning(s).`,
);
if (console.error.calls.count() > 0) {
console.log(`We saw these warnings:`);
for (var i = 0; i < console.error.calls.count(); i++) {
console.log(console.error.calls.argsFor(i)[0]);
}
}
}
if (__DEV__) {
expect(console.error.calls.count()).toBe(count);
}
return result;
}
// renders the reactElement into domElement, and expects a certain number of errors.
// returns a Promise that resolves when the render is complete.
function renderIntoDom(
reactElement,
domElement,
forceHydrate,
errorCount = 0,
) {
return expectErrors(async () => {
await asyncReactDOMRender(reactElement, domElement, forceHydrate);
return domElement.firstChild;
}, errorCount);
}
async function renderIntoString(reactElement, errorCount = 0) {
return await expectErrors(
() =>
new Promise(resolve =>
resolve(ReactDOMServer.renderToString(reactElement)),
),
errorCount,
);
}
// Renders text using SSR and then stuffs it into a DOM node; returns the DOM
// element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
async function serverRender(reactElement, errorCount = 0) {
const markup = await renderIntoString(reactElement, errorCount);
var domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
}
// this just drains a readable piped into it to a string, which can be accessed
// via .buffer.
class DrainWritable extends stream.Writable {
constructor(options) {
super(options);
this.buffer = '';
}
_write(chunk, encoding, cb) {
this.buffer += chunk;
cb();
}
}
async function renderIntoStream(reactElement, errorCount = 0) {
return await expectErrors(
() =>
new Promise(resolve => {
let writable = new DrainWritable();
ReactDOMServer.renderToNodeStream(reactElement).pipe(writable);
writable.on('finish', () => resolve(writable.buffer));
}),
errorCount,
);
}
// Renders text using node stream SSR and then stuffs it into a DOM node;
// returns the DOM element that corresponds with the reactElement.
// Does not render on client or perform client-side revival.
async function streamRender(reactElement, errorCount = 0) {
const markup = await renderIntoStream(reactElement, errorCount);
var domElement = document.createElement('div');
domElement.innerHTML = markup;
return domElement.firstChild;
}
const clientCleanRender = (element, errorCount = 0) => {
const div = document.createElement('div');
return renderIntoDom(element, div, false, errorCount);
};
const clientRenderOnServerString = async (element, errorCount = 0) => {
const markup = await renderIntoString(element, errorCount);
resetModules();
var domElement = document.createElement('div');
domElement.innerHTML = markup;
let serverNode = domElement.firstChild;
const firstClientNode = await renderIntoDom(
element,
domElement,
true,
errorCount,
);
let clientNode = firstClientNode;
// Make sure all top level nodes match up
while (serverNode || clientNode) {
expect(serverNode != null).toBe(true);
expect(clientNode != null).toBe(true);
expect(clientNode.nodeType).toBe(serverNode.nodeType);
// Assert that the DOM element hasn't been replaced.
// Note that we cannot use expect(serverNode).toBe(clientNode) because
// of jest bug #1772.
expect(serverNode === clientNode).toBe(true);
serverNode = serverNode.nextSibling;
clientNode = clientNode.nextSibling;
}
return firstClientNode;
};
function BadMarkupExpected() {}
const clientRenderOnBadMarkup = async (element, errorCount = 0) => {
// First we render the top of bad mark up.
var domElement = document.createElement('div');
domElement.innerHTML =
'<div id="badIdWhichWillCauseMismatch" data-reactroot="" data-reactid="1"></div>';
await renderIntoDom(element, domElement, true, errorCount + 1);
// This gives us the resulting text content.
var hydratedTextContent = domElement.textContent;
// Next we render the element into a clean DOM node client side.
const cleanDomElement = document.createElement('div');
await asyncReactDOMRender(element, cleanDomElement, true);
// This gives us the expected text content.
const cleanTextContent = cleanDomElement.textContent;
// The only guarantee is that text content has been patched up if needed.
expect(hydratedTextContent).toBe(cleanTextContent);
// Abort any further expects. All bets are off at this point.
throw new BadMarkupExpected();
};
// runs a DOM rendering test as four different tests, with four different rendering
// scenarios:
// -- render to string on server
// -- render on client without any server markup "clean client render"
// -- render on client on top of good server-generated string markup
// -- render on client on top of bad server-generated markup
//
// testFn is a test that has one arg, which is a render function. the render
// function takes in a ReactElement and an optional expected error count and
// returns a promise of a DOM Element.
//
// You should only perform tests that examine the DOM of the results of
// render; you should not depend on the interactivity of the returned DOM element,
// as that will not work in the server string scenario.
function itRenders(desc, testFn) {
it(`renders ${desc} with server string render`, () => testFn(serverRender));
it(`renders ${desc} with server stream render`, () => testFn(streamRender));
itClientRenders(desc, testFn);
}
// run testFn in three different rendering scenarios:
// -- render on client without any server markup "clean client render"
// -- render on client on top of good server-generated string markup
// -- render on client on top of bad server-generated markup
//
// testFn is a test that has one arg, which is a render function. the render
// function takes in a ReactElement and an optional expected error count and
// returns a promise of a DOM Element.
//
// Since all of the renders in this function are on the client, you can test interactivity,
// unlike with itRenders.
function itClientRenders(desc, testFn) {
it(`renders ${desc} with clean client render`, () =>
testFn(clientCleanRender));
it(`renders ${desc} with client render on top of good server markup`, () =>
testFn(clientRenderOnServerString));
it(`renders ${
desc
} with client render on top of bad server markup`, async () => {
try {
await testFn(clientRenderOnBadMarkup);
} catch (x) {
// We expect this to trigger the BadMarkupExpected rejection.
if (!(x instanceof BadMarkupExpected)) {
// If not, rethrow.
throw x;
}
}
});
}
function itThrows(desc, testFn, partialMessage) {
it(`throws ${desc}`, () => {
return testFn().then(
() => expect(false).toBe('The promise resolved and should not have.'),
err => {
expect(err).toBeInstanceOf(Error);
expect(err.message).toContain(partialMessage);
},
);
});
}
function itThrowsWhenRendering(desc, testFn, partialMessage) {
itThrows(
`when rendering ${desc} with server string render`,
() => testFn(serverRender),
partialMessage,
);
itThrows(
`when rendering ${desc} with clean client render`,
() => testFn(clientCleanRender),
partialMessage,
);
// we subtract one from the warning count here because the throw means that it won't
// get the usual markup mismatch warning.
itThrows(
`when rendering ${desc} with client render on top of bad server markup`,
() =>
testFn((element, warningCount = 0) =>
clientRenderOnBadMarkup(element, warningCount - 1),
),
partialMessage,
);
}
// renders serverElement to a string, sticks it into a DOM element, and then
// tries to render clientElement on top of it. shouldMatch is a boolean
// telling whether we should expect the markup to match or not.
async function testMarkupMatch(serverElement, clientElement, shouldMatch) {
const domElement = await serverRender(serverElement);
resetModules();
return renderIntoDom(
clientElement,
domElement.parentNode,
true,
shouldMatch ? 0 : 1,
);
}
// expects that rendering clientElement on top of a server-rendered
// serverElement does NOT raise a markup mismatch warning.
function expectMarkupMatch(serverElement, clientElement) {
return testMarkupMatch(serverElement, clientElement, true);
}
// expects that rendering clientElement on top of a server-rendered
// serverElement DOES raise a markup mismatch warning.
function expectMarkupMismatch(serverElement, clientElement) {
return testMarkupMatch(serverElement, clientElement, false);
}
return {
resetModules,
expectMarkupMismatch,
expectMarkupMatch,
itRenders,
itClientRenders,
itThrowsWhenRendering,
asyncReactDOMRender,
serverRender,
clientRenderOnServerString,
renderIntoDom,
streamRender,
};
};
+1 -1
View File
@@ -67,7 +67,7 @@ if (__DEV__) {
var validatePropertiesInDevelopment = function(type, props) {
validateARIAProperties(type, props);
validateInputProperties(type, props);
validateUnknownProperties(type, props);
validateUnknownProperties(type, props, /* canUseEventSystem */ true);
};
// HTML parsing normalizes CR and CRLF to LF.
+1 -1
View File
@@ -54,7 +54,7 @@ if (__DEV__) {
var validatePropertiesInDevelopment = function(type, props) {
validateARIAProperties(type, props);
validateInputProperties(type, props);
validateUnknownProperties(type, props);
validateUnknownProperties(type, props, /* canUseEventSystem */ false);
};
var describeStackFrame = function(element): string {
+60 -51
View File
@@ -7,7 +7,6 @@
import {
registrationNameModules,
plugins,
possibleRegistrationNames,
} from 'events/EventPluginRegistry';
import {ReactDebugCurrentFrame} from 'shared/ReactGlobalSharedState';
@@ -30,60 +29,17 @@ function getStackAddendum() {
if (__DEV__) {
var warnedProperties = {};
var hasOwnProperty = Object.prototype.hasOwnProperty;
var EVENT_NAME_REGEX = /^on[A-Z]/;
var EVENT_NAME_REGEX = /^on./;
var INVALID_EVENT_NAME_REGEX = /^on[^A-Z]/;
var rARIA = new RegExp('^(aria)-[' + ATTRIBUTE_NAME_CHAR + ']*$');
var rARIACamel = new RegExp('^(aria)[A-Z][' + ATTRIBUTE_NAME_CHAR + ']*$');
var validateProperty = function(tagName, name, value) {
var validateProperty = function(tagName, name, value, canUseEventSystem) {
if (hasOwnProperty.call(warnedProperties, name) && warnedProperties[name]) {
return true;
}
if (registrationNameModules.hasOwnProperty(name)) {
return true;
}
if (plugins.length === 0 && EVENT_NAME_REGEX.test(name)) {
// If no event plugins have been injected, we might be in a server environment.
// Don't check events in this case.
return true;
}
var lowerCasedName = name.toLowerCase();
var registrationName = possibleRegistrationNames.hasOwnProperty(
lowerCasedName,
)
? possibleRegistrationNames[lowerCasedName]
: null;
if (registrationName != null) {
warning(
false,
'Invalid event handler property `%s`. Did you mean `%s`?%s',
name,
registrationName,
getStackAddendum(),
);
warnedProperties[name] = true;
return true;
}
if (lowerCasedName.indexOf('on') === 0 && lowerCasedName.length > 2) {
warning(
false,
'Unknown event handler property `%s`. It will be ignored.%s',
name,
getStackAddendum(),
);
warnedProperties[name] = true;
return true;
}
// Let the ARIA attribute hook validate ARIA attributes
if (rARIA.test(name) || rARIACamel.test(name)) {
return true;
}
if (lowerCasedName === 'onfocusin' || lowerCasedName === 'onfocusout') {
warning(
false,
@@ -95,6 +51,59 @@ if (__DEV__) {
return true;
}
// We can't rely on the event system being injected on the server.
if (canUseEventSystem) {
if (registrationNameModules.hasOwnProperty(name)) {
return true;
}
var registrationName = possibleRegistrationNames.hasOwnProperty(
lowerCasedName,
)
? possibleRegistrationNames[lowerCasedName]
: null;
if (registrationName != null) {
warning(
false,
'Invalid event handler property `%s`. Did you mean `%s`?%s',
name,
registrationName,
getStackAddendum(),
);
warnedProperties[name] = true;
return true;
}
if (EVENT_NAME_REGEX.test(name)) {
warning(
false,
'Unknown event handler property `%s`. It will be ignored.%s',
name,
getStackAddendum(),
);
warnedProperties[name] = true;
return true;
}
} else if (EVENT_NAME_REGEX.test(name)) {
// If no event plugins have been injected, we are in a server environment.
// So we can't tell if the event name is correct for sure, but we can filter
// out known bad ones like `onclick`. We can't suggest a specific replacement though.
if (INVALID_EVENT_NAME_REGEX.test(name)) {
warning(
false,
'Invalid event handler property `%s`. ' +
'React events use the camelCase naming convention, for example `onClick`.%s',
name,
getStackAddendum(),
);
}
warnedProperties[name] = true;
return true;
}
// Let the ARIA attribute hook validate ARIA attributes
if (rARIA.test(name) || rARIACamel.test(name)) {
return true;
}
if (lowerCasedName === 'innerhtml') {
warning(
false,
@@ -233,10 +242,10 @@ if (__DEV__) {
};
}
var warnUnknownProperties = function(type, props) {
var warnUnknownProperties = function(type, props, canUseEventSystem) {
var unknownProps = [];
for (var key in props) {
var isValid = validateProperty(type, key, props[key]);
var isValid = validateProperty(type, key, props[key], canUseEventSystem);
if (!isValid) {
unknownProps.push(key);
}
@@ -266,9 +275,9 @@ var warnUnknownProperties = function(type, props) {
}
};
export function validateProperties(type, props) {
export function validateProperties(type, props, canUseEventSystem) {
if (isCustomComponent(type, props)) {
return;
}
warnUnknownProperties(type, props);
warnUnknownProperties(type, props, canUseEventSystem);
}
+5 -1
View File
@@ -1,3 +1,7 @@
'use strict';
module.exports = require('./cjs/react-noop-renderer.development.js');
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react-noop-renderer.production.min.js');
} else {
module.exports = require('./cjs/react-noop-renderer.development.js');
}
+2 -1
View File
@@ -9,7 +9,8 @@
"dependencies": {
"fbjs": "^0.8.16",
"object-assign": "^4.1.1",
"prop-types": "^15.6.0"
"prop-types": "^15.6.0",
"regenerator-runtime": "^0.11.0"
},
"peerDependencies": {
"react": "^16.0.0"
@@ -21,10 +21,6 @@ describe('ReactIncrementalErrorHandling', () => {
ReactNoop = require('react-noop-renderer');
});
afterEach(() => {
jest.unmock('../ReactFiberErrorLogger');
});
function div(...children) {
children = children.map(c => (typeof c === 'string' ? {text: c} : c));
return {type: 'div', children, prop: undefined};
@@ -954,254 +950,4 @@ describe('ReactIncrementalErrorHandling', () => {
});
expect(() => ReactNoop.flush()).toThrow('Error!');
});
describe('ReactFiberErrorLogger', () => {
function normalizeCodeLocInfo(str) {
return str && str.replace(/\(at .+?:\d+\)/g, '(at **)');
}
it('should log errors that occur during the begin phase', () => {
// Intentionally spy in production too.
spyOn(console, 'error');
class ErrorThrowingComponent extends React.Component {
componentWillMount() {
const error = new Error('componentWillMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
}
render() {
return <div />;
}
}
try {
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
ReactNoop.flushDeferredPri();
} catch (error) {}
expect(console.error.calls.count()).toBe(1);
const errorMessage = console.error.calls.argsFor(0)[0];
if (__DEV__) {
expect(normalizeCodeLocInfo(errorMessage)).toContain(
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
' in ErrorThrowingComponent (at **)\n' +
' in span (at **)\n' +
' in div (at **)',
);
expect(errorMessage).toContain(
'Consider adding an error boundary to your tree to customize error handling behavior.',
);
} else {
expect(errorMessage.message).toContain('componentWillMount error');
}
});
it('should log errors that occur during the commit phase', () => {
// Intentionally spy in production too.
spyOn(console, 'error');
class ErrorThrowingComponent extends React.Component {
componentDidMount() {
const error = new Error('componentDidMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
}
render() {
return <div />;
}
}
try {
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
ReactNoop.flushDeferredPri();
} catch (error) {}
expect(console.error.calls.count()).toBe(1);
const errorMessage = console.error.calls.argsFor(0)[0];
if (__DEV__) {
expect(normalizeCodeLocInfo(errorMessage)).toContain(
'The above error occurred in the <ErrorThrowingComponent> component:\n' +
' in ErrorThrowingComponent (at **)\n' +
' in span (at **)\n' +
' in div (at **)',
);
expect(errorMessage).toContain(
'Consider adding an error boundary to your tree to customize error handling behavior.',
);
} else {
expect(errorMessage.message).toBe('componentDidMount error');
}
});
it('should ignore errors thrown in log method to prevent cycle', () => {
jest.resetModules();
jest.mock('../ReactFiberErrorLogger');
React = require('react');
ReactNoop = require('react-noop-renderer');
// Intentionally spy in production too.
spyOn(console, 'error');
class ErrorThrowingComponent extends React.Component {
render() {
throw new Error('render error');
}
}
const logCapturedErrorCalls = [];
const ReactFiberErrorLogger = require('../ReactFiberErrorLogger');
ReactFiberErrorLogger.logCapturedError.mockImplementation(
capturedError => {
logCapturedErrorCalls.push(capturedError);
const error = new Error('logCapturedError error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
},
);
try {
ReactNoop.render(
<div>
<span>
<ErrorThrowingComponent />
</span>
</div>,
);
ReactNoop.flushDeferredPri();
} catch (error) {}
expect(logCapturedErrorCalls.length).toBe(1);
// The error thrown in logCapturedError should also be logged
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0].message).toContain(
'logCapturedError error',
);
});
it('should relay info about error boundary and retry attempts if applicable', () => {
// Intentionally spy in production too.
spyOn(console, 'error');
class ParentComponent extends React.Component {
render() {
return <ErrorBoundaryComponent />;
}
}
let handleErrorCalls = [];
let renderAttempts = 0;
class ErrorBoundaryComponent extends React.Component {
componentDidCatch(error) {
handleErrorCalls.push(error);
this.setState({}); // Render again
}
render() {
return <ErrorThrowingComponent />;
}
}
class ErrorThrowingComponent extends React.Component {
componentDidMount() {
const error = new Error('componentDidMount error');
// Note: it's `true` on the Error prototype our test environment.
// That lets us avoid asserting on warnings for each expected error.
// Here we intentionally shadow it to test logging, like in real apps.
error.suppressReactErrorLogging = undefined;
throw error;
}
render() {
renderAttempts++;
return <div />;
}
}
try {
ReactNoop.render(<ParentComponent />);
ReactNoop.flush();
} catch (error) {}
expect(renderAttempts).toBe(2);
expect(handleErrorCalls.length).toBe(1);
expect(console.error.calls.count()).toBe(2);
if (__DEV__) {
expect(console.error.calls.argsFor(0)[0]).toContain(
'The above error occurred in the <ErrorThrowingComponent> component:',
);
expect(console.error.calls.argsFor(0)[0]).toContain(
'React will try to recreate this component tree from scratch ' +
'using the error boundary you provided, ErrorBoundaryComponent.',
);
expect(console.error.calls.argsFor(1)[0]).toContain(
'The above error occurred in the <ErrorThrowingComponent> component:',
);
expect(console.error.calls.argsFor(1)[0]).toContain(
'This error was initially handled by the error boundary ErrorBoundaryComponent.\n' +
'Recreating the tree from scratch failed so React will unmount the tree.',
);
}
});
});
it('resets instance variables before unmounting failed node', () => {
class ErrorBoundary extends React.Component {
state = {error: null};
componentDidCatch(error) {
this.setState({error});
}
render() {
return this.state.error ? null : this.props.children;
}
}
class Foo extends React.Component {
state = {step: 0};
componentDidMount() {
this.setState({step: 1});
}
componentWillUnmount() {
ReactNoop.yield('componentWillUnmount: ' + this.state.step);
}
render() {
ReactNoop.yield('render: ' + this.state.step);
if (this.state.step > 0) {
throw new Error('oops');
}
return null;
}
}
ReactNoop.render(
<ErrorBoundary>
<Foo />
</ErrorBoundary>,
);
expect(ReactNoop.flush()).toEqual([
'render: 0',
'render: 1',
'componentWillUnmount: 0',
]);
});
});
@@ -0,0 +1,104 @@
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
*/
'use strict';
var React;
var ReactTestUtils;
describe('ReactJSXElementValidator', () => {
beforeEach(() => {
jest.resetModules();
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableReactFragment = true;
React = require('react');
ReactTestUtils = require('react-dom/test-utils');
});
it('warns for fragments with illegal attributes', () => {
spyOnDev(console, 'error');
class Foo extends React.Component {
render() {
return (
<React.Fragment a={1} b={2}>
hello
</React.Fragment>
);
}
}
ReactTestUtils.renderIntoDocument(<Foo />);
if (__DEV__) {
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain('Invalid prop `');
expect(console.error.calls.argsFor(0)[0]).toContain(
'` supplied to `React.Fragment`. React.Fragment ' +
'can only have `key` and `children` props.',
);
}
});
it('warns for fragments with refs', () => {
spyOnDev(console, 'error');
class Foo extends React.Component {
render() {
return (
<React.Fragment
ref={bar => {
this.foo = bar;
}}>
hello
</React.Fragment>
);
}
}
ReactTestUtils.renderIntoDocument(<Foo />);
if (__DEV__) {
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Invalid attribute `ref` supplied to `React.Fragment`.',
);
}
});
it('does not warn for fragments of multiple elements without keys', () => {
ReactTestUtils.renderIntoDocument(
<React.Fragment>
<span>1</span>
<span>2</span>
</React.Fragment>,
);
});
it('warns for fragments of multiple elements with same key', () => {
spyOnDev(console, 'error');
ReactTestUtils.renderIntoDocument(
<React.Fragment>
<span key="a">1</span>
<span key="a">2</span>
<span key="b">3</span>
</React.Fragment>,
);
if (__DEV__) {
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Encountered two children with the same key, `a`.',
);
}
});
});
@@ -27,9 +27,6 @@ describe('ReactJSXElementValidator', () => {
beforeEach(() => {
jest.resetModules();
const ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableReactFragment = true;
PropTypes = require('prop-types');
React = require('react');
ReactDOM = require('react-dom');
@@ -92,57 +89,6 @@ describe('ReactJSXElementValidator', () => {
}
});
it('warns for fragments with illegal attributes', () => {
spyOnDev(console, 'error');
class Foo extends React.Component {
render() {
return (
<React.Fragment a={1} b={2}>
hello
</React.Fragment>
);
}
}
ReactTestUtils.renderIntoDocument(<Foo />);
if (__DEV__) {
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain('Invalid prop `');
expect(console.error.calls.argsFor(0)[0]).toContain(
'` supplied to `React.Fragment`. React.Fragment ' +
'can only have `key` and `children` props.',
);
}
});
it('warns for fragments with refs', () => {
spyOnDev(console, 'error');
class Foo extends React.Component {
render() {
return (
<React.Fragment
ref={bar => {
this.foo = bar;
}}>
hello
</React.Fragment>
);
}
}
ReactTestUtils.renderIntoDocument(<Foo />);
if (__DEV__) {
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Invalid attribute `ref` supplied to `React.Fragment`.',
);
}
});
it('warns for keys for iterables of elements in rest args', () => {
spyOnDev(console, 'error');
@@ -168,34 +114,6 @@ describe('ReactJSXElementValidator', () => {
}
});
it('does not warn for fragments of multiple elements without keys', () => {
ReactTestUtils.renderIntoDocument(
<React.Fragment>
<span>1</span>
<span>2</span>
</React.Fragment>,
);
});
it('warns for fragments of multiple elements with same key', () => {
spyOnDev(console, 'error');
ReactTestUtils.renderIntoDocument(
<React.Fragment>
<span key="a">1</span>
<span key="a">2</span>
<span key="b">3</span>
</React.Fragment>,
);
if (__DEV__) {
expect(console.error.calls.count()).toBe(1);
expect(console.error.calls.argsFor(0)[0]).toContain(
'Encountered two children with the same key, `a`.',
);
}
});
it('does not warn for arrays of elements with keys', () => {
ReactTestUtils.renderIntoDocument(
<Component>{[<Component key="#1" />, <Component key="#2" />]}</Component>,
+15 -15
View File
@@ -4,29 +4,29 @@ set -e
COMMANDS_TO_RUN=()
if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('./scripts/circleci/test_coverage.sh')
fi
if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('yarn test-prod --runInBand')
fi
if [ $((3 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
fi
# These seem out of order but extract-errors must be run after jest.
if [ $((0 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('node ./scripts/prettier/index')
COMMANDS_TO_RUN+=('node ./scripts/tasks/flow')
COMMANDS_TO_RUN+=('node ./scripts/tasks/eslint')
COMMANDS_TO_RUN+=('yarn test --runInBand')
COMMANDS_TO_RUN+=('./scripts/circleci/build.sh')
COMMANDS_TO_RUN+=('./scripts/circleci/check_license.sh')
COMMANDS_TO_RUN+=('./scripts/circleci/check_modules.sh')
COMMANDS_TO_RUN+=('./scripts/circleci/test_print_warnings.sh')
COMMANDS_TO_RUN+=('./scripts/circleci/track_stats.sh')
# COMMANDS_TO_RUN+=('./scripts/circleci/bench.sh')
fi
if [ $((1 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('yarn test-prod --runInBand')
fi
if [ $((2 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('./scripts/circleci/build.sh')
COMMANDS_TO_RUN+=('yarn test-build --runInBand')
COMMANDS_TO_RUN+=('yarn test-build-prod --runInBand')
fi
if [ $((3 % CIRCLE_NODE_TOTAL)) -eq "$CIRCLE_NODE_INDEX" ]; then
COMMANDS_TO_RUN+=('./scripts/circleci/test_coverage.sh')
fi
RETURN_CODES=()
+32
View File
@@ -0,0 +1,32 @@
'use strict';
const {readdirSync, statSync} = require('fs');
const {join} = require('path');
const sourceConfig = require('./config.source');
// Find all folders in packages/* with package.json
const packagesRoot = join(__dirname, '..', '..', 'packages');
const packages = readdirSync(packagesRoot).filter(dir => {
if (dir.charAt(0) === '.') {
return false;
}
const packagePath = join(packagesRoot, dir, 'package.json');
return statSync(packagePath).isFile();
});
// Create a module map to point React packages to the build output
const moduleNameMapper = {};
packages.forEach(name => {
// Root entry point
moduleNameMapper[`^${name}$`] = `<rootDir>/build/packages/${name}`;
// Named entry points
moduleNameMapper[`^${name}/(.*)$`] = `<rootDir>/build/packages/${name}/$1`;
});
module.exports = Object.assign({}, sourceConfig, {
// Redirect imports to the compiled bundles
moduleNameMapper,
// Don't run bundle tests on blacklisted -test.internal.* files
testPathIgnorePatterns: ['/node_modules/', '-test.internal.js$'],
// Exclude the build output from transforms
transformIgnorePatterns: ['/node_modules/', '<rootDir>/build/'],
});
+20
View File
@@ -0,0 +1,20 @@
'use strict';
module.exports = {
modulePathIgnorePatterns: [
'<rootDir>/scripts/rollup/shims/',
'<rootDir>/scripts/bench/',
],
transform: {
'.*': require.resolve('./preprocessor.js'),
},
setupFiles: [require.resolve('./setupEnvironment.js')],
setupTestFrameworkScriptFile: require.resolve('./setupTests.js'),
// Only include files directly in __tests__, not in nested folders.
testRegex: '/__tests__/[^/]*(\\.js|\\.coffee|[^d]\\.ts)$',
moduleFileExtensions: ['js', 'json', 'node', 'coffee', 'ts'],
rootDir: process.cwd(),
roots: ['<rootDir>/packages', '<rootDir>/scripts'],
collectCoverageFrom: ['packages/**/*.js'],
timers: 'fake',
};
+1 -1
View File
@@ -5,7 +5,7 @@ var path = require('path');
var babel = require('babel-core');
var coffee = require('coffee-script');
var tsPreprocessor = require('./ts-preprocessor');
var tsPreprocessor = require('./typescript/preprocessor');
var createCacheKeyFunction = require('fbjs-scripts/jest/createCacheKeyFunction');
// Use require.resolve to be resilient to file moves, npm updates, etc
@@ -3,7 +3,7 @@
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
// Inside the class equivalence tester, we have a custom environment, let's
// require that instead.
require('./setupSpecEquivalenceReporter.js');
require('./spec-equivalence-reporter/setupTests.js');
} else {
var env = jasmine.getEnv();
var errorMap = require('../error-codes/codes.json');
@@ -75,6 +75,11 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
};
const OriginalError = global.Error;
const ErrorProxy = new Proxy(OriginalError, {
apply(target, thisArg, argumentsList) {
const error = Reflect.apply(target, thisArg, argumentsList);
error.message = decodeErrorMessage(error.message);
return error;
},
construct(target, argumentsList, newTarget) {
const error = Reflect.construct(target, argumentsList, newTarget);
error.message = decodeErrorMessage(error.message);
+15 -2
View File
@@ -178,14 +178,27 @@ const bundles = [
externals: ['react'],
},
/******* React Noop Renderer (used only for fixtures/fiber-debugger) *******/
/******* React Noop Renderer (used for tests) *******/
{
label: 'noop',
bundleTypes: [NODE_DEV],
bundleTypes: [NODE_DEV, NODE_PROD],
moduleType: RENDERER,
entry: 'react-noop-renderer',
global: 'ReactNoopRenderer',
externals: ['react', 'expect'],
// React Noop uses generators. However GCC currently
// breaks when we attempt to use them in the output.
// So we precompile them with regenerator, and include
// it as a runtime dependency of React Noop. In practice
// this isn't an issue because React Noop is only used
// in our tests. We wouldn't want to do this for any
// public package though.
babel: opts =>
Object.assign({}, opts, {
plugins: opts.plugins.concat([
require.resolve('babel-plugin-transform-regenerator'),
]),
}),
},
/******* React Reconciler *******/
+49 -45
View File
@@ -9,12 +9,12 @@
"gzip": 2820
},
"react.development.js (NODE_DEV)": {
"size": 45226,
"gzip": 12653
"size": 45228,
"gzip": 12654
},
"react.production.min.js (NODE_PROD)": {
"size": 5398,
"gzip": 2384
"size": 5399,
"gzip": 2385
},
"React-dev.js (FB_DEV)": {
"size": 44902,
@@ -33,12 +33,12 @@
"gzip": 30731
},
"react-dom.development.js (NODE_DEV)": {
"size": 550734,
"gzip": 128057
"size": 550933,
"gzip": 128104
},
"react-dom.production.min.js (NODE_PROD)": {
"size": 93027,
"gzip": 29952
"size": 93102,
"gzip": 29993
},
"ReactDOM-dev.js (FB_DEV)": {
"size": 575937,
@@ -49,12 +49,12 @@
"gzip": 51733
},
"react-dom-test-utils.development.js (NODE_DEV)": {
"size": 36191,
"gzip": 10293
"size": 36193,
"gzip": 10294
},
"react-dom-test-utils.production.min.js (NODE_PROD)": {
"size": 10188,
"gzip": 3835
"size": 10189,
"gzip": 3836
},
"ReactTestUtils-dev.js (FB_DEV)": {
"size": 36913,
@@ -69,12 +69,12 @@
"gzip": 3901
},
"react-dom-unstable-native-dependencies.development.js (NODE_DEV)": {
"size": 58979,
"gzip": 15256
"size": 58981,
"gzip": 15257
},
"react-dom-unstable-native-dependencies.production.min.js (NODE_PROD)": {
"size": 10909,
"gzip": 3772
"size": 10910,
"gzip": 3773
},
"ReactDOMUnstableNativeDependencies-dev.js (FB_DEV)": {
"size": 60056,
@@ -93,12 +93,12 @@
"gzip": 5865
},
"react-dom-server.browser.development.js (NODE_DEV)": {
"size": 83887,
"gzip": 22828
"size": 83924,
"gzip": 22831
},
"react-dom-server.browser.production.min.js (NODE_PROD)": {
"size": 14263,
"gzip": 5748
"size": 14235,
"gzip": 5744
},
"ReactDOMServer-dev.js (FB_DEV)": {
"size": 87596,
@@ -109,11 +109,11 @@
"gzip": 8156
},
"react-dom-server.node.development.js (NODE_DEV)": {
"size": 85855,
"gzip": 23337
"size": 85892,
"gzip": 23339
},
"react-dom-server.node.production.min.js (NODE_PROD)": {
"size": 15088,
"size": 15060,
"gzip": 6052
},
"react-art.development.js (UMD_DEV)": {
@@ -125,12 +125,12 @@
"gzip": 25785
},
"react-art.development.js (NODE_DEV)": {
"size": 283633,
"gzip": 60512
"size": 283832,
"gzip": 60561
},
"react-art.production.min.js (NODE_PROD)": {
"size": 47631,
"gzip": 14903
"size": 47706,
"gzip": 14932
},
"ReactART-dev.js (FB_DEV)": {
"size": 294853,
@@ -165,48 +165,48 @@
"gzip": 21823
},
"react-test-renderer.development.js (NODE_DEV)": {
"size": 280201,
"gzip": 59318
"size": 280400,
"gzip": 59367
},
"react-test-renderer.production.min.js (NODE_PROD)": {
"size": 45987,
"gzip": 14196
"size": 46062,
"gzip": 14232
},
"ReactTestRenderer-dev.js (FB_DEV)": {
"size": 291517,
"gzip": 61172
},
"react-test-renderer-shallow.development.js (NODE_DEV)": {
"size": 9686,
"gzip": 2628
"size": 10233,
"gzip": 2761
},
"react-test-renderer-shallow.production.min.js (NODE_PROD)": {
"size": 4865,
"gzip": 1795
"size": 5159,
"gzip": 1880
},
"ReactShallowRenderer-dev.js (FB_DEV)": {
"size": 9917,
"gzip": 2596
},
"react-noop-renderer.development.js (NODE_DEV)": {
"size": 276892,
"gzip": 58307
"size": 277870,
"gzip": 58556
},
"react-reconciler.development.js (NODE_DEV)": {
"size": 262062,
"gzip": 54933
"size": 262259,
"gzip": 54981
},
"react-reconciler.production.min.js (NODE_PROD)": {
"size": 39304,
"gzip": 12212
"size": 39382,
"gzip": 12251
},
"react-call-return.development.js (NODE_DEV)": {
"size": 2463,
"gzip": 918
"size": 2465,
"gzip": 919
},
"react-call-return.production.min.js (NODE_PROD)": {
"size": 880,
"gzip": 499
"size": 881,
"gzip": 500
},
"react-dom-test-utils.development.js (UMD_DEV)": {
"size": 41456,
@@ -215,6 +215,10 @@
"react-dom-test-utils.production.min.js (UMD_PROD)": {
"size": 10617,
"gzip": 3917
},
"react-noop-renderer.production.min.js (NODE_PROD)": {
"size": 45070,
"gzip": 14103
}
}
}
+17
View File
@@ -53,6 +53,15 @@ ${license}
'use strict';
${
globalName === 'ReactNoopRenderer'
? // React Noop needs regenerator runtime because it uses
// generators but GCC doesn't handle them in the output.
// So we use Babel for them.
`const regeneratorRuntime = require("regenerator-runtime");`
: ``
}
if (process.env.NODE_ENV !== "production") {
(function() {
${source}
@@ -67,6 +76,14 @@ ${source}
*
${license}
*/
${
globalName === 'ReactNoopRenderer'
? // React Noop needs regenerator runtime because it uses
// generators but GCC doesn't handle them in the output.
// So we use Babel for them.
`const regeneratorRuntime = require("regenerator-runtime");`
: ``
}
${source}`;
},
+39 -1
View File
@@ -757,6 +757,12 @@ babel-plugin-transform-react-jsx@^6.24.1:
babel-plugin-syntax-jsx "^6.8.0"
babel-runtime "^6.22.0"
babel-plugin-transform-regenerator@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f"
dependencies:
regenerator-transform "^0.10.0"
babel-plugin-transform-strict-mode@^6.24.1:
version "6.24.1"
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:
mkdirp "^0.5.1"
source-map-support "^0.4.2"
babel-runtime@^6.18.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
babel-runtime@^6.22.0:
version "6.23.0"
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:
lodash "^4.2.0"
to-fast-properties "^1.0.1"
babel-types@^6.19.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies:
babel-runtime "^6.26.0"
esutils "^2.0.2"
lodash "^4.17.4"
to-fast-properties "^1.0.3"
babel@^5.4.7:
version "5.8.38"
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:
version "3.10.1"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.2.0, lodash@^4.3.0:
lodash@^4.0.0, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
@@ -3906,6 +3928,18 @@ regenerator-runtime@^0.10.0:
version "0.10.4"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.4.tgz#74cb6598d3ba2eb18694e968a40e2b3b4df9cf93"
regenerator-runtime@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1"
regenerator-transform@^0.10.0:
version "0.10.1"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
dependencies:
babel-runtime "^6.18.0"
babel-types "^6.19.0"
private "^0.1.6"
regenerator@0.8.40:
version "0.8.40"
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:
version "1.0.2"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.2.tgz#f3f5c0c3ba7299a7ef99427e44633257ade43320"
to-fast-properties@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
tough-cookie@^2.3.2, tough-cookie@~2.3.0:
version "2.3.2"
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.2.tgz#f081f76e4c85720e6c37a5faced737150d84072a"