diff --git a/CHANGELOG.md b/CHANGELOG.md index 524d48696a..8fabd3654f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,27 @@ +## 15.1.0 (May 20, 2016) + +### React +- Ensure we're using the latest `object-assign`, which has protection against a non-spec-compliant native `Object.assign`. ([@zpao](https://github.com/zpao) in [#6681](https://github.com/facebook/react/pull/6681)) +- Add a new warning to communicate that `props` objects passed to `createElement` must be plain objects. ([@richardscarrott](https://github.com/richardscarrott) in [#6134](https://github.com/facebook/react/pull/6134)) +- Fix a batching bug resulting in some lifecycle methods incorrectly being called multiple times. ([@spicyj](https://github.com/spicyj) in [#6650](https://github.com/facebook/react/pull/6650)) + +### React DOM +- Fix regression in custom elements support. ([@jscissr](https://github.com/jscissr) in [#6570](https://github.com/facebook/react/pull/6570)) +- Stop incorrectly warning about using `onScroll` event handler with server rendering. ([@Aweary](https://github.com/Aweary) in [#6678](https://github.com/facebook/react/pull/6678)) +- Fix grammar in the controlled input warning. ([@jakeboone02](https://github.com/jakeboone02) in [#6657](https://github.com/facebook/react/pull/6657)) +- Fix issue preventing `` nodes from being able to read `` nodes in IE. ([@syranide](https://github.com/syranide) in [#6691](https://github.com/facebook/react/pull/6691)) +- Fix issue resulting in crash when using experimental error boundaries with server rendering. ([@jimfb](https://github.com/jimfb) in [#6694](https://github.com/facebook/react/pull/6694)) +- Add additional information to the controlled input warning. ([@borisyankov](https://github.com/borisyankov) in [#6341](https://github.com/facebook/react/pull/6341)) + +### React Perf Add-on +- Completely rewritten to collect data more accurately and to be easier to maintain. ([@gaearon](https://github.com/gaearon) in [#6647](https://github.com/facebook/react/pull/6647), [#6046](https://github.com/facebook/react/pull/6046)) + +### React Native Renderer +- Remove some special cases for platform specific branching. ([@sebmarkbage](https://github.com/sebmarkbage) in [#6660](https://github.com/facebook/react/pull/6660)) +- Remove use of `merge` utility. ([@sebmarkbage](https://github.com/sebmarkbage) in [#6634](https://github.com/facebook/react/pull/6634)) +- Renamed some modules to better indicate usage ([@javache](https://github.com/javache) in [#6643](https://github.com/facebook/react/pull/6643)) + + ## 15.0.2 (April 29, 2016) ### React diff --git a/examples/basic-commonjs/package.json b/examples/basic-commonjs/package.json index 89490abdf9..e42eb80de4 100644 --- a/examples/basic-commonjs/package.json +++ b/examples/basic-commonjs/package.json @@ -6,11 +6,11 @@ "dependencies": { "babel-preset-es2015": "^6.6.0", "babel-preset-react": "^6.5.0", - "babelify": "^7.2.0", - "browserify": "^11.0.1", - "react": "15.0.1", - "react-dom": "15.0.1", - "watchify": "^3.4.0" + "babelify": "^7.3.0", + "browserify": "^13.0.0", + "react": "^15.0.2", + "react-dom": "^15.0.2", + "watchify": "^3.7.0" }, "scripts": { "build": "browserify ./index.js -t babelify -o bundle.js", diff --git a/grunt/config/compress.js b/grunt/config/compress.js index f225443776..020fe3d425 100644 --- a/grunt/config/compress.js +++ b/grunt/config/compress.js @@ -10,7 +10,7 @@ module.exports = { archive: './build/react-' + version + '.zip', }, files: [ - {cwd: './build/starter', src: ['**'], dest: 'react-' + version + '/'}, + {cwd: './build/starter', src: ['**'], dot: true, dest: 'react-' + version + '/'}, ], }, }; diff --git a/grunt/tasks/jest.js b/grunt/tasks/jest.js index da406e9111..fb8c1113ab 100644 --- a/grunt/tasks/jest.js +++ b/grunt/tasks/jest.js @@ -77,7 +77,7 @@ function writeTempConfig(callback) { function run(done, configPath) { grunt.log.writeln('running jest (this may take a while)'); - var args = ['--harmony', path.join('node_modules', 'jest-cli', 'bin', 'jest')]; + var args = ['--harmony', path.join('node_modules', 'jest-cli', 'bin', 'jest'), '--runInBand']; if (configPath) { args.push('--config', configPath); } diff --git a/grunt/tasks/npm-react-addons.js b/grunt/tasks/npm-react-addons.js index 948acebf1c..0f3d088d9c 100644 --- a/grunt/tasks/npm-react-addons.js +++ b/grunt/tasks/npm-react-addons.js @@ -16,7 +16,7 @@ var addons = { docs: 'two-way-binding-helpers', }, Perf: { - module: 'ReactDefaultPerf', + module: 'ReactPerf', name: 'perf', docs: 'perf', }, diff --git a/gulpfile.js b/gulpfile.js index 2c2a7c9823..c3db76382c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -39,12 +39,7 @@ var whiteListNames = [ 'deepFreezeAndThrowOnMutationInDev', 'flattenStyle', 'InitializeJavaScriptAppEngine', - 'InteractionManager', - 'JSTimersExecution', - 'merge', - 'Platform', 'RCTEventEmitter', - 'RCTLog', 'TextInputState', 'UIManager', 'View', diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3fc4f414a5..df73d752b8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -1,6 +1,6 @@ { "name": "react-build", - "version": "15.0.1", + "version": "15.0.3-alpha.1", "dependencies": { "async": { "version": "1.5.2", @@ -13262,9 +13262,9 @@ } }, "jest-cli": { - "version": "0.9.2", - "from": "jest-cli@>=0.9.0 <0.10.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-0.9.2.tgz", + "version": "12.0.2", + "from": "jest-cli@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-12.0.2.tgz", "dependencies": { "chalk": { "version": "1.1.3", @@ -13352,9 +13352,9 @@ "resolved": "https://registry.npmjs.org/diff/-/diff-2.2.2.tgz" }, "graceful-fs": { - "version": "4.1.3", + "version": "4.1.4", "from": "graceful-fs@>=4.1.3 <5.0.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.3.tgz" + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.4.tgz" }, "istanbul": { "version": "0.4.3", @@ -13443,14 +13443,14 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-2.0.10.tgz", "dependencies": { "brace-expansion": { - "version": "1.1.3", + "version": "1.1.4", "from": "brace-expansion@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.3.tgz", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.4.tgz", "dependencies": { "balanced-match": { - "version": "0.3.0", - "from": "balanced-match@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.3.0.tgz" + "version": "0.4.1", + "from": "balanced-match@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.1.tgz" }, "concat-map": { "version": "0.0.1", @@ -13512,13 +13512,13 @@ } }, "js-yaml": { - "version": "3.5.5", + "version": "3.6.0", "from": "js-yaml@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.5.5.tgz", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.0.tgz", "dependencies": { "argparse": { "version": "1.0.7", - "from": "argparse@>=1.0.2 <2.0.0", + "from": "argparse@>=1.0.7 <2.0.0", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.7.tgz", "dependencies": { "sprintf-js": { @@ -13566,487 +13566,551 @@ } } }, - "jsdom": { - "version": "7.2.2", - "from": "jsdom@>=7.2.0 <8.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-7.2.2.tgz", + "jest-environment-jsdom": { + "version": "12.0.2", + "from": "jest-environment-jsdom@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-12.0.2.tgz", "dependencies": { - "abab": { - "version": "1.0.3", - "from": "abab@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz" - }, - "acorn": { - "version": "2.7.0", - "from": "acorn@>=2.4.0 <3.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz" - }, - "acorn-globals": { - "version": "1.0.9", - "from": "acorn-globals@>=1.0.4 <2.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz" - }, - "cssom": { - "version": "0.3.1", - "from": "cssom@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.1.tgz" - }, - "cssstyle": { - "version": "0.2.34", - "from": "cssstyle@>=0.2.29 <0.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.34.tgz" - }, - "escodegen": { - "version": "1.8.0", - "from": "escodegen@>=1.6.1 <2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.0.tgz", + "jsdom": { + "version": "8.5.0", + "from": "jsdom@>=8.4.1 <9.0.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-8.5.0.tgz", "dependencies": { - "estraverse": { - "version": "1.9.3", - "from": "estraverse@>=1.9.1 <2.0.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz" + "abab": { + "version": "1.0.3", + "from": "abab@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.3.tgz" }, - "esutils": { - "version": "2.0.2", - "from": "esutils@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz" + "acorn": { + "version": "2.7.0", + "from": "acorn@>=2.4.0 <3.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-2.7.0.tgz" }, - "esprima": { - "version": "2.7.2", - "from": "esprima@>=2.7.1 <3.0.0", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" + "acorn-globals": { + "version": "1.0.9", + "from": "acorn-globals@>=1.0.4 <2.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-1.0.9.tgz" }, - "optionator": { - "version": "0.8.1", - "from": "optionator@>=0.8.1 <0.9.0", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz", + "array-equal": { + "version": "1.0.0", + "from": "array-equal@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz" + }, + "cssom": { + "version": "0.3.1", + "from": "cssom@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.1.tgz" + }, + "cssstyle": { + "version": "0.2.34", + "from": "cssstyle@>=0.2.34 <0.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-0.2.34.tgz" + }, + "escodegen": { + "version": "1.8.0", + "from": "escodegen@>=1.6.1 <2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.0.tgz", "dependencies": { - "prelude-ls": { - "version": "1.1.2", - "from": "prelude-ls@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + "estraverse": { + "version": "1.9.3", + "from": "estraverse@>=1.9.1 <2.0.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz" }, - "deep-is": { - "version": "0.1.3", - "from": "deep-is@>=0.1.3 <0.2.0", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" + "esutils": { + "version": "2.0.2", + "from": "esutils@>=2.0.2 <3.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz" }, - "wordwrap": { - "version": "1.0.0", - "from": "wordwrap@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + "esprima": { + "version": "2.7.2", + "from": "esprima@>=2.7.1 <3.0.0", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.2.tgz" }, - "type-check": { - "version": "0.3.2", - "from": "type-check@>=0.3.2 <0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" - }, - "levn": { - "version": "0.3.0", - "from": "levn@>=0.3.0 <0.4.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" - }, - "fast-levenshtein": { - "version": "1.1.3", - "from": "fast-levenshtein@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.3.tgz" - } - } - }, - "source-map": { - "version": "0.2.0", - "from": "source-map@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "dependencies": { - "amdefine": { - "version": "1.0.0", - "from": "amdefine@>=0.0.4", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" - } - } - } - } - }, - "nwmatcher": { - "version": "1.3.7", - "from": "nwmatcher@>=1.3.7 <2.0.0", - "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.3.7.tgz" - }, - "parse5": { - "version": "1.5.1", - "from": "parse5@>=1.5.1 <2.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz" - }, - "request": { - "version": "2.70.0", - "from": "request@>=2.55.0 <3.0.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.70.0.tgz", - "dependencies": { - "aws-sign2": { - "version": "0.6.0", - "from": "aws-sign2@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" - }, - "aws4": { - "version": "1.3.2", - "from": "aws4@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.3.2.tgz", - "dependencies": { - "lru-cache": { - "version": "4.0.1", - "from": "lru-cache@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.1.tgz", + "optionator": { + "version": "0.8.1", + "from": "optionator@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.1.tgz", "dependencies": { - "pseudomap": { - "version": "1.0.2", - "from": "pseudomap@>=1.0.1 <2.0.0", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" + "prelude-ls": { + "version": "1.1.2", + "from": "prelude-ls@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" }, - "yallist": { - "version": "2.0.0", - "from": "yallist@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.0.0.tgz" - } - } - } - } - }, - "bl": { - "version": "1.1.2", - "from": "bl@>=1.1.2 <1.2.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", - "dependencies": { - "readable-stream": { - "version": "2.0.6", - "from": "readable-stream@>=2.0.5 <2.1.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "from": "core-util-is@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + "deep-is": { + "version": "0.1.3", + "from": "deep-is@>=0.1.3 <0.2.0", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" }, - "inherits": { - "version": "2.0.1", - "from": "inherits@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" - }, - "isarray": { + "wordwrap": { "version": "1.0.0", - "from": "isarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + "from": "wordwrap@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" }, - "process-nextick-args": { - "version": "1.0.6", - "from": "process-nextick-args@>=1.0.6 <1.1.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.6.tgz" + "type-check": { + "version": "0.3.2", + "from": "type-check@>=0.3.2 <0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" }, - "string_decoder": { - "version": "0.10.31", - "from": "string_decoder@>=0.10.0 <0.11.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + "levn": { + "version": "0.3.0", + "from": "levn@>=0.3.0 <0.4.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" }, - "util-deprecate": { - "version": "1.0.2", - "from": "util-deprecate@>=1.0.1 <1.1.0", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" - } - } - } - } - }, - "caseless": { - "version": "0.11.0", - "from": "caseless@>=0.11.0 <0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" - }, - "combined-stream": { - "version": "1.0.5", - "from": "combined-stream@>=1.0.5 <1.1.0", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "dependencies": { - "delayed-stream": { - "version": "1.0.0", - "from": "delayed-stream@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" - } - } - }, - "extend": { - "version": "3.0.0", - "from": "extend@>=3.0.0 <3.1.0", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" - }, - "forever-agent": { - "version": "0.6.1", - "from": "forever-agent@>=0.6.1 <0.7.0", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" - }, - "form-data": { - "version": "1.0.0-rc4", - "from": "form-data@>=1.0.0-rc3 <1.1.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz" - }, - "har-validator": { - "version": "2.0.6", - "from": "har-validator@>=2.0.6 <2.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "dependencies": { - "commander": { - "version": "2.9.0", - "from": "commander@>=2.9.0 <3.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "dependencies": { - "graceful-readlink": { - "version": "1.0.1", - "from": "graceful-readlink@>=1.0.0", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + "fast-levenshtein": { + "version": "1.1.3", + "from": "fast-levenshtein@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-1.1.3.tgz" } } }, - "is-my-json-valid": { - "version": "2.13.1", - "from": "is-my-json-valid@>=2.12.4 <3.0.0", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz", + "source-map": { + "version": "0.2.0", + "from": "source-map@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", "dependencies": { - "generate-function": { - "version": "2.0.0", - "from": "generate-function@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" - }, - "generate-object-property": { - "version": "1.2.0", - "from": "generate-object-property@>=1.1.0 <2.0.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "amdefine": { + "version": "1.0.0", + "from": "amdefine@>=0.0.4", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.0.tgz" + } + } + } + } + }, + "iconv-lite": { + "version": "0.4.13", + "from": "iconv-lite@>=0.4.13 <0.5.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.13.tgz" + }, + "nwmatcher": { + "version": "1.3.7", + "from": "nwmatcher@>=1.3.7 <2.0.0", + "resolved": "https://registry.npmjs.org/nwmatcher/-/nwmatcher-1.3.7.tgz" + }, + "parse5": { + "version": "1.5.1", + "from": "parse5@>=1.5.1 <2.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz" + }, + "request": { + "version": "2.72.0", + "from": "request@>=2.55.0 <3.0.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.72.0.tgz", + "dependencies": { + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "aws4": { + "version": "1.4.1", + "from": "aws4@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.4.1.tgz" + }, + "bl": { + "version": "1.1.2", + "from": "bl@>=1.1.2 <1.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "dependencies": { + "readable-stream": { + "version": "2.0.6", + "from": "readable-stream@>=2.0.5 <2.1.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", "dependencies": { - "is-property": { + "core-util-is": { "version": "1.0.2", - "from": "is-property@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + "from": "core-util-is@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, + "isarray": { + "version": "1.0.0", + "from": "isarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + }, + "process-nextick-args": { + "version": "1.0.7", + "from": "process-nextick-args@>=1.0.6 <1.1.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz" + }, + "string_decoder": { + "version": "0.10.31", + "from": "string_decoder@>=0.10.0 <0.11.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" + }, + "util-deprecate": { + "version": "1.0.2", + "from": "util-deprecate@>=1.0.1 <1.1.0", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + } + } + } + } + }, + "caseless": { + "version": "0.11.0", + "from": "caseless@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "dependencies": { + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + } + } + }, + "extend": { + "version": "3.0.0", + "from": "extend@>=3.0.0 <3.1.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "form-data": { + "version": "1.0.0-rc4", + "from": "form-data@>=1.0.0-rc3 <1.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz" + }, + "har-validator": { + "version": "2.0.6", + "from": "har-validator@>=2.0.6 <2.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "dependencies": { + "commander": { + "version": "2.9.0", + "from": "commander@>=2.9.0 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "dependencies": { + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" } } }, - "jsonpointer": { - "version": "2.0.0", - "from": "jsonpointer@2.0.0", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" - }, - "xtend": { - "version": "4.0.1", - "from": "xtend@>=4.0.0 <5.0.0", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" - } - } - }, - "pinkie-promise": { - "version": "2.0.0", - "from": "pinkie-promise@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.0.tgz", - "dependencies": { - "pinkie": { - "version": "2.0.4", - "from": "pinkie@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" - } - } - } - } - }, - "hawk": { - "version": "3.1.3", - "from": "hawk@>=3.1.3 <3.2.0", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "dependencies": { - "hoek": { - "version": "2.16.3", - "from": "hoek@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" - }, - "boom": { - "version": "2.10.1", - "from": "boom@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" - }, - "cryptiles": { - "version": "2.0.5", - "from": "cryptiles@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" - }, - "sntp": { - "version": "1.0.9", - "from": "sntp@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" - } - } - }, - "http-signature": { - "version": "1.1.1", - "from": "http-signature@>=1.1.0 <1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "dependencies": { - "assert-plus": { - "version": "0.2.0", - "from": "assert-plus@>=0.2.0 <0.3.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" - }, - "jsprim": { - "version": "1.2.2", - "from": "jsprim@>=1.2.2 <2.0.0", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz", - "dependencies": { - "extsprintf": { - "version": "1.0.2", - "from": "extsprintf@1.0.2", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" - }, - "json-schema": { - "version": "0.2.2", - "from": "json-schema@0.2.2", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" - }, - "verror": { - "version": "1.3.6", - "from": "verror@1.3.6", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" - } - } - }, - "sshpk": { - "version": "1.7.4", - "from": "sshpk@>=1.7.0 <2.0.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.7.4.tgz", - "dependencies": { - "asn1": { - "version": "0.2.3", - "from": "asn1@>=0.2.3 <0.3.0", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" - }, - "dashdash": { - "version": "1.13.0", - "from": "dashdash@>=1.10.1 <2.0.0", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.0.tgz", + "is-my-json-valid": { + "version": "2.13.1", + "from": "is-my-json-valid@>=2.12.4 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.13.1.tgz", "dependencies": { + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "dependencies": { + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + } + } + }, + "jsonpointer": { + "version": "2.0.0", + "from": "jsonpointer@2.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-2.0.0.tgz" + }, + "xtend": { + "version": "4.0.1", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + } + } + }, + "pinkie-promise": { + "version": "2.0.1", + "from": "pinkie-promise@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "dependencies": { + "pinkie": { + "version": "2.0.4", + "from": "pinkie@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + } + } + } + } + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@>=3.1.3 <3.2.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "dependencies": { + "hoek": { + "version": "2.16.3", + "from": "hoek@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "boom": { + "version": "2.10.1", + "from": "boom@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + } + } + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "dependencies": { + "assert-plus": { + "version": "0.2.0", + "from": "assert-plus@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" + }, + "jsprim": { + "version": "1.2.2", + "from": "jsprim@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.2.2.tgz", + "dependencies": { + "extsprintf": { + "version": "1.0.2", + "from": "extsprintf@1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" + }, + "json-schema": { + "version": "0.2.2", + "from": "json-schema@0.2.2", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.2.tgz" + }, + "verror": { + "version": "1.3.6", + "from": "verror@1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" + } + } + }, + "sshpk": { + "version": "1.8.3", + "from": "sshpk@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.8.3.tgz", + "dependencies": { + "asn1": { + "version": "0.2.3", + "from": "asn1@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, "assert-plus": { "version": "1.0.0", "from": "assert-plus@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + }, + "dashdash": { + "version": "1.13.1", + "from": "dashdash@>=1.12.0 <2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.13.1.tgz" + }, + "getpass": { + "version": "0.1.6", + "from": "getpass@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz" + }, + "jsbn": { + "version": "0.1.0", + "from": "jsbn@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" + }, + "tweetnacl": { + "version": "0.13.3", + "from": "tweetnacl@>=0.13.0 <0.14.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.13.3.tgz" + }, + "jodid25519": { + "version": "1.0.2", + "from": "jodid25519@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" + }, + "ecc-jsbn": { + "version": "0.1.1", + "from": "ecc-jsbn@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" } } - }, - "jsbn": { - "version": "0.1.0", - "from": "jsbn@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz" - }, - "tweetnacl": { - "version": "0.14.3", - "from": "tweetnacl@>=0.13.0 <1.0.0", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz" - }, - "jodid25519": { - "version": "1.0.2", - "from": "jodid25519@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz" - }, - "ecc-jsbn": { - "version": "0.1.1", - "from": "ecc-jsbn@>=0.0.1 <1.0.0", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz" } } + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@>=5.0.1 <5.1.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "mime-types": { + "version": "2.1.11", + "from": "mime-types@>=2.1.7 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.11.tgz", + "dependencies": { + "mime-db": { + "version": "1.23.0", + "from": "mime-db@>=1.23.0 <1.24.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.23.0.tgz" + } + } + }, + "node-uuid": { + "version": "1.4.7", + "from": "node-uuid@>=1.4.7 <1.5.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" + }, + "oauth-sign": { + "version": "0.8.2", + "from": "oauth-sign@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" + }, + "qs": { + "version": "6.1.0", + "from": "qs@>=6.1.0 <6.2.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" + }, + "stringstream": { + "version": "0.0.5", + "from": "stringstream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" + }, + "tunnel-agent": { + "version": "0.4.3", + "from": "tunnel-agent@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" } } }, - "is-typedarray": { - "version": "1.0.0", - "from": "is-typedarray@>=1.0.0 <1.1.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + "sax": { + "version": "1.2.1", + "from": "sax@>=1.1.4 <2.0.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" }, - "isstream": { - "version": "0.1.2", - "from": "isstream@>=0.1.2 <0.2.0", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + "symbol-tree": { + "version": "3.1.4", + "from": "symbol-tree@>=3.1.0 <4.0.0", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.1.4.tgz" }, - "json-stringify-safe": { - "version": "5.0.1", - "from": "json-stringify-safe@>=5.0.1 <5.1.0", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + "tough-cookie": { + "version": "2.2.2", + "from": "tough-cookie@>=2.2.0 <3.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" }, - "mime-types": { - "version": "2.1.10", - "from": "mime-types@>=2.1.7 <2.2.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.10.tgz", + "webidl-conversions": { + "version": "3.0.1", + "from": "webidl-conversions@>=3.0.1 <4.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + }, + "whatwg-url": { + "version": "2.0.1", + "from": "whatwg-url@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-2.0.1.tgz", "dependencies": { - "mime-db": { - "version": "1.22.0", - "from": "mime-db@>=1.22.0 <1.23.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.22.0.tgz" + "tr46": { + "version": "0.0.3", + "from": "tr46@>=0.0.3 <0.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" } } }, - "node-uuid": { - "version": "1.4.7", - "from": "node-uuid@>=1.4.7 <1.5.0", - "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" - }, - "oauth-sign": { - "version": "0.8.1", - "from": "oauth-sign@>=0.8.1 <0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.1.tgz" - }, - "qs": { - "version": "6.1.0", - "from": "qs@>=6.1.0 <6.2.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.1.0.tgz" - }, - "stringstream": { - "version": "0.0.5", - "from": "stringstream@>=0.0.4 <0.1.0", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" - }, - "tunnel-agent": { - "version": "0.4.2", - "from": "tunnel-agent@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.2.tgz" + "xml-name-validator": { + "version": "2.0.1", + "from": "xml-name-validator@>=2.0.1 <3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz" } } - }, - "sax": { + } + } + }, + "jest-environment-node": { + "version": "12.0.2", + "from": "jest-environment-node@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-12.0.2.tgz" + }, + "jest-haste-map": { + "version": "12.0.2", + "from": "jest-haste-map@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-12.0.2.tgz", + "dependencies": { + "denodeify": { "version": "1.2.1", - "from": "sax@>=1.1.4 <2.0.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz" + "from": "denodeify@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz" }, - "symbol-tree": { - "version": "3.1.4", - "from": "symbol-tree@>=3.1.0 <4.0.0", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.1.4.tgz" - }, - "tough-cookie": { - "version": "2.2.2", - "from": "tough-cookie@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.2.2.tgz" - }, - "webidl-conversions": { - "version": "2.0.1", - "from": "webidl-conversions@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-2.0.1.tgz" - }, - "whatwg-url-compat": { - "version": "0.6.5", - "from": "whatwg-url-compat@>=0.6.5 <0.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url-compat/-/whatwg-url-compat-0.6.5.tgz", + "fb-watchman": { + "version": "1.9.0", + "from": "fb-watchman@>=1.9.0 <2.0.0", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.0.tgz", "dependencies": { - "tr46": { - "version": "0.0.3", - "from": "tr46@>=0.0.1 <0.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + "bser": { + "version": "1.0.2", + "from": "bser@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/bser/-/bser-1.0.2.tgz", + "dependencies": { + "node-int64": { + "version": "0.4.0", + "from": "node-int64@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + } + } } } - }, - "xml-name-validator": { - "version": "2.0.1", - "from": "xml-name-validator@>=2.0.1 <3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-2.0.1.tgz" + } + } + }, + "jest-jasmine1": { + "version": "12.0.2", + "from": "jest-jasmine1@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-jasmine1/-/jest-jasmine1-12.0.2.tgz" + }, + "jest-jasmine2": { + "version": "12.0.2", + "from": "jest-jasmine2@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-12.0.2.tgz" + }, + "jest-mock": { + "version": "12.0.2", + "from": "jest-mock@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-12.0.2.tgz" + }, + "jest-util": { + "version": "12.0.2", + "from": "jest-util@>=12.0.2 <13.0.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-12.0.2.tgz", + "dependencies": { + "jest-mock": { + "version": "11.0.0", + "from": "jest-mock@11.0.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-11.0.0.tgz" } } }, @@ -14063,78 +14127,53 @@ } }, "lodash.template": { - "version": "3.6.2", - "from": "lodash.template@>=3.6.2 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "version": "4.2.4", + "from": "lodash.template@>=4.2.4 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.2.4.tgz", "dependencies": { - "lodash._basecopy": { - "version": "3.0.1", - "from": "lodash._basecopy@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz" - }, - "lodash._basetostring": { - "version": "3.0.1", - "from": "lodash._basetostring@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz" - }, - "lodash._basevalues": { - "version": "3.0.0", - "from": "lodash._basevalues@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz" - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "from": "lodash._isiterateecall@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz" - }, "lodash._reinterpolate": { "version": "3.0.0", - "from": "lodash._reinterpolate@>=3.0.0 <4.0.0", + "from": "lodash._reinterpolate@>=3.0.0 <3.1.0", "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" }, - "lodash.escape": { - "version": "3.2.0", - "from": "lodash.escape@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "lodash.assigninwith": { + "version": "4.0.6", + "from": "lodash.assigninwith@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.assigninwith/-/lodash.assigninwith-4.0.6.tgz", "dependencies": { - "lodash._root": { - "version": "3.0.1", - "from": "lodash._root@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz" + "lodash.keysin": { + "version": "4.1.3", + "from": "lodash.keysin@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.keysin/-/lodash.keysin-4.1.3.tgz" } } }, "lodash.keys": { - "version": "3.1.2", - "from": "lodash.keys@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "version": "4.0.6", + "from": "lodash.keys@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-4.0.6.tgz" + }, + "lodash.rest": { + "version": "4.0.2", + "from": "lodash.rest@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.rest/-/lodash.rest-4.0.2.tgz" + }, + "lodash.templatesettings": { + "version": "4.0.1", + "from": "lodash.templatesettings@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.0.1.tgz", "dependencies": { - "lodash._getnative": { - "version": "3.9.1", - "from": "lodash._getnative@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz" - }, - "lodash.isarguments": { - "version": "3.0.8", - "from": "lodash.isarguments@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.0.8.tgz" - }, - "lodash.isarray": { - "version": "3.0.4", - "from": "lodash.isarray@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz" + "lodash.escape": { + "version": "4.0.0", + "from": "lodash.escape@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.0.tgz" } } }, - "lodash.restparam": { - "version": "3.6.1", - "from": "lodash.restparam@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz" - }, - "lodash.templatesettings": { - "version": "3.1.1", - "from": "lodash.templatesettings@>=3.0.0 <4.0.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz" + "lodash.tostring": { + "version": "4.1.2", + "from": "lodash.tostring@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/lodash.tostring/-/lodash.tostring-4.1.2.tgz" } } }, @@ -14150,40 +14189,6 @@ } } }, - "node-haste": { - "version": "2.9.6", - "from": "node-haste@>=2.5.0 <3.0.0", - "resolved": "https://registry.npmjs.org/node-haste/-/node-haste-2.9.6.tgz", - "dependencies": { - "absolute-path": { - "version": "0.0.0", - "from": "absolute-path@>=0.0.0 <0.0.1", - "resolved": "https://registry.npmjs.org/absolute-path/-/absolute-path-0.0.0.tgz" - }, - "debug": { - "version": "2.2.0", - "from": "debug@>=2.2.0 <3.0.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", - "dependencies": { - "ms": { - "version": "0.7.1", - "from": "ms@0.7.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz" - } - } - }, - "denodeify": { - "version": "1.2.1", - "from": "denodeify@>=1.2.1 <2.0.0", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz" - }, - "throat": { - "version": "2.0.2", - "from": "throat@>=2.0.2 <3.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-2.0.2.tgz" - } - } - }, "optimist": { "version": "0.6.1", "from": "optimist@>=0.6.1 <0.7.0", @@ -14225,7 +14230,7 @@ }, "fb-watchman": { "version": "1.9.0", - "from": "fb-watchman@>=1.8.0 <2.0.0", + "from": "fb-watchman@>=1.9.0 <2.0.0", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-1.9.0.tgz", "dependencies": { "bser": { @@ -14291,9 +14296,9 @@ } }, "which": { - "version": "1.2.4", + "version": "1.2.8", "from": "which@>=1.1.1 <2.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-1.2.4.tgz", + "resolved": "https://registry.npmjs.org/which/-/which-1.2.8.tgz", "dependencies": { "is-absolute": { "version": "0.1.7", @@ -14353,9 +14358,9 @@ } }, "object-assign": { - "version": "4.0.1", - "from": "object-assign@>=4.0.1 <5.0.0", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.0.1.tgz" + "version": "4.1.0", + "from": "object-assign@>=4.1.0 <5.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz" }, "platform": { "version": "1.3.1", diff --git a/package.json b/package.json index 38b3480878..d42afd0864 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-build", "private": true, - "version": "15.0.2", + "version": "15.1.0-alpha.1", "devDependencies": { "async": "^1.5.0", "babel-cli": "^6.6.5", @@ -48,9 +48,9 @@ "gulp-babel": "^6.0.0", "gulp-flatten": "^0.2.0", "gzip-js": "~0.3.2", - "jest-cli": "^0.9.0", + "jest-cli": "^12.0.2", "loose-envify": "^1.1.0", - "object-assign": "^4.0.1", + "object-assign": "^4.1.0", "platform": "^1.1.0", "run-sequence": "^1.1.4", "through2": "^2.0.0", diff --git a/packages/react-addons/package.json b/packages/react-addons/package.json index b3234fe909..062f360a23 100644 --- a/packages/react-addons/package.json +++ b/packages/react-addons/package.json @@ -1,6 +1,6 @@ { "name": "react-addons-template", - "version": "15.0.2", + "version": "15.1.0-alpha.1", "main": "index.js", "repository": "facebook/react", "keywords": [ @@ -10,6 +10,6 @@ "license": "BSD-3-Clause", "dependencies": {}, "peerDependencies": { - "react": "^15.0.2" + "react": "^15.1.0-alpha.1" } } diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index dca80f0d0a..648d45765d 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-dom", - "version": "15.0.2", + "version": "15.1.0-alpha.1", "description": "React package for working with the DOM.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ "homepage": "https://facebook.github.io/react/", "dependencies": {}, "peerDependencies": { - "react": "^15.0.2" + "react": "^15.1.0-alpha.1" } } diff --git a/packages/react-native-renderer/package.json b/packages/react-native-renderer/package.json index 89adaee983..30b424a62c 100644 --- a/packages/react-native-renderer/package.json +++ b/packages/react-native-renderer/package.json @@ -1,6 +1,6 @@ { "name": "react-native-renderer", - "version": "15.0.2", + "version": "15.1.0-alpha.1", "description": "React package for use inside react-native.", "main": "index.js", "repository": "facebook/react", @@ -14,6 +14,6 @@ }, "homepage": "https://facebook.github.io/react-native/", "dependencies": { - "react": "^15.0.2" + "react": "^15.1.0-alpha.1" } } diff --git a/packages/react/package.json b/packages/react/package.json index 4a3ef56b14..ca9ab105aa 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -1,7 +1,7 @@ { "name": "react", "description": "React is a JavaScript library for building user interfaces.", - "version": "15.0.2", + "version": "15.1.0-alpha.1", "keywords": [ "react" ], @@ -25,7 +25,7 @@ "dependencies": { "fbjs": "^0.8.0", "loose-envify": "^1.1.0", - "object-assign": "^4.0.1" + "object-assign": "^4.1.0" }, "browserify": { "transform": [ diff --git a/src/ReactVersion.js b/src/ReactVersion.js index 5735f2b6c6..f30ca3df87 100644 --- a/src/ReactVersion.js +++ b/src/ReactVersion.js @@ -11,4 +11,4 @@ 'use strict'; -module.exports = '15.0.2'; +module.exports = '15.1.0-alpha.1'; diff --git a/src/addons/ReactComponentWithPureRenderMixin.js b/src/addons/ReactComponentWithPureRenderMixin.js index afafbdaa76..cf00d7cb3d 100644 --- a/src/addons/ReactComponentWithPureRenderMixin.js +++ b/src/addons/ReactComponentWithPureRenderMixin.js @@ -36,6 +36,8 @@ var shallowCompare = require('shallowCompare'); * complex data structures this mixin may have false-negatives for deeper * differences. Only mixin to components which have simple props and state, or * use `forceUpdate()` when you know deep data structures have changed. + * + * See https://facebook.github.io/react/docs/pure-render-mixin.html */ var ReactComponentWithPureRenderMixin = { shouldComponentUpdate: function(nextProps, nextState) { diff --git a/src/addons/ReactFragment.js b/src/addons/ReactFragment.js index f16ccf8bc0..5d83e84533 100644 --- a/src/addons/ReactFragment.js +++ b/src/addons/ReactFragment.js @@ -31,8 +31,11 @@ var numericPropertyRegex = /^\d+$/; var warnedAboutNumeric = false; var ReactFragment = { - // Wrap a keyed object in an opaque proxy that warns you if you access any - // of its properties. + /** + * Wrap a keyed object in an opaque proxy that warns you if you access any + * of its properties. + * See https://facebook.github.io/react/docs/create-fragment.html + */ create: function(object) { if (typeof object !== 'object' || !object || Array.isArray(object)) { warning( diff --git a/src/addons/ReactWithAddons.js b/src/addons/ReactWithAddons.js index bd13ea495f..94102c5c2e 100644 --- a/src/addons/ReactWithAddons.js +++ b/src/addons/ReactWithAddons.js @@ -34,7 +34,7 @@ React.addons = { }; if (__DEV__) { - React.addons.Perf = require('ReactDefaultPerf'); + React.addons.Perf = require('ReactPerf'); React.addons.TestUtils = require('ReactTestUtils'); } diff --git a/src/addons/link/LinkedStateMixin.js b/src/addons/link/LinkedStateMixin.js index c45a79faad..3e2ca17604 100644 --- a/src/addons/link/LinkedStateMixin.js +++ b/src/addons/link/LinkedStateMixin.js @@ -16,6 +16,7 @@ var ReactStateSetters = require('ReactStateSetters'); /** * A simple mixin around ReactLink.forState(). + * See https://facebook.github.io/react/docs/two-way-binding-helpers.html */ var LinkedStateMixin = { /** diff --git a/src/addons/link/ReactLink.js b/src/addons/link/ReactLink.js index ea6435c92b..d327b4f8ec 100644 --- a/src/addons/link/ReactLink.js +++ b/src/addons/link/ReactLink.js @@ -37,6 +37,9 @@ var React = require('React'); /** + * Deprecated: An an easy way to express two-way binding with React. + * See https://facebook.github.io/react/docs/two-way-binding-helpers.html + * * @param {*} value current value of the link * @param {function} requestChange callback to request a change */ diff --git a/src/addons/shallowCompare.js b/src/addons/shallowCompare.js index 33e4591ba8..a6b7a1095d 100644 --- a/src/addons/shallowCompare.js +++ b/src/addons/shallowCompare.js @@ -16,6 +16,7 @@ var shallowEqual = require('shallowEqual'); /** * Does a shallow comparison for props and state. * See ReactComponentWithPureRenderMixin + * See also https://facebook.github.io/react/docs/shallow-compare.html */ function shallowCompare(instance, nextProps, nextState) { return ( diff --git a/src/addons/transitions/ReactCSSTransitionGroup.js b/src/addons/transitions/ReactCSSTransitionGroup.js index 4177ec790b..6d61c2884a 100644 --- a/src/addons/transitions/ReactCSSTransitionGroup.js +++ b/src/addons/transitions/ReactCSSTransitionGroup.js @@ -41,6 +41,11 @@ function createTransitionTimeoutPropValidator(transitionType) { }; } +/** + * An easy way to perform CSS transitions and animations when a React component + * enters or leaves the DOM. + * See https://facebook.github.io/react/docs/animation.html#high-level-api-reactcsstransitiongroup + */ var ReactCSSTransitionGroup = React.createClass({ displayName: 'ReactCSSTransitionGroup', diff --git a/src/addons/transitions/ReactTransitionGroup.js b/src/addons/transitions/ReactTransitionGroup.js index 6d6df1a80b..a2c935eeb2 100644 --- a/src/addons/transitions/ReactTransitionGroup.js +++ b/src/addons/transitions/ReactTransitionGroup.js @@ -16,6 +16,11 @@ var ReactTransitionChildMapping = require('ReactTransitionChildMapping'); var emptyFunction = require('emptyFunction'); +/** + * A basis for animatins. When children are declaratively added or removed, + * special lifecycle hooks are called. + * See https://facebook.github.io/react/docs/animation.html#low-level-api-reacttransitiongroup + */ var ReactTransitionGroup = React.createClass({ displayName: 'ReactTransitionGroup', diff --git a/src/addons/update.js b/src/addons/update.js index cc2434e2bd..8704993c11 100644 --- a/src/addons/update.js +++ b/src/addons/update.js @@ -66,6 +66,10 @@ function invariantArrayCase(value, spec, command) { ); } +/** + * Returns a updated shallow copy of an object without mutating the original. + * See https://facebook.github.io/react/docs/update.html for details. + */ function update(value, spec) { invariant( typeof spec === 'object', diff --git a/src/core/__tests__/ReactErrorBoundaries-test.js b/src/core/__tests__/ReactErrorBoundaries-test.js index 0ba37323d5..c61d1f524d 100644 --- a/src/core/__tests__/ReactErrorBoundaries-test.js +++ b/src/core/__tests__/ReactErrorBoundaries-test.js @@ -13,11 +13,13 @@ var React; var ReactDOM; +var ReactDOMServer; describe('ReactErrorBoundaries', function() { beforeEach(function() { ReactDOM = require('ReactDOM'); + ReactDOMServer = require('ReactDOMServer'); React = require('React'); }); @@ -50,11 +52,46 @@ describe('ReactErrorBoundaries', function() { var EventPluginHub = require('EventPluginHub'); var container = document.createElement('div'); - EventPluginHub.putListener = jest.genMockFn(); + EventPluginHub.putListener = jest.fn(); ReactDOM.render(, container); expect(EventPluginHub.putListener).not.toBeCalled(); }); + it('renders an error state (ssr)', function() { + class Angry extends React.Component { + render() { + throw new Error('Please, do not render me.'); + } + } + + class Boundary extends React.Component { + constructor(props) { + super(props); + this.state = {error: false}; + } + render() { + if (!this.state.error) { + return (
); + } else { + return (
Happy Birthday!
); + } + } + onClick() { + /* do nothing */ + } + unstable_handleError() { + this.setState({error: true}); + } + } + + var EventPluginHub = require('EventPluginHub'); + var container = document.createElement('div'); + EventPluginHub.putListener = jest.fn(); + container.innerHTML = ReactDOMServer.renderToString(); + expect(container.firstChild.innerHTML).toBe('Happy Birthday!'); + expect(EventPluginHub.putListener).not.toBeCalled(); + }); + it('will catch exceptions in componentWillUnmount', function() { class ErrorBoundary extends React.Component { constructor() { @@ -120,7 +157,7 @@ describe('ReactErrorBoundaries', function() { var EventPluginHub = require('EventPluginHub'); var container = document.createElement('div'); - EventPluginHub.putListener = jest.genMockFn(); + EventPluginHub.putListener = jest.fn(); ReactDOM.render(, container); expect(EventPluginHub.putListener).toBeCalled(); }); diff --git a/src/isomorphic/ReactDebugInstanceMap.js b/src/isomorphic/ReactDebugInstanceMap.js deleted file mode 100644 index 50dddf4ad0..0000000000 --- a/src/isomorphic/ReactDebugInstanceMap.js +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright 2016-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactDebugInstanceMap - */ - -'use strict'; - -var warning = require('warning'); - -function checkValidInstance(internalInstance) { - if (!internalInstance) { - warning( - false, - 'There is an internal error in the React developer tools integration. ' + - 'Instead of an internal instance, received %s. ' + - 'Please report this as a bug in React.', - internalInstance - ); - return false; - } - var isValid = typeof internalInstance.mountComponent === 'function'; - warning( - isValid, - 'There is an internal error in the React developer tools integration. ' + - 'Instead of an internal instance, received an object with the following ' + - 'keys: %s. Please report this as a bug in React.', - Object.keys(internalInstance).join(', ') - ); - return isValid; -} - -var idCounter = 1; -var instancesByIDs = {}; -var instancesToIDs; - -function getIDForInstance(internalInstance) { - if (!instancesToIDs) { - instancesToIDs = new WeakMap(); - } - if (instancesToIDs.has(internalInstance)) { - return instancesToIDs.get(internalInstance); - } else { - var instanceID = (idCounter++).toString(); - instancesToIDs.set(internalInstance, instanceID); - return instanceID; - } -} - -function getInstanceByID(instanceID) { - return instancesByIDs[instanceID] || null; -} - -function isRegisteredInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - return instancesByIDs.hasOwnProperty(instanceID); - } else { - return false; - } -} - -function registerInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - instancesByIDs[instanceID] = internalInstance; - } -} - -function unregisterInstance(internalInstance) { - var instanceID = getIDForInstance(internalInstance); - if (instanceID) { - delete instancesByIDs[instanceID]; - } -} - -var ReactDebugInstanceMap = { - getIDForInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return null; - } - return getIDForInstance(internalInstance); - }, - getInstanceByID(instanceID) { - return getInstanceByID(instanceID); - }, - isRegisteredInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return false; - } - return isRegisteredInstance(internalInstance); - }, - registerInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return; - } - warning( - !isRegisteredInstance(internalInstance), - 'There is an internal error in the React developer tools integration. ' + - 'A registered instance should not be registered again. ' + - 'Please report this as a bug in React.' - ); - registerInstance(internalInstance); - }, - unregisterInstance(internalInstance) { - if (!checkValidInstance(internalInstance)) { - return; - } - warning( - isRegisteredInstance(internalInstance), - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - unregisterInstance(internalInstance); - }, -}; - -module.exports = ReactDebugInstanceMap; diff --git a/src/isomorphic/ReactDebugTool.js b/src/isomorphic/ReactDebugTool.js index 4d2c2a3536..9c084ad52f 100644 --- a/src/isomorphic/ReactDebugTool.js +++ b/src/isomorphic/ReactDebugTool.js @@ -11,7 +11,9 @@ 'use strict'; -var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool'); +var ExecutionEnvironment = require('ExecutionEnvironment'); + +var performanceNow = require('performanceNow'); var warning = require('warning'); var eventHandlers = []; @@ -37,6 +39,70 @@ function emitEvent(handlerFunctionName, arg1, arg2, arg3, arg4, arg5) { } } +var isProfiling = false; +var flushHistory = []; +var currentFlushNesting = 0; +var currentFlushMeasurements = null; +var currentFlushStartTime = null; +var currentTimerDebugID = null; +var currentTimerStartTime = null; +var currentTimerType = null; + +function clearHistory() { + ReactComponentTreeDevtool.purgeUnmountedComponents(); + ReactNativeOperationHistoryDevtool.clearHistory(); +} + +function getTreeSnapshot(registeredIDs) { + return registeredIDs.reduce((tree, id) => { + var ownerID = ReactComponentTreeDevtool.getOwnerID(id); + var parentID = ReactComponentTreeDevtool.getParentID(id); + tree[id] = { + displayName: ReactComponentTreeDevtool.getDisplayName(id), + text: ReactComponentTreeDevtool.getText(id), + updateCount: ReactComponentTreeDevtool.getUpdateCount(id), + childIDs: ReactComponentTreeDevtool.getChildIDs(id), + // Text nodes don't have owners but this is close enough. + ownerID: ownerID || ReactComponentTreeDevtool.getOwnerID(parentID), + parentID, + }; + return tree; + }, {}); +} + +function resetMeasurements() { + if (__DEV__) { + var previousStartTime = currentFlushStartTime; + var previousMeasurements = currentFlushMeasurements || []; + var previousOperations = ReactNativeOperationHistoryDevtool.getHistory(); + + if (!isProfiling || currentFlushNesting === 0) { + currentFlushStartTime = null; + currentFlushMeasurements = null; + clearHistory(); + return; + } + + if (previousMeasurements.length || previousOperations.length) { + var registeredIDs = ReactComponentTreeDevtool.getRegisteredIDs(); + flushHistory.push({ + duration: performanceNow() - previousStartTime, + measurements: previousMeasurements || [], + operations: previousOperations || [], + treeSnapshot: getTreeSnapshot(registeredIDs), + }); + } + + clearHistory(); + currentFlushStartTime = performanceNow(); + currentFlushMeasurements = []; + } +} + +function checkDebugID(debugID) { + warning(debugID, 'ReactDebugTool: debugID may not be empty.'); +} + var ReactDebugTool = { addDevtool(devtool) { eventHandlers.push(devtool); @@ -49,29 +115,157 @@ var ReactDebugTool = { } } }, + beginProfiling() { + if (__DEV__) { + if (isProfiling) { + return; + } + + isProfiling = true; + flushHistory.length = 0; + resetMeasurements(); + } + }, + endProfiling() { + if (__DEV__) { + if (!isProfiling) { + return; + } + + isProfiling = false; + resetMeasurements(); + } + }, + getFlushHistory() { + if (__DEV__) { + return flushHistory; + } + }, + onBeginFlush() { + if (__DEV__) { + currentFlushNesting++; + resetMeasurements(); + } + emitEvent('onBeginFlush'); + }, + onEndFlush() { + if (__DEV__) { + resetMeasurements(); + currentFlushNesting--; + } + emitEvent('onEndFlush'); + }, + onBeginLifeCycleTimer(debugID, timerType) { + checkDebugID(debugID); + emitEvent('onBeginLifeCycleTimer', debugID, timerType); + if (__DEV__) { + if (isProfiling && currentFlushNesting > 0) { + warning( + !currentTimerType, + 'There is an internal error in the React performance measurement code. ' + + 'Did not expect %s timer to start while %s timer is still in ' + + 'progress for %s instance.', + timerType, + currentTimerType || 'no', + (debugID === currentTimerDebugID) ? 'the same' : 'another' + ); + currentTimerStartTime = performanceNow(); + currentTimerDebugID = debugID; + currentTimerType = timerType; + } + } + }, + onEndLifeCycleTimer(debugID, timerType) { + checkDebugID(debugID); + if (__DEV__) { + if (isProfiling && currentFlushNesting > 0) { + warning( + currentTimerType === timerType, + 'There is an internal error in the React performance measurement code. ' + + 'We did not expect %s timer to stop while %s timer is still in ' + + 'progress for %s instance. Please report this as a bug in React.', + timerType, + currentTimerType || 'no', + (debugID === currentTimerDebugID) ? 'the same' : 'another' + ); + currentFlushMeasurements.push({ + timerType, + instanceID: debugID, + duration: performanceNow() - currentTimerStartTime, + }); + currentTimerStartTime = null; + currentTimerDebugID = null; + currentTimerType = null; + } + } + emitEvent('onEndLifeCycleTimer', debugID, timerType); + }, + onBeginReconcilerTimer(debugID, timerType) { + checkDebugID(debugID); + emitEvent('onBeginReconcilerTimer', debugID, timerType); + }, + onEndReconcilerTimer(debugID, timerType) { + checkDebugID(debugID); + emitEvent('onEndReconcilerTimer', debugID, timerType); + }, onBeginProcessingChildContext() { emitEvent('onBeginProcessingChildContext'); }, onEndProcessingChildContext() { emitEvent('onEndProcessingChildContext'); }, + onNativeOperation(debugID, type, payload) { + checkDebugID(debugID); + emitEvent('onNativeOperation', debugID, type, payload); + }, onSetState() { emitEvent('onSetState'); }, - onMountRootComponent(internalInstance) { - emitEvent('onMountRootComponent', internalInstance); + onSetDisplayName(debugID, displayName) { + checkDebugID(debugID); + emitEvent('onSetDisplayName', debugID, displayName); }, - onMountComponent(internalInstance) { - emitEvent('onMountComponent', internalInstance); + onSetChildren(debugID, childDebugIDs) { + checkDebugID(debugID); + emitEvent('onSetChildren', debugID, childDebugIDs); }, - onUpdateComponent(internalInstance) { - emitEvent('onUpdateComponent', internalInstance); + onSetOwner(debugID, ownerDebugID) { + checkDebugID(debugID); + emitEvent('onSetOwner', debugID, ownerDebugID); }, - onUnmountComponent(internalInstance) { - emitEvent('onUnmountComponent', internalInstance); + onSetText(debugID, text) { + checkDebugID(debugID); + emitEvent('onSetText', debugID, text); + }, + onMountRootComponent(debugID) { + checkDebugID(debugID); + emitEvent('onMountRootComponent', debugID); + }, + onMountComponent(debugID) { + checkDebugID(debugID); + emitEvent('onMountComponent', debugID); + }, + onUpdateComponent(debugID) { + checkDebugID(debugID); + emitEvent('onUpdateComponent', debugID); + }, + onUnmountComponent(debugID) { + checkDebugID(debugID); + emitEvent('onUnmountComponent', debugID); }, }; -ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool); +if (__DEV__) { + var ReactInvalidSetStateWarningDevTool = require('ReactInvalidSetStateWarningDevTool'); + var ReactNativeOperationHistoryDevtool = require('ReactNativeOperationHistoryDevtool'); + var ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); + ReactDebugTool.addDevtool(ReactInvalidSetStateWarningDevTool); + ReactDebugTool.addDevtool(ReactComponentTreeDevtool); + ReactDebugTool.addDevtool(ReactNativeOperationHistoryDevtool); + var url = (ExecutionEnvironment.canUseDOM && window.location.href) || ''; + if ((/[?&]react_perf\b/).test(url)) { + ReactDebugTool.beginProfiling(); + } +} module.exports = ReactDebugTool; diff --git a/src/isomorphic/ReactPerf.js b/src/isomorphic/ReactPerf.js new file mode 100644 index 0000000000..4c631a7f13 --- /dev/null +++ b/src/isomorphic/ReactPerf.js @@ -0,0 +1,371 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactPerf + */ + +'use strict'; + +var ReactDebugTool = require('ReactDebugTool'); +var warning = require('warning'); + +function roundFloat(val, base = 2) { + var n = Math.pow(10, base); + return Math.floor(val * n) / n; +} + +function getFlushHistory() { + return ReactDebugTool.getFlushHistory(); +} + +function getExclusive(flushHistory = getFlushHistory()) { + var aggregatedStats = {}; + var affectedIDs = {}; + + function updateAggregatedStats(treeSnapshot, instanceID, timerType, applyUpdate) { + var {displayName} = treeSnapshot[instanceID]; + var key = displayName; + var stats = aggregatedStats[key]; + if (!stats) { + affectedIDs[key] = {}; + stats = aggregatedStats[key] = { + key, + instanceCount: 0, + counts: {}, + durations: {}, + totalDuration: 0, + }; + } + if (!stats.durations[timerType]) { + stats.durations[timerType] = 0; + } + if (!stats.counts[timerType]) { + stats.counts[timerType] = 0; + } + affectedIDs[key][instanceID] = true; + applyUpdate(stats); + } + + flushHistory.forEach(flush => { + var {measurements, treeSnapshot} = flush; + measurements.forEach(measurement => { + var {duration, instanceID, timerType} = measurement; + updateAggregatedStats(treeSnapshot, instanceID, timerType, stats => { + stats.totalDuration += duration; + stats.durations[timerType] += duration; + stats.counts[timerType]++; + }); + }); + }); + + return Object.keys(aggregatedStats) + .map(key => ({ + ...aggregatedStats[key], + instanceCount: Object.keys(affectedIDs[key]).length, + })) + .sort((a, b) => + b.totalDuration - a.totalDuration + ); +} + +function getInclusive(flushHistory = getFlushHistory()) { + var aggregatedStats = {}; + var affectedIDs = {}; + + function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) { + var {displayName, ownerID} = treeSnapshot[instanceID]; + var owner = treeSnapshot[ownerID]; + var key = (owner ? owner.displayName + ' > ' : '') + displayName; + var stats = aggregatedStats[key]; + if (!stats) { + affectedIDs[key] = {}; + stats = aggregatedStats[key] = { + key, + instanceCount: 0, + inclusiveRenderDuration: 0, + renderCount: 0, + }; + } + affectedIDs[key][instanceID] = true; + applyUpdate(stats); + } + + var isCompositeByID = {}; + flushHistory.forEach(flush => { + var {measurements} = flush; + measurements.forEach(measurement => { + var {instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + isCompositeByID[instanceID] = true; + }); + }); + + flushHistory.forEach(flush => { + var {measurements, treeSnapshot} = flush; + measurements.forEach(measurement => { + var {duration, instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + updateAggregatedStats(treeSnapshot, instanceID, stats => { + stats.renderCount++; + }); + var nextParentID = instanceID; + while (nextParentID) { + // As we traverse parents, only count inclusive time towards composites. + // We know something is a composite if its render() was called. + if (isCompositeByID[nextParentID]) { + updateAggregatedStats(treeSnapshot, nextParentID, stats => { + stats.inclusiveRenderDuration += duration; + }); + } + nextParentID = treeSnapshot[nextParentID].parentID; + } + }); + }); + + return Object.keys(aggregatedStats) + .map(key => ({ + ...aggregatedStats[key], + instanceCount: Object.keys(affectedIDs[key]).length, + })) + .sort((a, b) => + b.inclusiveRenderDuration - a.inclusiveRenderDuration + ); +} + +function getWasted(flushHistory = getFlushHistory()) { + var aggregatedStats = {}; + var affectedIDs = {}; + + function updateAggregatedStats(treeSnapshot, instanceID, applyUpdate) { + var {displayName, ownerID} = treeSnapshot[instanceID]; + var owner = treeSnapshot[ownerID]; + var key = (owner ? owner.displayName + ' > ' : '') + displayName; + var stats = aggregatedStats[key]; + if (!stats) { + affectedIDs[key] = {}; + stats = aggregatedStats[key] = { + key, + instanceCount: 0, + inclusiveRenderDuration: 0, + renderCount: 0, + }; + } + affectedIDs[key][instanceID] = true; + applyUpdate(stats); + } + + flushHistory.forEach(flush => { + var {measurements, treeSnapshot, operations} = flush; + var isDefinitelyNotWastedByID = {}; + + // Find native components associated with an operation in this batch. + // Mark all components in their parent tree as definitely not wasted. + operations.forEach(operation => { + var {instanceID} = operation; + var nextParentID = instanceID; + while (nextParentID) { + isDefinitelyNotWastedByID[nextParentID] = true; + nextParentID = treeSnapshot[nextParentID].parentID; + } + }); + + // Find composite components that rendered in this batch. + // These are potential candidates for being wasted renders. + var renderedCompositeIDs = {}; + measurements.forEach(measurement => { + var {instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + renderedCompositeIDs[instanceID] = true; + }); + + measurements.forEach(measurement => { + var {duration, instanceID, timerType} = measurement; + if (timerType !== 'render') { + return; + } + + // If there was a DOM update below this component, or it has just been + // mounted, its render() is not considered wasted. + var { updateCount } = treeSnapshot[instanceID]; + if (isDefinitelyNotWastedByID[instanceID] || updateCount === 0) { + return; + } + + // We consider this render() wasted. + updateAggregatedStats(treeSnapshot, instanceID, stats => { + stats.renderCount++; + }); + + var nextParentID = instanceID; + while (nextParentID) { + // Any parents rendered during this batch are considered wasted + // unless we previously marked them as dirty. + var isWasted = + renderedCompositeIDs[nextParentID] && + !isDefinitelyNotWastedByID[nextParentID]; + if (isWasted) { + updateAggregatedStats(treeSnapshot, nextParentID, stats => { + stats.inclusiveRenderDuration += duration; + }); + } + nextParentID = treeSnapshot[nextParentID].parentID; + } + }); + }); + + return Object.keys(aggregatedStats) + .map(key => ({ + ...aggregatedStats[key], + instanceCount: Object.keys(affectedIDs[key]).length, + })) + .sort((a, b) => + b.inclusiveRenderDuration - a.inclusiveRenderDuration + ); +} + +function getOperations(flushHistory = getFlushHistory()) { + var stats = []; + flushHistory.forEach((flush, flushIndex) => { + var {operations, treeSnapshot} = flush; + operations.forEach(operation => { + var {instanceID, type, payload} = operation; + var {displayName, ownerID} = treeSnapshot[instanceID]; + var owner = treeSnapshot[ownerID]; + var key = (owner ? owner.displayName + ' > ' : '') + displayName; + + stats.push({ + flushIndex, + instanceID, + key, + type, + ownerID, + payload, + }); + }); + }); + return stats; +} + +function printExclusive(flushHistory) { + var stats = getExclusive(flushHistory); + var table = stats.map(item => { + var {key, instanceCount, totalDuration} = item; + var renderCount = item.counts.render || 0; + var renderDuration = item.durations.render || 0; + return { + 'Component': key, + 'Total time (ms)': roundFloat(totalDuration), + 'Instance count': instanceCount, + 'Total render time (ms)': roundFloat(renderDuration), + 'Average render time (ms)': renderCount ? + roundFloat(renderDuration / renderCount) : + undefined, + 'Render count': renderCount, + 'Total lifecycle time (ms)': roundFloat(totalDuration - renderDuration), + }; + }); + console.table(table); +} + +function printInclusive(flushHistory) { + var stats = getInclusive(flushHistory); + var table = stats.map(item => { + var {key, instanceCount, inclusiveRenderDuration, renderCount} = item; + return { + 'Owner > Component': key, + 'Inclusive render time (ms)': roundFloat(inclusiveRenderDuration), + 'Instance count': instanceCount, + 'Render count': renderCount, + }; + }); + console.table(table); +} + +function printWasted(flushHistory) { + var stats = getWasted(flushHistory); + var table = stats.map(item => { + var {key, instanceCount, inclusiveRenderDuration, renderCount} = item; + return { + 'Owner > Component': key, + 'Inclusive wasted time (ms)': roundFloat(inclusiveRenderDuration), + 'Instance count': instanceCount, + 'Render count': renderCount, + }; + }); + console.table(table); +} + +function printOperations(flushHistory) { + var stats = getOperations(flushHistory); + var table = stats.map(stat => ({ + 'Owner > Node': stat.key, + 'Operation': stat.type, + 'Payload': typeof stat.payload === 'object' ? + JSON.stringify(stat.payload) : + stat.payload, + 'Flush index': stat.flushIndex, + 'Owner Component ID': stat.ownerID, + 'DOM Component ID': stat.instanceID, + })); + console.table(table); +} + +var warnedAboutPrintDOM = false; +function printDOM(measurements) { + warning( + warnedAboutPrintDOM, + '`ReactPerf.printDOM(...)` is deprecated. Use ' + + '`ReactPerf.printOperations(...)` instead.' + ); + warnedAboutPrintDOM = true; + return printOperations(measurements); +} + +var warnedAboutGetMeasurementsSummaryMap = false; +function getMeasurementsSummaryMap(measurements) { + warning( + warnedAboutGetMeasurementsSummaryMap, + '`ReactPerf.getMeasurementsSummaryMap(...)` is deprecated. Use ' + + '`ReactPerf.getWasted(...)` instead.' + ); + warnedAboutGetMeasurementsSummaryMap = true; + return getWasted(measurements); +} + +function start() { + ReactDebugTool.beginProfiling(); +} + +function stop() { + ReactDebugTool.endProfiling(); +} + +var ReactPerfAnalysis = { + getLastMeasurements: getFlushHistory, + getExclusive, + getInclusive, + getWasted, + getOperations, + printExclusive, + printInclusive, + printWasted, + printOperations, + start, + stop, + // Deprecated: + printDOM, + getMeasurementsSummaryMap, +}; + +module.exports = ReactPerfAnalysis; diff --git a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js b/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js deleted file mode 100644 index d9a063e2c6..0000000000 --- a/src/isomorphic/__tests__/ReactDebugInstanceMap-test.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Copyright 2016-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @emails react-core - */ - -'use strict'; - -describe('ReactDebugInstanceMap', function() { - var React; - var ReactDebugInstanceMap; - var ReactDOM; - - beforeEach(function() { - jest.resetModuleRegistry(); - React = require('React'); - ReactDebugInstanceMap = require('ReactDebugInstanceMap'); - ReactDOM = require('ReactDOM'); - }); - - function createStubInstance() { - return { mountComponent: () => {} }; - } - - it('should register and unregister instances', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(false); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.registerInstance(inst1); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(true); - - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(true); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - - ReactDebugInstanceMap.unregisterInstance(inst1); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst1)).toBe(false); - expect(ReactDebugInstanceMap.isRegisteredInstance(inst2)).toBe(false); - }); - - it('should assign stable IDs', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - var inst1ID = ReactDebugInstanceMap.getIDForInstance(inst1); - var inst2ID = ReactDebugInstanceMap.getIDForInstance(inst2); - expect(typeof inst1ID).toBe('string'); - expect(typeof inst2ID).toBe('string'); - expect(inst1ID).not.toBe(inst2ID); - - ReactDebugInstanceMap.registerInstance(inst1); - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.getIDForInstance(inst1)).toBe(inst1ID); - expect(ReactDebugInstanceMap.getIDForInstance(inst2)).toBe(inst2ID); - - ReactDebugInstanceMap.unregisterInstance(inst1); - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.getIDForInstance(inst1)).toBe(inst1ID); - expect(ReactDebugInstanceMap.getIDForInstance(inst2)).toBe(inst2ID); - }); - - it('should retrieve registered instance by its ID', function() { - var inst1 = createStubInstance(); - var inst2 = createStubInstance(); - - var inst1ID = ReactDebugInstanceMap.getIDForInstance(inst1); - var inst2ID = ReactDebugInstanceMap.getIDForInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(null); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(null); - - ReactDebugInstanceMap.registerInstance(inst1); - ReactDebugInstanceMap.registerInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(inst1); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(inst2); - - ReactDebugInstanceMap.unregisterInstance(inst1); - ReactDebugInstanceMap.unregisterInstance(inst2); - expect(ReactDebugInstanceMap.getInstanceByID(inst1ID)).toBe(null); - expect(ReactDebugInstanceMap.getInstanceByID(inst2ID)).toBe(null); - }); - - it('should warn when registering an instance twice', function() { - spyOn(console, 'error'); - - var inst = createStubInstance(); - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(0); - - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'A registered instance should not be registered again. ' + - 'Please report this as a bug in React.' - ); - - ReactDebugInstanceMap.unregisterInstance(inst); - ReactDebugInstanceMap.registerInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - }); - - it('should warn when unregistering an instance twice', function() { - spyOn(console, 'error'); - var inst = createStubInstance(); - - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - expect(console.error.argsForCall[0][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - - ReactDebugInstanceMap.registerInstance(inst); - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(1); - - ReactDebugInstanceMap.unregisterInstance(inst); - expect(console.error.argsForCall.length).toBe(2); - expect(console.error.argsForCall[1][0]).toContain( - 'There is an internal error in the React developer tools integration. ' + - 'An unregistered instance should not be unregistered again. ' + - 'Please report this as a bug in React.' - ); - }); - - it('should warn about anything than is not an internal instance', function() { - class Foo extends React.Component { - render() { - return
; - } - } - - spyOn(console, 'error'); - var warningCount = 0; - var div = document.createElement('div'); - var publicInst = ReactDOM.render(, div); - - [false, null, undefined, {}, div, publicInst].forEach(falsyValue => { - ReactDebugInstanceMap.registerInstance(falsyValue); - warningCount++; - expect(ReactDebugInstanceMap.getIDForInstance(falsyValue)).toBe(null); - warningCount++; - expect(ReactDebugInstanceMap.isRegisteredInstance(falsyValue)).toBe(false); - warningCount++; - ReactDebugInstanceMap.unregisterInstance(falsyValue); - warningCount++; - }); - - expect(console.error.argsForCall.length).toBe(warningCount); - for (var i = 0; i < warningCount.length; i++) { - // Ideally we could check for the more detailed error message here - // but it depends on the input type and is meant for internal bugs - // anyway so I don't think it's worth complicating the test with it. - expect(console.error.argsForCall[i][0]).toContain( - 'There is an internal error in the React developer tools integration.' - ); - } - }); -}); diff --git a/src/test/__tests__/ReactDefaultPerf-test.js b/src/isomorphic/__tests__/ReactPerf-test.js similarity index 66% rename from src/test/__tests__/ReactDefaultPerf-test.js rename to src/isomorphic/__tests__/ReactPerf-test.js index 8019f0fe8e..644bc618b8 100644 --- a/src/test/__tests__/ReactDefaultPerf-test.js +++ b/src/isomorphic/__tests__/ReactPerf-test.js @@ -11,13 +11,11 @@ 'use strict'; -describe('ReactDefaultPerf', function() { +describe('ReactPerf', function() { var React; var ReactDOM; - var ReactDOMFeatureFlags; - var ReactDefaultPerf; + var ReactPerf; var ReactTestUtils; - var ReactDefaultPerfAnalysis; var App; var Box; @@ -36,10 +34,8 @@ describe('ReactDefaultPerf', function() { React = require('React'); ReactDOM = require('ReactDOM'); - ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); - ReactDefaultPerf = require('ReactDefaultPerf'); + ReactPerf = require('ReactPerf'); ReactTestUtils = require('ReactTestUtils'); - ReactDefaultPerfAnalysis = require('ReactDefaultPerfAnalysis'); App = React.createClass({ render: function() { @@ -68,10 +64,17 @@ describe('ReactDefaultPerf', function() { }); function measure(fn) { - ReactDefaultPerf.start(); + ReactPerf.start(); fn(); - ReactDefaultPerf.stop(); - return ReactDefaultPerf.getLastMeasurements().__unstable_this_format_will_change; + ReactPerf.stop(); + + // Make sure none of the methods crash. + ReactPerf.getWasted(); + ReactPerf.getInclusive(); + ReactPerf.getExclusive(); + ReactPerf.getOperations(); + + return ReactPerf.getLastMeasurements(); } it('should count no-op update as waste', function() { @@ -81,20 +84,18 @@ describe('ReactDefaultPerf', function() { ReactDOM.render(, container); }); - var summary = ReactDefaultPerf.getWasted(measurements); - expect(summary.length).toBe(2); - - /*eslint-disable dot-notation */ - - expect(summary[0]['Owner > component']).toBe(' > App'); - expect(summary[0]['Wasted time (ms)']).not.toBe(0); - expect(summary[0]['Instances']).toBe(1); - - expect(summary[1]['Owner > component']).toBe('App > Box'); - expect(summary[1]['Wasted time (ms)']).not.toBe(0); - expect(summary[1]['Instances']).toBe(2); - - /*eslint-enable dot-notation */ + var summary = ReactPerf.getWasted(measurements); + expect(summary).toEqual([{ + key: 'App', + instanceCount: 1, + inclusiveRenderDuration: 3, + renderCount: 1, + }, { + key: 'App > Box', + instanceCount: 2, + inclusiveRenderDuration: 2, + renderCount: 2, + }]); }); it('should count no-op update in child as waste', function() { @@ -107,21 +108,18 @@ describe('ReactDefaultPerf', function() { ReactDOM.render(, container); }); - var summary = ReactDefaultPerf.getWasted(measurements); - expect(summary.length).toBe(1); - - /*eslint-disable dot-notation */ - - expect(summary[0]['Owner > component']).toBe('App > Box'); - expect(summary[0]['Wasted time (ms)']).not.toBe(0); - expect(summary[0]['Instances']).toBe(1); - - /*eslint-enable dot-notation */ + var summary = ReactPerf.getWasted(measurements); + expect(summary).toEqual([{ + key: 'App > Box', + instanceCount: 1, + inclusiveRenderDuration: 1, + renderCount: 1, + }]); }); function expectNoWaste(fn) { var measurements = measure(fn); - var summary = ReactDefaultPerf.getWasted(measurements); + var summary = ReactPerf.getWasted(measurements); expect(summary).toEqual([]); } @@ -217,83 +215,72 @@ describe('ReactDefaultPerf', function() { }); }); - it('putListener should not be instrumented', function() { + it('should not count replacing null with a native as waste', function() { + var element = null; + function Foo() { + return element; + } var container = document.createElement('div'); - ReactDOM.render(
hey
, container); - var measurements = measure(() => { - ReactDOM.render(
hey
, container); - }); - - var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements); - expect(summary).toEqual([]); - }); - - it('deleteListener should not be instrumented', function() { - var container = document.createElement('div'); - ReactDOM.render(
hey
, container); - var measurements = measure(() => { - ReactDOM.render(
hey
, container); - }); - - var summary = ReactDefaultPerfAnalysis.getDOMSummary(measurements); - expect(summary).toEqual([]); - }); - - it('should not fail on input change events', function() { - var container = document.createElement('div'); - var onChange = () => {}; - var input = ReactDOM.render( - , - container - ); + ReactDOM.render(, container); expectNoWaste(() => { - ReactTestUtils.Simulate.change(input); + element =
; + ReactDOM.render(, container); }); }); - it('should print a table after calling printOperations', function() { + it('should not count replacing a native with null as waste', function() { + var element =
; + function Foo() { + return element; + } + var container = document.createElement('div'); + ReactDOM.render(, container); + expectNoWaste(() => { + element = null; + ReactDOM.render(, container); + }); + }); + + it('should include stats for components unmounted during measurement', function() { var container = document.createElement('div'); var measurements = measure(() => { - ReactDOM.render(
hey
, container); + ReactDOM.render(
, container); + ReactDOM.render(
, container); }); - spyOn(console, 'table'); - ReactDefaultPerf.printOperations(measurements); - expect(console.table.calls.length).toBe(1); - expect(console.table.argsForCall[0][0]).toEqual([{ - 'data-reactid': '', - type: 'set innerHTML', - args: ReactDOMFeatureFlags.useCreateElement ? - '{"node":"","children":[],"html":null,"text":null}' : - '"
hey
"', + expect(ReactPerf.getExclusive(measurements)).toEqual([{ + key: 'Div', + instanceCount: 3, + counts: { ctor: 3, render: 4 }, + durations: { ctor: 3, render: 4 }, + totalDuration: 7, }]); }); it('warns once when using getMeasurementsSummaryMap', function() { var measurements = measure(() => {}); spyOn(console, 'error'); - ReactDefaultPerf.getMeasurementsSummaryMap(measurements); + ReactPerf.getMeasurementsSummaryMap(measurements); expect(console.error.calls.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( '`ReactPerf.getMeasurementsSummaryMap(...)` is deprecated. Use ' + '`ReactPerf.getWasted(...)` instead.' ); - ReactDefaultPerf.getMeasurementsSummaryMap(measurements); + ReactPerf.getMeasurementsSummaryMap(measurements); expect(console.error.calls.length).toBe(1); }); it('warns once when using printDOM', function() { var measurements = measure(() => {}); spyOn(console, 'error'); - ReactDefaultPerf.printDOM(measurements); + ReactPerf.printDOM(measurements); expect(console.error.calls.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( '`ReactPerf.printDOM(...)` is deprecated. Use ' + '`ReactPerf.printOperations(...)` instead.' ); - ReactDefaultPerf.printDOM(measurements); + ReactPerf.printDOM(measurements); expect(console.error.calls.length).toBe(1); }); - }); diff --git a/src/isomorphic/children/ReactChildren.js b/src/isomorphic/children/ReactChildren.js index 417f78aa28..75670dbe36 100644 --- a/src/isomorphic/children/ReactChildren.js +++ b/src/isomorphic/children/ReactChildren.js @@ -55,6 +55,8 @@ function forEachSingleChild(bookKeeping, child, name) { /** * Iterates through children that are typically specified as `props.children`. * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.foreach + * * The provided forEachFunc(child, index) will be called for each * leaf child. * @@ -146,7 +148,9 @@ function mapIntoWithKeyPrefixInternal(children, array, prefix, func, context) { /** * Maps children that are typically specified as `props.children`. * - * The provided mapFunction(child, index) will be called for each + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.map + * + * The provided mapFunction(child, key, index) will be called for each * leaf child. * * @param {?*} children Children tree container. @@ -173,6 +177,8 @@ function forEachSingleChildDummy(traverseContext, child, name) { * Count the number of children that are typically specified as * `props.children`. * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.count + * * @param {?*} children Children tree container. * @return {number} The number of children. */ @@ -184,6 +190,8 @@ function countChildren(children, context) { /** * Flatten a children object (typically specified as `props.children`) and * return an array with appropriately re-keyed children. + * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.toarray */ function toArray(children) { var result = []; diff --git a/src/isomorphic/children/onlyChild.js b/src/isomorphic/children/onlyChild.js index e1d3706c7e..505dea4246 100644 --- a/src/isomorphic/children/onlyChild.js +++ b/src/isomorphic/children/onlyChild.js @@ -16,10 +16,13 @@ var invariant = require('invariant'); /** * Returns the first child in a collection of children and verifies that there - * is only one child in the collection. The current implementation of this - * function assumes that a single child gets passed without a wrapper, but the - * purpose of this helper function is to abstract away the particular structure - * of children. + * is only one child in the collection. + * + * See https://facebook.github.io/react/docs/top-level-api.html#react.children.only + * + * The current implementation of this function assumes that a single child gets + * passed without a wrapper, but the purpose of this helper function is to + * abstract away the particular structure of children. * * @param {?object} children Child collection structure. * @return {ReactElement} The first and only `ReactElement` contained in the diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js index 56f479a42e..fe82510b89 100644 --- a/src/isomorphic/classic/class/ReactClass.js +++ b/src/isomorphic/classic/class/ReactClass.js @@ -740,6 +740,7 @@ var ReactClass = { /** * Creates a composite component class given a class specification. + * See https://facebook.github.io/react/docs/top-level-api.html#react.createclass * * @param {object} spec Class specification (which must define `render`). * @return {function} Component constructor function. diff --git a/src/isomorphic/classic/class/__tests__/ReactBind-test.js b/src/isomorphic/classic/class/__tests__/ReactBind-test.js index 63aa641a9c..bbf9d0c7da 100644 --- a/src/isomorphic/classic/class/__tests__/ReactBind-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactBind-test.js @@ -20,9 +20,9 @@ describe('autobinding', function() { it('Holds reference to instance', function() { - var mouseDidEnter = jest.genMockFn(); - var mouseDidLeave = jest.genMockFn(); - var mouseDidClick = jest.genMockFn(); + var mouseDidEnter = jest.fn(); + var mouseDidLeave = jest.fn(); + var mouseDidClick = jest.fn(); var TestBindComponent = React.createClass({ getInitialState: function() { @@ -95,7 +95,7 @@ describe('autobinding', function() { }); it('works with mixins', function() { - var mouseDidClick = jest.genMockFn(); + var mouseDidClick = jest.fn(); var TestMixin = { onClick: mouseDidClick, diff --git a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js index 395e345770..54d3c11f5c 100644 --- a/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactBindOptout-test.js @@ -20,9 +20,9 @@ describe('autobind optout', function() { it('should work with manual binding', function() { - var mouseDidEnter = jest.genMockFn(); - var mouseDidLeave = jest.genMockFn(); - var mouseDidClick = jest.genMockFn(); + var mouseDidEnter = jest.fn(); + var mouseDidLeave = jest.fn(); + var mouseDidClick = jest.fn(); var TestBindComponent = React.createClass({ autobind: false, @@ -138,7 +138,7 @@ describe('autobind optout', function() { }); it('works with mixins that have not opted out of autobinding', function() { - var mouseDidClick = jest.genMockFn(); + var mouseDidClick = jest.fn(); var TestMixin = { onClick: mouseDidClick, @@ -164,7 +164,7 @@ describe('autobind optout', function() { }); it('works with mixins that have opted out of autobinding', function() { - var mouseDidClick = jest.genMockFn(); + var mouseDidClick = jest.fn(); var TestMixin = { autobind: false, diff --git a/src/isomorphic/classic/class/__tests__/ReactClass-test.js b/src/isomorphic/classic/class/__tests__/ReactClass-test.js index 07a1e4bcb3..9a5587f7b7 100644 --- a/src/isomorphic/classic/class/__tests__/ReactClass-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactClass-test.js @@ -43,7 +43,7 @@ describe('ReactClass-spec', function() { }); it('should copy prop types onto the Constructor', function() { - var propValidator = jest.genMockFn(); + var propValidator = jest.fn(); var TestComponent = React.createClass({ propTypes: { value: propValidator, diff --git a/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js b/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js index a65c68ff07..f32af1e046 100644 --- a/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js +++ b/src/isomorphic/classic/class/__tests__/ReactClassMixin-test.js @@ -25,8 +25,8 @@ describe('ReactClass-mixin', function() { beforeEach(function() { React = require('React'); ReactTestUtils = require('ReactTestUtils'); - mixinPropValidator = jest.genMockFn(); - componentPropValidator = jest.genMockFn(); + mixinPropValidator = jest.fn(); + componentPropValidator = jest.fn(); var MixinA = { propTypes: { @@ -107,7 +107,7 @@ describe('ReactClass-mixin', function() { }); it('should support merging propTypes and statics', function() { - var listener = jest.genMockFn(); + var listener = jest.fn(); var instance = ; instance = ReactTestUtils.renderIntoDocument(instance); @@ -122,7 +122,7 @@ describe('ReactClass-mixin', function() { }); it('should support chaining delegate functions', function() { - var listener = jest.genMockFn(); + var listener = jest.fn(); var instance = ; instance = ReactTestUtils.renderIntoDocument(instance); @@ -135,7 +135,7 @@ describe('ReactClass-mixin', function() { }); it('should chain functions regardless of spec property order', function() { - var listener = jest.genMockFn(); + var listener = jest.fn(); var instance = ; instance = ReactTestUtils.renderIntoDocument(instance); diff --git a/src/isomorphic/classic/element/ReactElement.js b/src/isomorphic/classic/element/ReactElement.js index ee46a5f392..40c1c7fc29 100644 --- a/src/isomorphic/classic/element/ReactElement.js +++ b/src/isomorphic/classic/element/ReactElement.js @@ -113,6 +113,10 @@ var ReactElement = function(type, key, ref, self, source, owner, props) { return element; }; +/** + * Create and return a new ReactElement of the given type. + * See https://facebook.github.io/react/docs/top-level-api.html#react.createelement + */ ReactElement.createElement = function(type, config, children) { var propName; @@ -126,6 +130,13 @@ ReactElement.createElement = function(type, config, children) { if (config != null) { if (__DEV__) { + warning( + /* eslint-disable no-proto */ + config.__proto__ == null || config.__proto__ === Object.prototype, + /* eslint-enable no-proto */ + 'React.createElement(...): Expected props argument to be a plain object. ' + + 'Properties defined in its prototype chain will be ignored.' + ); ref = !config.hasOwnProperty('ref') || Object.getOwnPropertyDescriptor(config, 'ref').get ? null : config.ref; key = !config.hasOwnProperty('key') || @@ -223,6 +234,10 @@ ReactElement.createElement = function(type, config, children) { ); }; +/** + * Return a function that produces ReactElements of a given type. + * See https://facebook.github.io/react/docs/top-level-api.html#react.createfactory + */ ReactElement.createFactory = function(type) { var factory = ReactElement.createElement.bind(null, type); // Expose the type on the factory and the prototype so that it can be @@ -248,6 +263,10 @@ ReactElement.cloneAndReplaceKey = function(oldElement, newKey) { return newElement; }; +/** + * Clone and return a new ReactElement using element as the starting point. + * See https://facebook.github.io/react/docs/top-level-api.html#react.cloneelement + */ ReactElement.cloneElement = function(element, config, children) { var propName; @@ -268,6 +287,15 @@ ReactElement.cloneElement = function(element, config, children) { var owner = element._owner; if (config != null) { + if (__DEV__) { + warning( + /* eslint-disable no-proto */ + config.__proto__ == null || config.__proto__ === Object.prototype, + /* eslint-enable no-proto */ + 'React.cloneElement(...): Expected props argument to be a plain object. ' + + 'Properties defined in its prototype chain will be ignored.' + ); + } if (config.ref !== undefined) { // Silently steal the ref from the parent. ref = config.ref; @@ -319,6 +347,8 @@ ReactElement.cloneElement = function(element, config, children) { }; /** + * Verifies the object is a ReactElement. + * See https://facebook.github.io/react/docs/top-level-api.html#react.isvalidelement * @param {?object} object * @return {boolean} True if `object` is a valid component. * @final diff --git a/src/isomorphic/classic/element/__tests__/ReactElement-test.js b/src/isomorphic/classic/element/__tests__/ReactElement-test.js index 402c9b95ed..8b3900db9f 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElement-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElement-test.js @@ -138,6 +138,18 @@ describe('ReactElement', function() { expect(element.props.foo).toBe(1); }); + it('warns if the config object inherits from any type other than Object', function() { + spyOn(console, 'error'); + React.createElement('div', {foo: 1}); + expect(console.error).not.toHaveBeenCalled(); + React.createElement('div', Object.create({foo: 1})); + expect(console.error.argsForCall.length).toBe(1); + expect(console.error.argsForCall[0][0]).toContain( + 'React.createElement(...): Expected props argument to be a plain object. ' + + 'Properties defined in its prototype chain will be ignored.' + ); + }); + it('extracts key and ref from the config', function() { var element = React.createFactory(ComponentClass)({ key: '12', diff --git a/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js b/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js index f4c5b4b761..20b4089726 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementClone-test.js @@ -66,6 +66,18 @@ describe('ReactElementClone', function() { expect(ReactDOM.findDOMNode(component).childNodes[0].className).toBe('xyz'); }); + it('should warn if the config object inherits from any type other than Object', function() { + spyOn(console, 'error'); + React.cloneElement('div', {foo: 1}); + expect(console.error).not.toHaveBeenCalled(); + React.cloneElement('div', Object.create({foo: 1})); + expect(console.error.argsForCall.length).toBe(1); + expect(console.error.argsForCall[0][0]).toContain( + 'React.cloneElement(...): Expected props argument to be a plain object. ' + + 'Properties defined in its prototype chain will be ignored.' + ); + }); + it('should keep the original ref if it is not overridden', function() { var Grandparent = React.createClass({ render: function() { diff --git a/src/isomorphic/devtools/ReactComponentTreeDevtool.js b/src/isomorphic/devtools/ReactComponentTreeDevtool.js new file mode 100644 index 0000000000..85902c30d0 --- /dev/null +++ b/src/isomorphic/devtools/ReactComponentTreeDevtool.js @@ -0,0 +1,163 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactComponentTreeDevtool + */ + +'use strict'; + +var invariant = require('invariant'); + +var tree = {}; +var rootIDs = []; + +function updateTree(id, update) { + if (!tree[id]) { + tree[id] = { + parentID: null, + ownerID: null, + text: null, + childIDs: [], + displayName: 'Unknown', + isMounted: false, + updateCount: 0, + }; + } + update(tree[id]); +} + +function purgeDeep(id) { + var item = tree[id]; + if (item) { + var {childIDs} = item; + delete tree[id]; + childIDs.forEach(purgeDeep); + } +} + +var ReactComponentTreeDevtool = { + onSetDisplayName(id, displayName) { + updateTree(id, item => item.displayName = displayName); + }, + + onSetChildren(id, nextChildIDs) { + updateTree(id, item => { + var prevChildIDs = item.childIDs; + item.childIDs = nextChildIDs; + + nextChildIDs.forEach(nextChildID => { + var nextChild = tree[nextChildID]; + invariant( + nextChild, + 'Expected devtool events to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + invariant( + nextChild.displayName != null, + 'Expected onSetDisplayName() to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + invariant( + nextChild.childIDs != null || nextChild.text != null, + 'Expected onSetChildren() or onSetText() to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + invariant( + nextChild.isMounted, + 'Expected onMountComponent() to fire for the child ' + + 'before its parent includes it in onSetChildren().' + ); + + if (prevChildIDs.indexOf(nextChildID) === -1) { + nextChild.parentID = id; + } + }); + }); + }, + + onSetOwner(id, ownerID) { + updateTree(id, item => item.ownerID = ownerID); + }, + + onSetText(id, text) { + updateTree(id, item => item.text = text); + }, + + onMountComponent(id) { + updateTree(id, item => item.isMounted = true); + }, + + onMountRootComponent(id) { + rootIDs.push(id); + }, + + onUpdateComponent(id) { + updateTree(id, item => item.updateCount++); + }, + + onUnmountComponent(id) { + updateTree(id, item => item.isMounted = false); + rootIDs = rootIDs.filter(rootID => rootID !== id); + }, + + purgeUnmountedComponents() { + if (ReactComponentTreeDevtool._preventPurging) { + // Should only be used for testing. + return; + } + + Object.keys(tree) + .filter(id => !tree[id].isMounted) + .forEach(purgeDeep); + }, + + isMounted(id) { + var item = tree[id]; + return item ? item.isMounted : false; + }, + + getChildIDs(id) { + var item = tree[id]; + return item ? item.childIDs : []; + }, + + getDisplayName(id) { + var item = tree[id]; + return item ? item.displayName : 'Unknown'; + }, + + getOwnerID(id) { + var item = tree[id]; + return item ? item.ownerID : null; + }, + + getParentID(id) { + var item = tree[id]; + return item ? item.parentID : null; + }, + + getText(id) { + var item = tree[id]; + return item ? item.text : null; + }, + + getUpdateCount(id) { + var item = tree[id]; + return item ? item.updateCount : 0; + }, + + getRootIDs() { + return rootIDs; + }, + + getRegisteredIDs() { + return Object.keys(tree); + }, +}; + +module.exports = ReactComponentTreeDevtool; diff --git a/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js b/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js new file mode 100644 index 0000000000..46b8194465 --- /dev/null +++ b/src/isomorphic/devtools/ReactNativeOperationHistoryDevtool.js @@ -0,0 +1,39 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactNativeOperationHistoryDevtool + */ + +'use strict'; + +var history = []; + +var ReactNativeOperationHistoryDevtool = { + onNativeOperation(debugID, type, payload) { + history.push({ + instanceID: debugID, + type, + payload, + }); + }, + + clearHistory() { + if (ReactNativeOperationHistoryDevtool._preventClearing) { + // Should only be used for tests. + return; + } + + history = []; + }, + + getHistory() { + return history; + }, +}; + +module.exports = ReactNativeOperationHistoryDevtool; diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js new file mode 100644 index 0000000000..1c5cdb7049 --- /dev/null +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.js @@ -0,0 +1,1744 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactComponentTreeDevtool', () => { + var React; + var ReactDOM; + var ReactDOMServer; + var ReactInstanceMap; + var ReactComponentTreeDevtool; + + beforeEach(() => { + jest.resetModuleRegistry(); + + React = require('React'); + ReactDOM = require('ReactDOM'); + ReactDOMServer = require('ReactDOMServer'); + ReactInstanceMap = require('ReactInstanceMap'); + ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); + }); + + function getRootDisplayNames() { + return ReactComponentTreeDevtool.getRootIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getRegisteredDisplayNames() { + return ReactComponentTreeDevtool.getRegisteredIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getTree(rootID, options = {}) { + var { + includeOwnerDisplayName = false, + includeParentDisplayName = false, + expectedParentID = null, + } = options; + + var result = { + displayName: ReactComponentTreeDevtool.getDisplayName(rootID), + }; + + var ownerID = ReactComponentTreeDevtool.getOwnerID(rootID); + var parentID = ReactComponentTreeDevtool.getParentID(rootID); + expect(parentID).toBe(expectedParentID); + + if (includeParentDisplayName && parentID) { + result.parentDisplayName = ReactComponentTreeDevtool.getDisplayName(parentID); + } + if (includeOwnerDisplayName && ownerID) { + result.ownerDisplayName = ReactComponentTreeDevtool.getDisplayName(ownerID); + } + + var childIDs = ReactComponentTreeDevtool.getChildIDs(rootID); + var text = ReactComponentTreeDevtool.getText(rootID); + if (text != null) { + result.text = text; + } else { + result.children = childIDs.map(childID => + getTree(childID, {...options, expectedParentID: rootID }) + ); + } + + return result; + } + + function assertTreeMatches(pairs, options) { + if (!Array.isArray(pairs[0])) { + pairs = [pairs]; + } + + var node = document.createElement('div'); + var currentElement; + var rootInstance; + + class Wrapper extends React.Component { + render() { + rootInstance = ReactInstanceMap.get(this); + return currentElement; + } + } + + function getActualTree() { + return getTree(rootInstance._debugID, options).children[0]; + } + + // Mount once, render updates, then unmount. + // Ensure the tree is correct on every step. + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + + // Mount a new tree or update the existing tree. + ReactDOM.render(, node); + expect(getActualTree()).toEqual(expectedTree); + + // Purging should have no effect + // on the tree we expect to see. + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toEqual(expectedTree); + }); + + // Unmounting the root node should purge + // the whole subtree automatically. + ReactDOM.unmountComponentAtNode(node); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + + // Server render every pair. + // Ensure the tree is correct on every step. + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + + // Rendering to string should not produce any entries + // because ReactDebugTool purges it when the flush ends. + ReactDOMServer.renderToString(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + + // To test it, we tell the devtool to ignore next purge + // so the cleanup request by ReactDebugTool is ignored. + // This lets us make assertions on the actual tree. + ReactComponentTreeDevtool._preventPurging = true; + ReactDOMServer.renderToString(); + ReactComponentTreeDevtool._preventPurging = false; + expect(getActualTree()).toEqual(expectedTree); + + // Purge manually since we skipped the automatic purge. + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + }); + } + + describe('mount', () => { + it('uses displayName or Unknown for classic components', () => { + var Foo = React.createClass({ + render() { + return null; + }, + }); + Foo.displayName = 'Bar'; + var Baz = React.createClass({ + render() { + return null; + }, + }); + var Qux = React.createClass({ + render() { + return null; + }, + }); + delete Qux.displayName; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or ReactComponent for modern components', () => { + class Foo extends React.Component { + render() { + return null; + } + } + Foo.displayName = 'Bar'; + class Baz extends React.Component { + render() { + return null; + } + } + class Qux extends React.Component { + render() { + return null; + } + } + delete Qux.name; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + // Note: Ideally fallback name should be consistent (e.g. "Unknown") + displayName: 'ReactComponent', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or Object for factory components', () => { + function Foo() { + return { + render() { + return null; + }, + }; + } + Foo.displayName = 'Bar'; + function Baz() { + return { + render() { + return null; + }, + }; + } + function Qux() { + return { + render() { + return null; + }, + }; + } + delete Qux.name; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or StatelessComponent for functional components', () => { + function Foo() { + return null; + } + Foo.displayName = 'Bar'; + function Baz() { + return null; + } + function Qux() { + return null; + } + delete Qux.name; + + var element =
; + var tree = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a native tree correctly', () => { + var element = ( +
+

+ + Hi! + + Wow. +

+
+
+ ); + var tree = { + displayName: 'div', + children: [{ + displayName: 'p', + children: [{ + displayName: 'span', + children: [{ + displayName: '#text', + text: 'Hi!', + }], + }, { + displayName: '#text', + text: 'Wow.', + }], + }, { + displayName: 'hr', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a simple tree with composites correctly', () => { + class Foo extends React.Component { + render() { + return
; + } + } + + var element = ; + var tree = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a tree with composites correctly', () => { + var Qux = React.createClass({ + render() { + return null; + }, + }); + function Foo() { + return { + render() { + return ; + }, + }; + } + function Bar({children}) { + return

{children}

; + } + class Baz extends React.Component { + render() { + return ( +
+ + + Hi, + Mom + + Click me. +
+ ); + } + } + + var element = ; + var tree = { + displayName: 'Baz', + children: [{ + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Qux', + children: [], + }], + }, { + displayName: 'Bar', + children: [{ + displayName: 'h1', + children: [{ + displayName: 'span', + children: [{ + displayName: '#text', + text: 'Hi,', + }], + }, { + displayName: '#text', + text: 'Mom', + }], + }], + }, { + displayName: 'a', + children: [{ + displayName: '#text', + text: 'Click me.', + }], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores null children', () => { + class Foo extends React.Component { + render() { + return null; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores false children', () => { + class Foo extends React.Component { + render() { + return false; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('reports text nodes as children', () => { + var element =
{'1'}{2}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '1', + }, { + displayName: '#text', + text: '2', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single text node as a child', () => { + var element =
{'1'}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '1', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single number node as a child', () => { + var element =
{42}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '42', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a zero as a child', () => { + var element =
{0}
; + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: '0', + }], + }; + assertTreeMatches([element, tree]); + }); + + it('skips empty nodes for multiple children', () => { + function Foo() { + return
; + } + var element = ( +
+ {'hi'} + {false} + {42} + {null} + +
+ ); + var tree = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'hi', + }, { + displayName: '#text', + text: '42', + }, { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports html content as no children', () => { + var element =
; + var tree = { + displayName: 'div', + children: [], + }; + assertTreeMatches([element, tree]); + }); + }); + + describe('update', () => { + describe('native component', () => { + it('updates text of a single text child', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
Bye.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single text child', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
Hi.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single text child to no children', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from html content to a single text child', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
Hi.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single text child to html content', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to multiple text children', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
{'Hi.'}{'Bye.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to no children', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from html content to multiple text children', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
{'Hi.'}{'Bye.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to html content', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from html content to no children', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to html content', () => { + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from one text child to multiple text children', () => { + var elementBefore =
Hi.
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + + var elementAfter =
{'Hi.'}{'Bye.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to one text child', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
Hi.
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates text nodes when reordering', () => { + var elementBefore =
{'Hi.'}{'Bye.'}
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }; + + var elementAfter =
{'Bye.'}{'Hi.'}
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }, { + displayName: '#text', + text: 'Hi.', + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering with keys', () => { + var elementBefore = ( +
+
Hi.
+
Bye.
+
+ ); + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = ( +
+
Bye.
+
Hi.
+
+ ); + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering without keys', () => { + var elementBefore = ( +
+
Hi.
+
Bye.
+
+ ); + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = ( +
+
Bye.
+
Hi.
+
+ ); + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }, { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of a different type', () => { + function Foo() { + return null; + } + + function Bar() { + return null; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of the same type', () => { + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'span', + children: [], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single composite child', () => { + function Foo() { + return null; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single composite child to no children', () => { + function Foo() { + return null; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'div', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates mixed children', () => { + function Foo() { + return
; + } + var element1 = ( +
+ {'hi'} + {false} + {42} + {null} + +
+ ); + var tree1 = { + displayName: 'div', + children: [{ + displayName: '#text', + text: 'hi', + }, { + displayName: '#text', + text: '42', + }, { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + + var element2 = ( +
+ + {false} + {'hi'} + {null} +
+ ); + var tree2 = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }, { + displayName: '#text', + text: 'hi', + }], + }; + + var element3 = ( +
+ +
+ ); + var tree3 = { + displayName: 'div', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }], + }; + + assertTreeMatches([ + [element1, tree1], + [element2, tree2], + [element3, tree3], + ]); + }); + }); + + describe('functional component', () => { + it('updates with a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'span', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + + describe('class component', () => { + it('updates with a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'span', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore =
; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter =
; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'div', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + }); + + it('tracks owner correctly', () => { + class Foo extends React.Component { + render() { + return

Hi.

; + } + } + function Bar({children}) { + return
{children} Mom
; + } + + // Note that owner is not calculated for text nodes + // because they are not created from real elements. + var element =
; + var tree = { + displayName: 'article', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Bar', + ownerDisplayName: 'Foo', + children: [{ + displayName: 'div', + ownerDisplayName: 'Bar', + children: [{ + displayName: 'h1', + ownerDisplayName: 'Foo', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }, { + displayName: '#text', + text: ' Mom', + }], + }], + }], + }], + }; + assertTreeMatches([element, tree], {includeOwnerDisplayName: true}); + }); + + it('purges unmounted components automatically', () => { + var node = document.createElement('div'); + var renderBar = true; + var fooInstance; + var barInstance; + + class Foo extends React.Component { + render() { + fooInstance = ReactInstanceMap.get(this); + return renderBar ? : null; + } + } + + class Bar extends React.Component { + render() { + barInstance = ReactInstanceMap.get(this); + return null; + } + } + + ReactDOM.render(, node); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + renderBar = false; + ReactDOM.render(, node); + expect( + getTree(barInstance._debugID, {expectedParentID: null}) + ).toEqual({ + displayName: 'Unknown', + children: [], + }); + + ReactDOM.unmountComponentAtNode(node); + expect( + getTree(barInstance._debugID, {expectedParentID: null}) + ).toEqual({ + displayName: 'Unknown', + children: [], + }); + }); + + it('reports update counts', () => { + var node = document.createElement('div'); + + ReactDOM.render(
, node); + var divID = ReactComponentTreeDevtool.getRootIDs()[0]; + expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0); + + ReactDOM.render(, node); + var spanID = ReactComponentTreeDevtool.getRootIDs()[0]; + expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(0); + + ReactDOM.render(, node); + expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(1); + + ReactDOM.render(, node); + expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(2); + + ReactDOM.unmountComponentAtNode(node); + expect(ReactComponentTreeDevtool.getUpdateCount(divID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(spanID)).toEqual(0); + }); + + it('does not report top-level wrapper as a root', () => { + var node = document.createElement('div'); + + ReactDOM.render(
, node); + expect(getRootDisplayNames()).toEqual(['div']); + + ReactDOM.render(
, node); + expect(getRootDisplayNames()).toEqual(['div']); + + ReactDOM.unmountComponentAtNode(node); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + }); +}); diff --git a/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js new file mode 100644 index 0000000000..f3b026b459 --- /dev/null +++ b/src/isomorphic/devtools/__tests__/ReactComponentTreeDevtool-test.native.js @@ -0,0 +1,1716 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactComponentTreeDevtool', () => { + var React; + var ReactNative; + var ReactInstanceMap; + var ReactComponentTreeDevtool; + var createReactNativeComponentClass; + var View; + var Image; + var Text; + + beforeEach(() => { + jest.resetModuleRegistry(); + + React = require('React'); + ReactNative = require('ReactNative'); + ReactInstanceMap = require('ReactInstanceMap'); + ReactComponentTreeDevtool = require('ReactComponentTreeDevtool'); + View = require('View'); + createReactNativeComponentClass = require('createReactNativeComponentClass'); + Image = createReactNativeComponentClass({ + validAttributes: {}, + uiViewClassName: 'Image', + }); + var RCText = createReactNativeComponentClass({ + validAttributes: {}, + uiViewClassName: 'RCText', + }); + Text = React.createClass({ + childContextTypes: { + isInAParentText: React.PropTypes.bool, + }, + getChildContext() { + return {isInAParentText: true}; + }, + render() { + return ; + }, + }); + }); + + function getRootDisplayNames() { + return ReactComponentTreeDevtool.getRootIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getRegisteredDisplayNames() { + return ReactComponentTreeDevtool.getRegisteredIDs() + .map(ReactComponentTreeDevtool.getDisplayName); + } + + function getTree(rootID, options = {}) { + var { + includeOwnerDisplayName = false, + includeParentDisplayName = false, + expectedParentID = null, + } = options; + + var result = { + displayName: ReactComponentTreeDevtool.getDisplayName(rootID), + }; + + var ownerID = ReactComponentTreeDevtool.getOwnerID(rootID); + var parentID = ReactComponentTreeDevtool.getParentID(rootID); + expect(parentID).toBe(expectedParentID); + + if (includeParentDisplayName && parentID) { + result.parentDisplayName = ReactComponentTreeDevtool.getDisplayName(parentID); + } + if (includeOwnerDisplayName && ownerID) { + result.ownerDisplayName = ReactComponentTreeDevtool.getDisplayName(ownerID); + } + + var childIDs = ReactComponentTreeDevtool.getChildIDs(rootID); + var text = ReactComponentTreeDevtool.getText(rootID); + if (text != null) { + result.text = text; + } else { + result.children = childIDs.map(childID => + getTree(childID, {...options, expectedParentID: rootID }) + ); + } + + return result; + } + + function assertTreeMatches(pairs, options) { + if (!Array.isArray(pairs[0])) { + pairs = [pairs]; + } + + var currentElement; + var rootInstance; + + class Wrapper extends React.Component { + render() { + rootInstance = ReactInstanceMap.get(this); + return currentElement; + } + } + + function getActualTree() { + return getTree(rootInstance._debugID, options).children[0]; + } + + // Mount once, render updates, then unmount. + // Ensure the tree is correct on every step. + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + + // Mount a new tree or update the existing tree. + ReactNative.render(, 1); + expect(getActualTree()).toEqual(expectedTree); + + // Purging should have no effect + // on the tree we expect to see. + ReactComponentTreeDevtool.purgeUnmountedComponents(); + expect(getActualTree()).toEqual(expectedTree); + }); + + // Unmounting the root node should purge + // the whole subtree automatically. + ReactNative.unmountComponentAtNode(1); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + + // Mount and unmount for every pair. + // Ensure the tree is correct on every step. + pairs.forEach(([element, expectedTree]) => { + currentElement = element; + + // Mount a new tree. + ReactNative.render(, 1); + expect(getActualTree()).toEqual(expectedTree); + + // Unmounting should clean it up. + ReactNative.unmountComponentAtNode(1); + expect(getActualTree()).toBe(undefined); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + }); + } + + describe('mount', () => { + it('uses displayName or Unknown for classic components', () => { + var Foo = React.createClass({ + render() { + return null; + }, + }); + Foo.displayName = 'Bar'; + var Baz = React.createClass({ + render() { + return null; + }, + }); + var Qux = React.createClass({ + render() { + return null; + }, + }); + delete Qux.displayName; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or ReactComponent for modern components', () => { + class Foo extends React.Component { + render() { + return null; + } + } + Foo.displayName = 'Bar'; + class Baz extends React.Component { + render() { + return null; + } + } + class Qux extends React.Component { + render() { + return null; + } + } + delete Qux.name; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + // Note: Ideally fallback name should be consistent (e.g. "Unknown") + displayName: 'ReactComponent', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or Object for factory components', () => { + function Foo() { + return { + render() { + return null; + }, + }; + } + Foo.displayName = 'Bar'; + function Baz() { + return { + render() { + return null; + }, + }; + } + function Qux() { + return { + render() { + return null; + }, + }; + } + delete Qux.name; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('uses displayName, name, or StatelessComponent for functional components', () => { + function Foo() { + return null; + } + Foo.displayName = 'Bar'; + function Baz() { + return null; + } + function Qux() { + return null; + } + delete Qux.name; + + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }, { + displayName: 'Baz', + children: [], + }, { + displayName: 'Unknown', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a native tree correctly', () => { + var element = ( + + + + Hi! + + + + + ); + var tree = { + displayName: 'View', + children: [{ + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi!', + }], + }], + }], + }, { + displayName: 'Image', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a simple tree with composites correctly', () => { + class Foo extends React.Component { + render() { + return ; + } + } + + var element = ; + var tree = { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a tree with composites correctly', () => { + var Qux = React.createClass({ + render() { + return null; + }, + }); + function Foo() { + return { + render() { + return ; + }, + }; + } + function Bar({children}) { + return {children}; + } + class Baz extends React.Component { + render() { + return ( + + + + Hi, + + + + ); + } + } + + var element = ; + var tree = { + displayName: 'Baz', + children: [{ + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Qux', + children: [], + }], + }, { + displayName: 'Bar', + children: [{ + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi,', + }], + }], + }], + }], + }, { + displayName: 'Image', + children: [], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores null children', () => { + class Foo extends React.Component { + render() { + return null; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('ignores false children', () => { + class Foo extends React.Component { + render() { + return false; + } + } + var element = ; + var tree = { + displayName: 'Foo', + children: [], + }; + assertTreeMatches([element, tree]); + }); + + it('reports text nodes as children', () => { + var element = {'1'}{2}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '1', + }, { + displayName: '#text', + text: '2', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single text node as a child', () => { + var element = {'1'}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '1', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a single number node as a child', () => { + var element = {42}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '42', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('reports a zero as a child', () => { + var element = {0}; + var tree = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '0', + }], + }], + }; + assertTreeMatches([element, tree]); + }); + + it('skips empty nodes for multiple children', () => { + function Foo() { + return ; + } + var element = ( + + {false} + + {null} + + + ); + var tree = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }, { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }], + }; + assertTreeMatches([element, tree]); + }); + }); + + describe('update', () => { + describe('native component', () => { + it('updates text of a single text child', () => { + var elementBefore = Hi.; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + var elementAfter = Bye.; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single text child', () => { + var elementBefore = ; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + + var elementAfter = Hi.; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single text child to no children', () => { + var elementBefore = Hi.; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to multiple text children', () => { + var elementBefore = ; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + + var elementAfter = {'Hi.'}{'Bye.'}; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to no children', () => { + var elementBefore = {'Hi.'}{'Bye.'}; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [], + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from one text child to multiple text children', () => { + var elementBefore = Hi.; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + + var elementAfter = {'Hi.'}{'Bye.'}; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from multiple text children to one text child', () => { + var elementBefore = {'Hi.'}{'Bye.'}; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = Hi.; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates text nodes when reordering', () => { + var elementBefore = {'Hi.'}{'Bye.'}; + var treeBefore = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }, { + displayName: '#text', + text: 'Bye.', + }], + }], + }; + + var elementAfter = {'Bye.'}{'Hi.'}; + var treeAfter = { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }, { + displayName: '#text', + text: 'Hi.', + }], + }], + }; + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering with keys', () => { + var elementBefore = ( + + Hi. + Bye. + + ); + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }], + }; + + var elementAfter = ( + + Bye. + Hi. + + ); + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates native nodes when reordering with keys', () => { + var elementBefore = ( + + Hi. + Bye. + + ); + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }], + }; + + var elementAfter = ( + + Bye. + Hi. + + ); + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Bye.', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of a different type', () => { + function Foo() { + return null; + } + + function Bar() { + return null; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates a single composite child of the same type', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from no children to a single composite child', () => { + function Foo() { + return null; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a single composite child to no children', () => { + function Foo() { + return null; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'View', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates mixed children', () => { + function Foo() { + return ; + } + var element1 = ( + + hi + {false} + {42} + {null} + + + ); + var tree1 = { + displayName: 'View', + children: [{ + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'hi', + }], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: '42', + }], + }], + }, { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }], + }; + + var element2 = ( + + + {false} + hi + {null} + + ); + var tree2 = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }, { + displayName: 'Text', + children: [{ + displayName: 'RCText', + children: [{ + displayName: '#text', + text: 'hi', + }], + }], + }], + }; + + var element3 = ( + + + + ); + var tree3 = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }], + }; + + assertTreeMatches([ + [element1, tree1], + [element2, tree2], + [element3, tree3], + ]); + }); + }); + + describe('functional component', () => { + it('updates with a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + function Bar() { + return null; + } + + function Foo({ children }) { + return children; + } + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + + describe('class component', () => { + it('updates with a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Image', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a native child', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to null', () => { + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a native child to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to a native child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'View', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from null to a composite child', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = {null}; + var treeBefore = { + displayName: 'Foo', + children: [], + }; + + var elementAfter = ; + var treeAfter = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + + it('updates from a composite child to null', () => { + var Bar = React.createClass({ + render() { + return null; + }, + }); + + var Foo = React.createClass({ + render() { + return this.props.children; + }, + }); + + var elementBefore = ; + var treeBefore = { + displayName: 'Foo', + children: [{ + displayName: 'Bar', + children: [], + }], + }; + + var elementAfter = {null}; + var treeAfter = { + displayName: 'Foo', + children: [], + }; + + assertTreeMatches([ + [elementBefore, treeBefore], + [elementAfter, treeAfter], + ]); + }); + }); + }); + + it('tracks owner correctly', () => { + class Foo extends React.Component { + render() { + return Hi.; + } + } + function Bar({children}) { + return {children}Mom; + } + + // Note that owner is not calculated for text nodes + // because they are not created from real elements. + var element = ; + var tree = { + displayName: 'View', + children: [{ + displayName: 'Foo', + children: [{ + displayName: 'Bar', + ownerDisplayName: 'Foo', + children: [{ + displayName: 'View', + ownerDisplayName: 'Bar', + children: [{ + displayName: 'Text', + ownerDisplayName: 'Foo', + children: [{ + displayName: 'RCText', + ownerDisplayName: 'Text', + children: [{ + displayName: '#text', + text: 'Hi.', + }], + }], + }, { + displayName: 'Text', + ownerDisplayName: 'Bar', + children: [{ + displayName: 'RCText', + ownerDisplayName: 'Text', + children: [{ + displayName: '#text', + text: 'Mom', + }], + }], + }], + }], + }], + }], + }; + assertTreeMatches([element, tree], {includeOwnerDisplayName: true}); + }); + + it('purges unmounted components automatically', () => { + var renderBar = true; + var fooInstance; + var barInstance; + + class Foo extends React.Component { + render() { + fooInstance = ReactInstanceMap.get(this); + return renderBar ? : null; + } + } + + class Bar extends React.Component { + render() { + barInstance = ReactInstanceMap.get(this); + return null; + } + } + + ReactNative.render(, 1); + expect( + getTree(barInstance._debugID, { + includeParentDisplayName: true, + expectedParentID: fooInstance._debugID, + }) + ).toEqual({ + displayName: 'Bar', + parentDisplayName: 'Foo', + children: [], + }); + + renderBar = false; + ReactNative.render(, 1); + expect( + getTree(barInstance._debugID, {expectedParentID: null}) + ).toEqual({ + displayName: 'Unknown', + children: [], + }); + + ReactNative.unmountComponentAtNode(1); + expect( + getTree(barInstance._debugID, {expectedParentID: null}) + ).toEqual({ + displayName: 'Unknown', + children: [], + }); + }); + + it('reports update counts', () => { + ReactNative.render(, 1); + var viewID = ReactComponentTreeDevtool.getRootIDs()[0]; + expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0); + + ReactNative.render(, 1); + var imageID = ReactComponentTreeDevtool.getRootIDs()[0]; + expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(0); + + ReactNative.render(, 1); + expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(1); + + ReactNative.render(, 1); + expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(2); + + ReactNative.unmountComponentAtNode(1); + expect(ReactComponentTreeDevtool.getUpdateCount(viewID)).toEqual(0); + expect(ReactComponentTreeDevtool.getUpdateCount(imageID)).toEqual(0); + }); + + it('does not report top-level wrapper as a root', () => { + ReactNative.render(, 1); + expect(getRootDisplayNames()).toEqual(['View']); + + ReactNative.render(, 1); + expect(getRootDisplayNames()).toEqual(['View']); + + ReactNative.unmountComponentAtNode(1); + expect(getRootDisplayNames()).toEqual([]); + expect(getRegisteredDisplayNames()).toEqual([]); + }); +}); diff --git a/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js new file mode 100644 index 0000000000..769fe675b1 --- /dev/null +++ b/src/isomorphic/devtools/__tests__/ReactNativeOperationHistoryDevtool-test.js @@ -0,0 +1,724 @@ +/** + * Copyright 2016-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactNativeOperationHistoryDevtool', () => { + var React; + var ReactDOM; + var ReactDOMComponentTree; + var ReactDOMFeatureFlags; + var ReactNativeOperationHistoryDevtool; + + beforeEach(() => { + jest.resetModuleRegistry(); + + React = require('React'); + ReactDOM = require('ReactDOM'); + ReactDOMComponentTree = require('ReactDOMComponentTree'); + ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + ReactNativeOperationHistoryDevtool = require('ReactNativeOperationHistoryDevtool'); + }); + + function assertHistoryMatches(expectedHistory) { + var actualHistory = ReactNativeOperationHistoryDevtool.getHistory(); + expect(actualHistory).toEqual(expectedHistory); + } + + describe('mount', () => { + it('gets recorded for native roots', () => { + var node = document.createElement('div'); + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(

Hi.

, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: ReactDOMFeatureFlags.useCreateElement ? + 'DIV' : + '

Hi.

', + }]); + }); + + it('gets recorded for composite roots', () => { + function Foo() { + return

Hi.

; + } + var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: ReactDOMFeatureFlags.useCreateElement ? + 'DIV' : + '
' + + '

Hi.

', + }]); + }); + + it('gets ignored for composite roots that return null', () => { + function Foo() { + return null; + } + var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + + // Empty DOM components should be invisible to devtools. + assertHistoryMatches([]); + }); + + it('gets recorded when a native is mounted deeply instead of null', () => { + var element; + function Foo() { + return element; + } + + ReactNativeOperationHistoryDevtool._preventClearing = true; + + var node = document.createElement('div'); + element = null; + ReactDOM.render(, node); + + element = ; + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + // Since empty components should be invisible to devtools, + // we record a "mount" event rather than a "replace with". + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: 'SPAN', + }]); + }); + }); + + describe('update styles', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update styles', + payload: { + color: 'red', + backgroundColor: 'yellow', + }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'DIV', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '
', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update styles', + payload: { color: 'red' }, + }, { + instanceID: inst._debugID, + type: 'update styles', + payload: { color: 'blue', backgroundColor: 'yellow' }, + }, { + instanceID: inst._debugID, + type: 'update styles', + payload: { color: '', backgroundColor: 'green' }, + }, { + instanceID: inst._debugID, + type: 'update styles', + payload: { backgroundColor: '' }, + }]); + }); + + it('gets ignored if the styles are shallowly equal', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update styles', + payload: { + color: 'red', + backgroundColor: 'yellow', + }, + }]); + }); + }); + + describe('update attribute', () => { + describe('simple attribute', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'DIV', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '
', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'mad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'className', + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 43 }, + }]); + }); + }); + + describe('attribute that gets removed with certain values', () => { + it('gets recorded as a removal during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { disabled: true }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'disabled', + }]); + }); + }); + + describe('custom attribute', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-x': 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-y': 42 }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'DIV', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '
', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + ReactDOM.render(
, node); + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-x': 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-x': 'mad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-y': 42 }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'data-x', + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { 'data-y': 43 }, + }]); + }); + }); + + describe('attribute on a web component', () => { + it('gets recorded during mount', () => { + var node = document.createElement('div'); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + if (ReactDOMFeatureFlags.useCreateElement) { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'mount', + payload: 'MY-COMPONENT', + }]); + } else { + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'mount', + payload: '', + }]); + } + }); + + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + ReactDOM.render(, node); + ReactDOM.render(, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'rad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { className: 'mad' }, + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 42 }, + }, { + instanceID: inst._debugID, + type: 'remove attribute', + payload: 'className', + }, { + instanceID: inst._debugID, + type: 'update attribute', + payload: { tabIndex: 43 }, + }]); + }); + }); + }); + + describe('replace text', () => { + describe('text content', () => { + it('gets recorded during an update from text content', () => { + var node = document.createElement('div'); + ReactDOM.render(
Hi.
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
Bye.
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace text', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from html', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
Bye.
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace text', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from children', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
Bye.
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 0}, + }, { + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 1}, + }, { + instanceID: inst._debugID, + type: 'replace text', + payload: 'Bye.', + }]); + }); + + it('gets ignored if new text is equal', () => { + var node = document.createElement('div'); + ReactDOM.render(
Hi.
, node); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
Hi.
, node); + + assertHistoryMatches([]); + }); + }); + + describe('text node', () => { + it('gets recorded during an update', () => { + var node = document.createElement('div'); + ReactDOM.render(
{'Hi.'}{42}
, node); + var inst1 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[0]); + var inst2 = ReactDOMComponentTree.getInstanceFromNode(node.firstChild.childNodes[3]); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
{'Bye.'}{43}
, node); + + assertHistoryMatches([{ + instanceID: inst1._debugID, + type: 'replace text', + payload: 'Bye.', + }, { + instanceID: inst2._debugID, + type: 'replace text', + payload: '43', + }]); + }); + + it('gets ignored if new text is equal', () => { + var node = document.createElement('div'); + ReactDOM.render(
{'Hi.'}{42}
, node); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
{'Hi.'}{42}
, node); + + assertHistoryMatches([]); + }); + }); + }); + + describe('replace with', () => { + it('gets recorded when composite renders to a different type', () => { + var element; + function Foo() { + return element; + } + + var node = document.createElement('div'); + element =
; + ReactDOM.render(, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + element = ; + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace with', + payload: 'SPAN', + }]); + }); + + it('gets recorded when composite renders to null after a native', () => { + var element; + function Foo() { + return element; + } + + var node = document.createElement('div'); + element = ; + ReactDOM.render(, node); + + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + element = null; + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace with', + payload: '#comment', + }]); + }); + + it('gets ignored if the type has not changed', () => { + var element; + function Foo() { + return element; + } + + var node = document.createElement('div'); + element =
; + ReactDOM.render(, node); + + element =
; + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(, node); + + assertHistoryMatches([]); + }); + }); + + describe('replace children', () => { + it('gets recorded during an update from text content', () => { + var node = document.createElement('div'); + ReactDOM.render(
Hi.
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render( +
, + node + ); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace children', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from html', () => { + var node = document.createElement('div'); + ReactDOM.render( +
, + node + ); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render( +
, + node + ); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'replace children', + payload: 'Bye.', + }]); + }); + + it('gets recorded during an update from children', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render( +
, + node + ); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 0}, + }, { + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 1}, + }, { + instanceID: inst._debugID, + type: 'replace children', + payload: 'Hi.', + }]); + }); + + it('gets ignored if new html is equal', () => { + var node = document.createElement('div'); + ReactDOM.render( +
, + node + ); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render( +
, + node + ); + + assertHistoryMatches([]); + }); + }); + + describe('insert child', () => { + it('gets reported when a child is inserted', () => { + var node = document.createElement('div'); + ReactDOM.render(
, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(

, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'insert child', + payload: {toIndex: 1, content: 'P'}, + }]); + }); + }); + + describe('move child', () => { + it('gets reported when a child is inserted', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(

, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'move child', + payload: {fromIndex: 0, toIndex: 1}, + }]); + }); + }); + + describe('remove child', () => { + it('gets reported when a child is removed', () => { + var node = document.createElement('div'); + ReactDOM.render(

, node); + var inst = ReactDOMComponentTree.getInstanceFromNode(node.firstChild); + + ReactNativeOperationHistoryDevtool._preventClearing = true; + ReactDOM.render(
, node); + + assertHistoryMatches([{ + instanceID: inst._debugID, + type: 'remove child', + payload: {fromIndex: 1}, + }]); + }); + }); +}); diff --git a/src/renderers/dom/ReactDOM.js b/src/renderers/dom/ReactDOM.js index 75644b1209..7e7fbe4639 100644 --- a/src/renderers/dom/ReactDOM.js +++ b/src/renderers/dom/ReactDOM.js @@ -16,7 +16,6 @@ var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDefaultInjection = require('ReactDefaultInjection'); var ReactMount = require('ReactMount'); -var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); var ReactUpdates = require('ReactUpdates'); var ReactVersion = require('ReactVersion'); @@ -28,11 +27,9 @@ var warning = require('warning'); ReactDefaultInjection.inject(); -var render = ReactPerf.measure('React', 'render', ReactMount.render); - var React = { findDOMNode: findDOMNode, - render: render, + render: ReactMount.render, unmountComponentAtNode: ReactMount.unmountComponentAtNode, version: ReactVersion, diff --git a/src/renderers/dom/client/ReactDOMIDOperations.js b/src/renderers/dom/client/ReactDOMIDOperations.js index cb03d3f06a..fd8c16797b 100644 --- a/src/renderers/dom/client/ReactDOMIDOperations.js +++ b/src/renderers/dom/client/ReactDOMIDOperations.js @@ -13,7 +13,6 @@ var DOMChildrenOperations = require('DOMChildrenOperations'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); -var ReactPerf = require('ReactPerf'); /** * Operations used to process updates to DOM nodes. @@ -32,8 +31,4 @@ var ReactDOMIDOperations = { }, }; -ReactPerf.measureMethods(ReactDOMIDOperations, 'ReactDOMIDOperations', { - dangerouslyProcessChildrenUpdates: 'dangerouslyProcessChildrenUpdates', -}); - module.exports = ReactDOMIDOperations; diff --git a/src/renderers/dom/client/ReactMount.js b/src/renderers/dom/client/ReactMount.js index 7f6f8cac6f..6ec60a21fa 100644 --- a/src/renderers/dom/client/ReactMount.js +++ b/src/renderers/dom/client/ReactMount.js @@ -22,7 +22,6 @@ var ReactElement = require('ReactElement'); var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactInstrumentation = require('ReactInstrumentation'); var ReactMarkupChecksum = require('ReactMarkupChecksum'); -var ReactPerf = require('ReactPerf'); var ReactReconciler = require('ReactReconciler'); var ReactUpdateQueue = require('ReactUpdateQueue'); var ReactUpdates = require('ReactUpdates'); @@ -308,6 +307,10 @@ var ReactMount = { shouldReuseMarkup, context ) { + if (__DEV__) { + ReactInstrumentation.debugTool.onBeginFlush(); + } + // Various parts of our code (such as ReactCompositeComponent's // _renderValidatedComponent) assume that calls to render aren't nested; // verify that that's the case. @@ -333,6 +336,12 @@ var ReactMount = { ReactBrowserEventEmitter.ensureScrollValueMonitoring(); var componentInstance = instantiateReactComponent(nextElement); + if (__DEV__) { + // Mute future events from the top level wrapper. + // It is an implementation detail that devtools should not know about. + componentInstance._debugID = 0; + } + // The initial render is synchronous but any updates that happen during // rendering, in componentWillMount or componentDidMount, will be batched // according to the current batching strategy. @@ -349,7 +358,11 @@ var ReactMount = { instancesByReactRootID[wrapperID] = componentInstance; if (__DEV__) { - ReactInstrumentation.debugTool.onMountRootComponent(componentInstance); + // The instance here is TopLevelWrapper so we report mount for its child. + ReactInstrumentation.debugTool.onMountRootComponent( + componentInstance._renderedComponent._debugID + ); + ReactInstrumentation.debugTool.onEndFlush(); } return componentInstance; @@ -497,6 +510,7 @@ var ReactMount = { /** * Renders a React component into the DOM in the supplied `container`. + * See https://facebook.github.io/react/docs/top-level-api.html#reactdom.render * * If the React component was previously rendered into `container`, this will * perform an update on it and only mutate the DOM as necessary to reflect the @@ -513,6 +527,7 @@ var ReactMount = { /** * Unmounts and destroys the React component rendered in the `container`. + * See https://facebook.github.io/react/docs/top-level-api.html#reactdom.unmountcomponentatnode * * @param {DOMElement} container DOM element containing a React component. * @return {boolean} True if a component was found in and unmounted from @@ -684,12 +699,18 @@ var ReactMount = { setInnerHTML(container, markup); ReactDOMComponentTree.precacheNode(instance, container.firstChild); } + + if (__DEV__) { + var nativeNode = ReactDOMComponentTree.getInstanceFromNode(container.firstChild); + if (nativeNode._debugID !== 0) { + ReactInstrumentation.debugTool.onNativeOperation( + nativeNode._debugID, + 'mount', + markup.toString() + ); + } + } }, }; -ReactPerf.measureMethods(ReactMount, 'ReactMount', { - _renderNewRootComponent: '_renderNewRootComponent', - _mountImageIntoNode: '_mountImageIntoNode', -}); - module.exports = ReactMount; diff --git a/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js b/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js index 5dbf91c2cd..9de6b2e8e2 100644 --- a/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js +++ b/src/renderers/dom/client/__tests__/ReactBrowserEventEmitter-test.js @@ -35,7 +35,7 @@ var recordIDAndReturnFalse = function(id, event) { recordID(id); return false; }; -var LISTENER = jest.genMockFn(); +var LISTENER = jest.fn(); var ON_CLICK_KEY = keyOf({onClick: null}); var ON_TOUCH_TAP_KEY = keyOf({onTouchTap: null}); var ON_CHANGE_KEY = keyOf({onChange: null}); @@ -284,7 +284,7 @@ describe('ReactBrowserEventEmitter', function() { */ it('should invoke handlers that were removed while bubbling', function() { - var handleParentClick = jest.genMockFn(); + var handleParentClick = jest.fn(); var handleChildClick = function(event) { EventPluginHub.deleteAllListeners(getInternal(PARENT)); }; @@ -303,7 +303,7 @@ describe('ReactBrowserEventEmitter', function() { }); it('should not invoke newly inserted handlers while bubbling', function() { - var handleParentClick = jest.genMockFn(); + var handleParentClick = jest.fn(); var handleChildClick = function(event) { EventPluginHub.putListener( getInternal(PARENT), diff --git a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js index bdeb485e24..431a21e0f2 100644 --- a/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js +++ b/src/renderers/dom/client/__tests__/ReactDOMIDOperations-test.js @@ -12,15 +12,18 @@ 'use strict'; describe('ReactDOMIDOperations', function() { + var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMIDOperations = require('ReactDOMIDOperations'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); it('should update innerHTML and preserve whitespace', function() { var stubNode = document.createElement('div'); - var html = '\n \t \n testContent \t \n \t'; + var stubInstance = {_debugID: 1}; + ReactDOMComponentTree.precacheNode(stubInstance, stubNode); + var html = '\n \t \n testContent \t \n \t'; ReactDOMIDOperations.dangerouslyProcessChildrenUpdates( - {_nativeNode: stubNode}, + stubInstance, [{ type: ReactMultiChildUpdateTypes.SET_MARKUP, content: html, diff --git a/src/renderers/dom/client/__tests__/ReactEventListener-test.js b/src/renderers/dom/client/__tests__/ReactEventListener-test.js index 49d4f195d5..049284f8f7 100644 --- a/src/renderers/dom/client/__tests__/ReactEventListener-test.js +++ b/src/renderers/dom/client/__tests__/ReactEventListener-test.js @@ -30,7 +30,7 @@ describe('ReactEventListener', function() { ReactEventListener = require('ReactEventListener'); ReactTestUtils = require('ReactTestUtils'); - handleTopLevel = jest.genMockFn(); + handleTopLevel = jest.fn(); ReactEventListener._handleTopLevel = handleTopLevel; }); diff --git a/src/renderers/dom/client/__tests__/ReactMount-test.js b/src/renderers/dom/client/__tests__/ReactMount-test.js index 06b1c16415..96ca4aef33 100644 --- a/src/renderers/dom/client/__tests__/ReactMount-test.js +++ b/src/renderers/dom/client/__tests__/ReactMount-test.js @@ -87,8 +87,8 @@ describe('ReactMount', function() { it('should unmount and remount if the key changes', function() { var container = document.createElement('container'); - var mockMount = jest.genMockFn(); - var mockUnmount = jest.genMockFn(); + var mockMount = jest.fn(); + var mockUnmount = jest.fn(); var Component = React.createClass({ componentDidMount: mockMount, diff --git a/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js b/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js index 0f032902d4..46e21b96ce 100644 --- a/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js +++ b/src/renderers/dom/client/eventPlugins/__tests__/SelectEventPlugin-test.js @@ -66,7 +66,7 @@ describe('SelectEventPlugin', function() { }, }); - var cb = jest.genMockFn(); + var cb = jest.fn(); var rendered = ReactTestUtils.renderIntoDocument( diff --git a/src/renderers/dom/client/findDOMNode.js b/src/renderers/dom/client/findDOMNode.js index 65c8070093..9c2a3b9ea5 100644 --- a/src/renderers/dom/client/findDOMNode.js +++ b/src/renderers/dom/client/findDOMNode.js @@ -22,6 +22,8 @@ var warning = require('warning'); /** * Returns the DOM node rendered by this element. * + * See https://facebook.github.io/react/docs/top-level-api.html#reactdom.finddomnode + * * @param {ReactComponent|DOMElement} componentOrElement * @return {?DOMElement} The root node of this element. */ diff --git a/src/renderers/dom/client/utils/DOMChildrenOperations.js b/src/renderers/dom/client/utils/DOMChildrenOperations.js index f123aef6ba..4f0da4b7cc 100644 --- a/src/renderers/dom/client/utils/DOMChildrenOperations.js +++ b/src/renderers/dom/client/utils/DOMChildrenOperations.js @@ -14,7 +14,8 @@ var DOMLazyTree = require('DOMLazyTree'); var Danger = require('Danger'); var ReactMultiChildUpdateTypes = require('ReactMultiChildUpdateTypes'); -var ReactPerf = require('ReactPerf'); +var ReactDOMComponentTree = require('ReactDOMComponentTree'); +var ReactInstrumentation = require('ReactInstrumentation'); var createMicrosoftUnsafeLocalFunction = require('createMicrosoftUnsafeLocalFunction'); var setInnerHTML = require('setInnerHTML'); @@ -120,6 +121,37 @@ function replaceDelimitedText(openingComment, closingComment, stringText) { removeDelimitedText(parentNode, openingComment, closingComment); } } + + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + ReactDOMComponentTree.getInstanceFromNode(openingComment)._debugID, + 'replace text', + stringText + ); + } +} + +var dangerouslyReplaceNodeWithMarkup = Danger.dangerouslyReplaceNodeWithMarkup; +if (__DEV__) { + dangerouslyReplaceNodeWithMarkup = function(oldChild, markup, prevInstance) { + Danger.dangerouslyReplaceNodeWithMarkup(oldChild, markup); + if (prevInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onNativeOperation( + prevInstance._debugID, + 'replace with', + markup.toString() + ); + } else { + var nextInstance = ReactDOMComponentTree.getInstanceFromNode(markup.node); + if (nextInstance._debugID !== 0) { + ReactInstrumentation.debugTool.onNativeOperation( + nextInstance._debugID, + 'mount', + markup.toString() + ); + } + } + }; } /** @@ -127,7 +159,7 @@ function replaceDelimitedText(openingComment, closingComment, stringText) { */ var DOMChildrenOperations = { - dangerouslyReplaceNodeWithMarkup: Danger.dangerouslyReplaceNodeWithMarkup, + dangerouslyReplaceNodeWithMarkup: dangerouslyReplaceNodeWithMarkup, replaceDelimitedText: replaceDelimitedText, @@ -139,6 +171,11 @@ var DOMChildrenOperations = { * @internal */ processUpdates: function(parentNode, updates) { + if (__DEV__) { + var parentNodeDebugID = + ReactDOMComponentTree.getInstanceFromNode(parentNode)._debugID; + } + for (var k = 0; k < updates.length; k++) { var update = updates[k]; switch (update.type) { @@ -148,6 +185,13 @@ var DOMChildrenOperations = { update.content, getNodeAfter(parentNode, update.afterNode) ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'insert child', + {toIndex: update.toIndex, content: update.content.toString()} + ); + } break; case ReactMultiChildUpdateTypes.MOVE_EXISTING: moveChild( @@ -155,21 +199,49 @@ var DOMChildrenOperations = { update.fromNode, getNodeAfter(parentNode, update.afterNode) ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'move child', + {fromIndex: update.fromIndex, toIndex: update.toIndex} + ); + } break; case ReactMultiChildUpdateTypes.SET_MARKUP: setInnerHTML( parentNode, update.content ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'replace children', + update.content.toString() + ); + } break; case ReactMultiChildUpdateTypes.TEXT_CONTENT: setTextContent( parentNode, update.content ); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'replace text', + update.content.toString() + ); + } break; case ReactMultiChildUpdateTypes.REMOVE_NODE: removeChild(parentNode, update.fromNode); + if (__DEV__) { + ReactInstrumentation.debugTool.onNativeOperation( + parentNodeDebugID, + 'remove child', + {fromIndex: update.fromIndex} + ); + } break; } } @@ -177,8 +249,4 @@ var DOMChildrenOperations = { }; -ReactPerf.measureMethods(DOMChildrenOperations, 'DOMChildrenOperations', { - replaceDelimitedText: 'replaceDelimitedText', -}); - module.exports = DOMChildrenOperations; diff --git a/src/renderers/dom/client/utils/DOMLazyTree.js b/src/renderers/dom/client/utils/DOMLazyTree.js index 249eb475c5..269f5c3714 100644 --- a/src/renderers/dom/client/utils/DOMLazyTree.js +++ b/src/renderers/dom/client/utils/DOMLazyTree.js @@ -11,9 +11,14 @@ 'use strict'; +var DOMNamespaces = require('DOMNamespaces'); + var createMicrosoftUnsafeLocalFunction = require('createMicrosoftUnsafeLocalFunction'); var setTextContent = require('setTextContent'); +var ELEMENT_NODE_TYPE = 1; +var DOCUMENT_FRAGMENT_NODE_TYPE = 11; + /** * In IE (8-11) and Edge, appending nodes with no children is dramatically * faster than appending a full subtree, so we essentially queue up the @@ -56,8 +61,15 @@ var insertTreeBefore = createMicrosoftUnsafeLocalFunction( // DocumentFragments aren't actually part of the DOM after insertion so // appending children won't update the DOM. We need to ensure the fragment // is properly populated first, breaking out of our lazy approach for just - // this level. - if (tree.node.nodeType === 11) { + // this level. Also, some plugins (like Flash Player) will read + // nodes immediately upon insertion into the DOM, so + // must also be populated prior to insertion into the DOM. + if (tree.node.nodeType === DOCUMENT_FRAGMENT_NODE_TYPE + || + tree.node.nodeType === ELEMENT_NODE_TYPE && + tree.node.nodeName.toLowerCase() === 'object' && + (tree.node.namespaceURI == null || + tree.node.namespaceURI === DOMNamespaces.html)) { insertTreeChildren(tree); parentNode.insertBefore(tree.node, referenceNode); } else { @@ -96,12 +108,17 @@ function queueText(tree, text) { } } +function toString() { + return this.node.nodeName; +} + function DOMLazyTree(node) { return { node: node, children: [], html: null, text: null, + toString, }; } diff --git a/src/renderers/dom/client/wrappers/ReactDOMInput.js b/src/renderers/dom/client/wrappers/ReactDOMInput.js index c1ad1a6330..d4ce97698e 100644 --- a/src/renderers/dom/client/wrappers/ReactDOMInput.js +++ b/src/renderers/dom/client/wrappers/ReactDOMInput.js @@ -92,6 +92,8 @@ var ReactDOMInput = { inst._currentElement._owner ); + var owner = inst._currentElement._owner; + if (props.valueLink !== undefined && !didWarnValueLink) { warning( false, @@ -113,11 +115,14 @@ var ReactDOMInput = { ) { warning( false, + '%s contains an input of type %s with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + - 'https://fb.me/react-controlled-components' + 'https://fb.me/react-controlled-components', + owner && owner.getName() || 'A component', + props.type ); didWarnCheckedDefaultChecked = true; } @@ -128,11 +133,14 @@ var ReactDOMInput = { ) { warning( false, + '%s contains an input of type %s with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + - 'https://fb.me/react-controlled-components' + 'https://fb.me/react-controlled-components', + owner && owner.getName() || 'A component', + props.type ); didWarnValueDefaultValue = true; } @@ -169,7 +177,7 @@ var ReactDOMInput = { ) { warning( false, - '%s is changing a uncontrolled input of type %s to be controlled. ' + + '%s is changing an uncontrolled input of type %s to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', diff --git a/src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js b/src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js index 0e4c77d556..a99546ecc2 100644 --- a/src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js +++ b/src/renderers/dom/client/wrappers/__tests__/DisabledInputUtil-test.js @@ -36,7 +36,7 @@ describe('DisabledInputUtils', function() { return element; } - var onClick = jest.genMockFn(); + var onClick = jest.fn(); elements.forEach(function(tagName) { diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js index 7a1f57dd0d..a1ab23d9e8 100644 --- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js +++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMInput-test.js @@ -236,7 +236,7 @@ describe('ReactDOMInput', function() { }); it('should support ReactLink', function() { - var link = new ReactLink('yolo', jest.genMockFn()); + var link = new ReactLink('yolo', jest.fn()); var instance = ; instance = ReactTestUtils.renderIntoDocument(instance); @@ -253,7 +253,7 @@ describe('ReactDOMInput', function() { }); it('should warn with value and no onChange handler', function() { - var link = new ReactLink('yolo', jest.genMockFn()); + var link = new ReactLink('yolo', jest.fn()); ReactTestUtils.renderIntoDocument(); expect(console.error.argsForCall.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( @@ -261,7 +261,7 @@ describe('ReactDOMInput', function() { ); ReactTestUtils.renderIntoDocument( - + ); expect(console.error.argsForCall.length).toBe(1); ReactTestUtils.renderIntoDocument(); @@ -293,7 +293,7 @@ describe('ReactDOMInput', function() { it('should throw if both value and valueLink are provided', function() { var node = document.createElement('div'); - var link = new ReactLink('yolo', jest.genMockFn()); + var link = new ReactLink('yolo', jest.fn()); var instance = ; expect(() => ReactDOM.render(instance, node)).not.toThrow(); @@ -313,7 +313,7 @@ describe('ReactDOMInput', function() { }); it('should support checkedLink', function() { - var link = new ReactLink(true, jest.genMockFn()); + var link = new ReactLink(true, jest.fn()); var instance = ; instance = ReactTestUtils.renderIntoDocument(instance); @@ -331,7 +331,7 @@ describe('ReactDOMInput', function() { it('should warn with checked and no onChange handler', function() { var node = document.createElement('div'); - var link = new ReactLink(true, jest.genMockFn()); + var link = new ReactLink(true, jest.fn()); ReactDOM.render(, node); expect(console.error.argsForCall.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( @@ -342,7 +342,7 @@ describe('ReactDOMInput', function() { ); expect(console.error.argsForCall.length).toBe(1); @@ -370,7 +370,7 @@ describe('ReactDOMInput', function() { it('should throw if both checked and checkedLink are provided', function() { var node = document.createElement('div'); - var link = new ReactLink(true, jest.genMockFn()); + var link = new ReactLink(true, jest.fn()); var instance = ; expect(() => ReactDOM.render(instance, node)).not.toThrow(); @@ -392,7 +392,7 @@ describe('ReactDOMInput', function() { it('should throw if both checkedLink and valueLink are provided', function() { var node = document.createElement('div'); - var link = new ReactLink(true, jest.genMockFn()); + var link = new ReactLink(true, jest.fn()); var instance = ; expect(() => ReactDOM.render(instance, node)).not.toThrow(); @@ -422,6 +422,7 @@ describe('ReactDOMInput', function() { ); expect(console.error.argsForCall[0][0]).toContain( + 'A component contains an input of type radio with both checked and defaultChecked props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the checked prop, or the defaultChecked prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + @@ -440,6 +441,7 @@ describe('ReactDOMInput', function() { ); expect(console.error.argsForCall[0][0]).toContain( + 'A component contains an input of type text with both value and defaultValue props. ' + 'Input elements must be either controlled or uncontrolled ' + '(specify either the value prop, or the defaultValue prop, but not ' + 'both). Decide between using a controlled or uncontrolled input ' + @@ -488,7 +490,7 @@ describe('ReactDOMInput', function() { ReactDOM.render(, container); expect(console.error.argsForCall.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( - 'A component is changing a uncontrolled input of type text to be controlled. ' + + 'A component is changing an uncontrolled input of type text to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components' @@ -530,7 +532,7 @@ describe('ReactDOMInput', function() { ReactDOM.render(, container); expect(console.error.argsForCall.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( - 'A component is changing a uncontrolled input of type checkbox to be controlled. ' + + 'A component is changing an uncontrolled input of type checkbox to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components' @@ -572,7 +574,7 @@ describe('ReactDOMInput', function() { ReactDOM.render(, container); expect(console.error.argsForCall.length).toBe(1); expect(console.error.argsForCall[0][0]).toContain( - 'A component is changing a uncontrolled input of type radio to be controlled. ' + + 'A component is changing an uncontrolled input of type radio to be controlled. ' + 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components' diff --git a/src/renderers/dom/client/wrappers/__tests__/ReactDOMSelect-test.js b/src/renderers/dom/client/wrappers/__tests__/ReactDOMSelect-test.js index 0a3f530283..c4276c143f 100644 --- a/src/renderers/dom/client/wrappers/__tests__/ReactDOMSelect-test.js +++ b/src/renderers/dom/client/wrappers/__tests__/ReactDOMSelect-test.js @@ -348,7 +348,7 @@ describe('ReactDOMSelect', function() { }); it('should support ReactLink', function() { - var link = new ReactLink('giraffe', jest.genMockFn()); + var link = new ReactLink('giraffe', jest.fn()); var stub =