├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc.js ├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── babel.config.js ├── index.js ├── jest.config.js ├── npm ├── esm │ └── index.js └── index.js ├── package.json ├── rollup.config.js ├── scripts ├── babel │ └── transform-object-assign-require.js ├── copyFiles.js └── jest │ ├── matchers │ └── toWarnDev.js │ ├── setupTests.js │ └── shouldIgnoreConsoleError.js ├── src ├── ReactShallowRenderer.js ├── __tests__ │ ├── ReactShallowRenderer-test.js │ ├── ReactShallowRendererHooks-test.js │ └── ReactShallowRendererMemo-test.js └── shared │ ├── ReactLazyComponent.js │ ├── ReactSharedInternals.js │ ├── ReactSymbols.js │ ├── checkPropTypes.js │ ├── consoleWithStackDev.js │ ├── describeComponentFrame.js │ ├── getComponentName.js │ ├── objectIs.js │ └── shallowEqual.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # JavaScript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10 11 | 12 | steps: 13 | - checkout 14 | 15 | # Download and cache dependencies 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "yarn.lock" }} 19 | # fallback to using the latest cache if no exact match is found 20 | - v1-dependencies- 21 | 22 | - run: yarn --frozen-lockfile 23 | 24 | - save_cache: 25 | key: v1-dependencies-{{ checksum "yarn.lock" }} 26 | paths: 27 | - ~/.cache/yarn 28 | 29 | - run: yarn lint 30 | 31 | - run: yarn test 32 | 33 | - run: yarn build 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const restrictedGlobals = require('confusing-browser-globals'); 4 | 5 | const OFF = 'off'; 6 | const ERROR = 'error'; 7 | 8 | // Files that are transformed and can use ES6/JSX. 9 | const esNextPaths = [ 10 | // Internal forwarding modules 11 | './index.js', 12 | // Source files 13 | 'src/**/*.js', 14 | // Jest 15 | 'scripts/jest/setupTests.js', 16 | ]; 17 | 18 | // Files that we distribute on npm that should be ES5-only. 19 | const es5Paths = ['npm/**/*.js']; 20 | 21 | module.exports = { 22 | env: { 23 | browser: true, 24 | es6: true, 25 | node: true, 26 | }, 27 | extends: [ 28 | 'eslint:recommended', 29 | 'plugin:react/recommended', 30 | 'plugin:prettier/recommended', 31 | 'prettier/react', 32 | ], 33 | globals: { 34 | Atomics: 'readonly', 35 | SharedArrayBuffer: 'readonly', 36 | }, 37 | parser: 'babel-eslint', 38 | parserOptions: { 39 | ecmaVersion: 2018, 40 | sourceType: 'script', 41 | }, 42 | plugins: ['react'], 43 | settings: { 44 | react: { 45 | version: 'detect', 46 | }, 47 | }, 48 | rules: { 49 | 'no-console': ERROR, 50 | 'no-empty': OFF, 51 | 'no-restricted-globals': [ERROR, ...restrictedGlobals], 52 | 'no-unsafe-finally': OFF, 53 | 'no-unused-vars': [ERROR, {args: 'none'}], 54 | 'no-useless-escape': OFF, 55 | 56 | // We apply these settings to files that should run on Node. 57 | // They can't use JSX or ES6 modules, and must be in strict mode. 58 | // They can, however, use other ES6 features. 59 | // (Note these rules are overridden later for source files.) 60 | 'no-var': ERROR, 61 | strict: ERROR, 62 | }, 63 | overrides: [ 64 | { 65 | // We apply these settings to files that we ship through npm. 66 | // They must be ES5. 67 | files: es5Paths, 68 | parser: 'espree', 69 | parserOptions: { 70 | ecmaVersion: 5, 71 | sourceType: 'script', 72 | }, 73 | rules: { 74 | 'no-var': OFF, 75 | strict: ERROR, 76 | }, 77 | overrides: [ 78 | { 79 | // These files are ES5 but with ESM support. 80 | files: ['npm/esm/**/*.js'], 81 | parserOptions: { 82 | // Although this is supposed to be 5, ESLint doesn't allow sourceType 'module' when ecmaVersion < 2015. 83 | // See https://github.com/eslint/eslint/issues/9687#issuecomment-508448526 84 | ecmaVersion: 2015, 85 | sourceType: 'module', 86 | }, 87 | }, 88 | ], 89 | }, 90 | { 91 | // We apply these settings to the source files that get compiled. 92 | // They can use all features including JSX (but shouldn't use `var`). 93 | files: esNextPaths, 94 | parserOptions: { 95 | ecmaVersion: 2018, 96 | sourceType: 'module', 97 | }, 98 | rules: { 99 | 'no-var': ERROR, 100 | strict: OFF, 101 | }, 102 | }, 103 | { 104 | // Rollup understands ESM 105 | files: ['rollup.config.js'], 106 | parserOptions: { 107 | ecmaVersion: 2018, 108 | sourceType: 'module', 109 | }, 110 | }, 111 | { 112 | files: ['**/__tests__/**/*.js', 'scripts/jest/setupTests.js'], 113 | env: { 114 | 'jest/globals': true, 115 | }, 116 | plugins: ['jest'], 117 | rules: { 118 | // https://github.com/jest-community/eslint-plugin-jest 119 | 'jest/no-focused-tests': ERROR, 120 | 'jest/valid-expect': ERROR, 121 | 'jest/valid-expect-in-promise': ERROR, 122 | 123 | // React & JSX 124 | // This isn't useful in our test code 125 | 'react/display-name': OFF, 126 | 'react/jsx-key': OFF, 127 | 'react/no-deprecated': OFF, 128 | 'react/no-string-refs': OFF, 129 | 'react/prop-types': OFF, 130 | }, 131 | }, 132 | { 133 | files: ['scripts/**/*.js', 'npm/**/*.js'], 134 | rules: { 135 | 'no-console': OFF, 136 | }, 137 | }, 138 | ], 139 | }; 140 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | .eslintcache 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "jsxBracketSameLine": true, 5 | "trailingComma": "all", 6 | "printWidth": 80 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Facebook, Inc. and its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `react-shallow-renderer` 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/enzymejs/react-shallow-renderer/blob/master/LICENSE) 4 | [![npm version](https://img.shields.io/npm/v/react-shallow-renderer)](https://www.npmjs.com/package/react-shallow-renderer) 5 | [![CircleCI](https://img.shields.io/circleci/build/github/enzymejs/react-shallow-renderer)](https://circleci.com/gh/enzymejs/react-shallow-renderer/tree/master) 6 | 7 | When writing unit tests for React, shallow rendering can be helpful. Shallow rendering lets you render a component "one level deep" and assert facts about what its render method returns, without worrying about the behavior of child components, which are not instantiated or rendered. This does not require a DOM. 8 | 9 | ## Installation 10 | 11 | ```sh 12 | # npm 13 | npm install react-shallow-renderer --save-dev 14 | 15 | # Yarn 16 | yarn add react-shallow-renderer --dev 17 | ``` 18 | 19 | ## Usage 20 | 21 | For example, if you have the following component: 22 | 23 | ```jsx 24 | function MyComponent() { 25 | return ( 26 |
27 | Title 28 | 29 |
30 | ); 31 | } 32 | ``` 33 | 34 | Then you can assert: 35 | 36 | ```jsx 37 | import ShallowRenderer from 'react-shallow-renderer'; 38 | // in your test: 39 | const renderer = new ShallowRenderer(); 40 | renderer.render(); 41 | const result = renderer.getRenderOutput(); 42 | expect(result.type).toBe('div'); 43 | expect(result.props.children).toEqual([ 44 | Title, 45 | , 46 | ]); 47 | ``` 48 | 49 | Shallow testing currently has some limitations, namely not supporting refs. 50 | 51 | > Note: 52 | > 53 | > We also recommend checking out Enzyme's [Shallow Rendering API](https://airbnb.io/enzyme/docs/api/shallow.html). It provides a nicer higher-level API over the same functionality. 54 | 55 | ## Reference 56 | 57 | ### `shallowRenderer.render()` 58 | 59 | You can think of the shallowRenderer as a "place" to render the component you're testing, and from which you can extract the component's output. 60 | 61 | `shallowRenderer.render()` is similar to [`ReactDOM.render()`](https://reactjs.org/docs/react-dom.html#render) but it doesn't require DOM and only renders a single level deep. This means you can test components isolated from how their children are implemented. 62 | 63 | ### `shallowRenderer.getRenderOutput()` 64 | 65 | After `shallowRenderer.render()` has been called, you can use `shallowRenderer.getRenderOutput()` to get the shallowly rendered output. 66 | 67 | You can then begin to assert facts about the output. 68 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | presets: [ 5 | [ 6 | '@babel/preset-env', 7 | { 8 | modules: process.env.NODE_ENV === 'test' ? 'auto' : false, 9 | // Exclude transforms that make all code slower. 10 | exclude: ['transform-typeof-symbol'], 11 | targets: process.env.NODE_ENV === 'test' ? {node: 'current'} : {ie: 11}, 12 | }, 13 | ], 14 | '@babel/preset-react', 15 | ], 16 | plugins: [ 17 | ['@babel/plugin-proposal-class-properties', {loose: true}], 18 | ['@babel/plugin-transform-classes', {loose: true}], 19 | ['@babel/plugin-transform-template-literals', {loose: true}], 20 | // The following plugin is configured to use `Object.assign` directly. 21 | // Note that we ponyfill `Object.assign` below. 22 | // { ...todo, complete: true } 23 | [ 24 | '@babel/plugin-proposal-object-rest-spread', 25 | {loose: true, useBuiltIns: true}, 26 | ], 27 | // Use 'object-assign' ponyfill. 28 | require.resolve('./scripts/babel/transform-object-assign-require'), 29 | // Keep stacks detailed in tests. 30 | process.env.NODE_ENV === 'test' && 31 | '@babel/plugin-transform-react-jsx-source', 32 | ].filter(Boolean), 33 | }; 34 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @flow 8 | */ 9 | 10 | export {default} from './src/ReactShallowRenderer'; 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | modulePathIgnorePatterns: ['/build/'], 5 | setupFilesAfterEnv: ['./scripts/jest/setupTests.js'], 6 | }; 7 | -------------------------------------------------------------------------------- /npm/esm/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './react-shallow-renderer'; 2 | -------------------------------------------------------------------------------- /npm/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./cjs/react-shallow-renderer.js'); 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-shallow-renderer", 3 | "version": "16.15.0", 4 | "description": "React package for shallow rendering.", 5 | "main": "index.js", 6 | "repository": "https://github.com/NMinhNguyen/react-shallow-renderer.git", 7 | "keywords": [ 8 | "react", 9 | "react-native", 10 | "react-testing" 11 | ], 12 | "license": "MIT", 13 | "bugs": { 14 | "url": "https://github.com/NMinhNguyen/react-shallow-renderer/issues" 15 | }, 16 | "homepage": "https://reactjs.org/", 17 | "dependencies": { 18 | "object-assign": "^4.1.1", 19 | "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/cli": "^7.8.4", 23 | "@babel/core": "^7.8.6", 24 | "@babel/plugin-proposal-class-properties": "^7.8.3", 25 | "@babel/plugin-proposal-object-rest-spread": "^7.8.3", 26 | "@babel/plugin-transform-classes": "^7.8.6", 27 | "@babel/plugin-transform-react-jsx-source": "^7.8.3", 28 | "@babel/plugin-transform-template-literals": "^7.8.3", 29 | "@babel/preset-env": "^7.8.6", 30 | "@babel/preset-react": "^7.8.3", 31 | "@rollup/plugin-commonjs": "^11.0.1", 32 | "@rollup/plugin-node-resolve": "^7.0.0", 33 | "@rollup/plugin-replace": "^2.3.0", 34 | "babel-eslint": "^10.1.0", 35 | "babel-jest": "^25.1.0", 36 | "confusing-browser-globals": "^1.0.9", 37 | "eslint": "^6.8.0", 38 | "eslint-config-prettier": "^6.10.0", 39 | "eslint-plugin-jest": "^23.8.0", 40 | "eslint-plugin-prettier": "^3.1.2", 41 | "eslint-plugin-react": "^7.18.3", 42 | "fs-extra": "^8.1.0", 43 | "husky": "^4.2.3", 44 | "jest": "^25.1.0", 45 | "jest-diff": "^25.1.0", 46 | "lint-staged": "^10.0.8", 47 | "prettier": "1.19.1", 48 | "react": "^18.0.0", 49 | "rimraf": "^3.0.1", 50 | "rollup": "^1.30.1", 51 | "rollup-plugin-babel": "^4.3.3", 52 | "rollup-plugin-strip-banner": "^1.0.0" 53 | }, 54 | "peerDependencies": { 55 | "react": "^16.0.0 || ^17.0.0 || ^18.0.0" 56 | }, 57 | "files": [ 58 | "LICENSE", 59 | "README.md", 60 | "index.js", 61 | "cjs/", 62 | "esm/", 63 | "umd/" 64 | ], 65 | "scripts": { 66 | "prebuild": "rimraf build", 67 | "build": "rollup --config", 68 | "postbuild": "node ./scripts/copyFiles.js", 69 | "lint": "eslint --ignore-path .gitignore .", 70 | "test": "jest", 71 | "test:debug": "node --inspect-brk node_modules/jest/bin/jest.js --runInBand --no-cache" 72 | }, 73 | "husky": { 74 | "hooks": { 75 | "pre-commit": "lint-staged" 76 | } 77 | }, 78 | "lint-staged": { 79 | "*.js": "eslint --cache --fix" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import babel from 'rollup-plugin-babel'; 3 | import replace from '@rollup/plugin-replace'; 4 | import commonjs from '@rollup/plugin-commonjs'; 5 | import stripBanner from 'rollup-plugin-strip-banner'; 6 | 7 | import pkgJson from './package.json'; 8 | 9 | const reactShallowRendererVersion = pkgJson.version; 10 | 11 | const knownGlobals = { 12 | react: 'React', 13 | }; 14 | 15 | const license = ` * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree.`; 19 | 20 | function wrapBundle(source, filename) { 21 | return `/** @license ReactShallowRenderer v${reactShallowRendererVersion} 22 | * ${filename} 23 | * 24 | ${license} 25 | */ 26 | 27 | ${source}`; 28 | } 29 | 30 | function createConfig(bundleType) { 31 | const filename = 'react-shallow-renderer.js'; 32 | const outputPath = `build/${bundleType}/${filename}`; 33 | 34 | const shouldBundleDependencies = bundleType === 'umd'; 35 | const isUMDBundle = bundleType === 'umd'; 36 | 37 | let externals = Object.keys(knownGlobals); 38 | if (!shouldBundleDependencies) { 39 | externals = [...externals, ...Object.keys(pkgJson.dependencies)]; 40 | } 41 | 42 | return { 43 | input: 'src/ReactShallowRenderer.js', 44 | external(id) { 45 | const containsThisModule = pkg => id === pkg || id.startsWith(pkg + '/'); 46 | const isProvidedByDependency = externals.some(containsThisModule); 47 | if (!shouldBundleDependencies && isProvidedByDependency) { 48 | return true; 49 | } 50 | return !!knownGlobals[id]; 51 | }, 52 | plugins: [ 53 | resolve(), 54 | stripBanner({ 55 | exclude: /node_modules/, 56 | }), 57 | babel({exclude: 'node_modules/**'}), 58 | isUMDBundle && 59 | replace({ 60 | 'process.env.NODE_ENV': "'development'", 61 | }), 62 | commonjs({ 63 | include: /node_modules/, 64 | namedExports: {'react-is': ['isForwardRef', 'isMemo', 'ForwardRef']}, 65 | }), 66 | // License header. 67 | { 68 | renderChunk(source) { 69 | return wrapBundle(source, filename); 70 | }, 71 | }, 72 | ].filter(Boolean), 73 | output: { 74 | file: outputPath, 75 | format: bundleType, 76 | globals: knownGlobals, 77 | interop: false, 78 | name: 'ReactShallowRenderer', 79 | }, 80 | }; 81 | } 82 | 83 | export default [createConfig('cjs'), createConfig('esm'), createConfig('umd')]; 84 | -------------------------------------------------------------------------------- /scripts/babel/transform-object-assign-require.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | 'use strict'; 9 | 10 | const helperModuleImports = require('@babel/helper-module-imports'); 11 | 12 | module.exports = function autoImporter(babel) { 13 | function getAssignIdent(path, file, state) { 14 | if (state.id) { 15 | return babel.types.cloneNode(state.id); 16 | } 17 | state.id = helperModuleImports.addDefault(path, 'object-assign', { 18 | nameHint: 'assign', 19 | }); 20 | return state.id; 21 | } 22 | 23 | return { 24 | pre: function() { 25 | // map from module to generated identifier 26 | this.id = null; 27 | }, 28 | 29 | visitor: { 30 | CallExpression: function(path, file) { 31 | if (path.get('callee').matchesPattern('Object.assign')) { 32 | // generate identifier and require if it hasn't been already 33 | const id = getAssignIdent(path, file, this); 34 | path.node.callee = id; 35 | } 36 | }, 37 | 38 | MemberExpression: function(path, file) { 39 | if (path.matchesPattern('Object.assign')) { 40 | const id = getAssignIdent(path, file, this); 41 | path.replaceWith(id); 42 | } 43 | }, 44 | }, 45 | }; 46 | }; 47 | -------------------------------------------------------------------------------- /scripts/copyFiles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs-extra'); 4 | 5 | // Makes the script crash on unhandled rejections instead of silently 6 | // ignoring them. In the future, promise rejections that are not handled will 7 | // terminate the Node.js process with a non-zero exit code. 8 | // See https://github.com/facebook/create-react-app/blob/4582491/packages/react-scripts/scripts/build.js#L15-L20 9 | process.on('unhandledRejection', err => { 10 | throw err; 11 | }); 12 | 13 | async function copyFiles() { 14 | await Promise.all([ 15 | fs.copy('LICENSE', 'build/LICENSE'), 16 | fs.copy('package.json', 'build/package.json'), 17 | fs.copy('README.md', 'build/README.md'), 18 | fs.copy('npm', 'build'), 19 | ]); 20 | } 21 | 22 | copyFiles(); 23 | -------------------------------------------------------------------------------- /scripts/jest/matchers/toWarnDev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const jestDiff = require('jest-diff').default; 4 | const util = require('util'); 5 | const shouldIgnoreConsoleError = require('../shouldIgnoreConsoleError'); 6 | 7 | function normalizeCodeLocInfo(str) { 8 | return str && str.replace(/at .+?:\d+/g, 'at **'); 9 | } 10 | 11 | const createMatcherFor = (consoleMethod, matcherName) => 12 | function matcher(callback, expectedMessages, options = {}) { 13 | if (process.env.NODE_ENV !== 'production') { 14 | // Warn about incorrect usage of matcher. 15 | if (typeof expectedMessages === 'string') { 16 | expectedMessages = [expectedMessages]; 17 | } else if (!Array.isArray(expectedMessages)) { 18 | throw Error( 19 | `${matcherName}() requires a parameter of type string or an array of strings ` + 20 | `but was given ${typeof expectedMessages}.`, 21 | ); 22 | } 23 | if ( 24 | options != null && 25 | (typeof options !== 'object' || Array.isArray(options)) 26 | ) { 27 | throw new Error( 28 | `${matcherName}() second argument, when present, should be an object. ` + 29 | 'Did you forget to wrap the messages into an array?', 30 | ); 31 | } 32 | if (arguments.length > 3) { 33 | // `matcher` comes from Jest, so it's more than 2 in practice 34 | throw new Error( 35 | `${matcherName}() received more than two arguments. ` + 36 | 'Did you forget to wrap the messages into an array?', 37 | ); 38 | } 39 | 40 | const withoutStack = options.withoutStack; 41 | const logAllErrors = options.logAllErrors; 42 | const warningsWithoutComponentStack = []; 43 | const warningsWithComponentStack = []; 44 | const unexpectedWarnings = []; 45 | 46 | let lastWarningWithMismatchingFormat = null; 47 | let lastWarningWithExtraComponentStack = null; 48 | 49 | // Catch errors thrown by the callback, 50 | // But only rethrow them if all test expectations have been satisfied. 51 | // Otherwise an Error in the callback can mask a failed expectation, 52 | // and result in a test that passes when it shouldn't. 53 | let caughtError; 54 | 55 | const isLikelyAComponentStack = message => 56 | typeof message === 'string' && message.includes('\n in '); 57 | 58 | const consoleSpy = (format, ...args) => { 59 | // Ignore uncaught errors reported by jsdom 60 | // and React addendums because they're too noisy. 61 | if ( 62 | !logAllErrors && 63 | consoleMethod === 'error' && 64 | shouldIgnoreConsoleError(format, args) 65 | ) { 66 | return; 67 | } 68 | 69 | const message = util.format(format, ...args); 70 | const normalizedMessage = normalizeCodeLocInfo(message); 71 | 72 | // Remember if the number of %s interpolations 73 | // doesn't match the number of arguments. 74 | // We'll fail the test if it happens. 75 | let argIndex = 0; 76 | format.replace(/%s/g, () => argIndex++); 77 | if (argIndex !== args.length) { 78 | lastWarningWithMismatchingFormat = { 79 | format, 80 | args, 81 | expectedArgCount: argIndex, 82 | }; 83 | } 84 | 85 | // Protect against accidentally passing a component stack 86 | // to warning() which already injects the component stack. 87 | if ( 88 | args.length >= 2 && 89 | isLikelyAComponentStack(args[args.length - 1]) && 90 | isLikelyAComponentStack(args[args.length - 2]) 91 | ) { 92 | lastWarningWithExtraComponentStack = { 93 | format, 94 | }; 95 | } 96 | 97 | for (let index = 0; index < expectedMessages.length; index++) { 98 | const expectedMessage = expectedMessages[index]; 99 | if ( 100 | normalizedMessage === expectedMessage || 101 | normalizedMessage.includes(expectedMessage) 102 | ) { 103 | if (isLikelyAComponentStack(normalizedMessage)) { 104 | warningsWithComponentStack.push(normalizedMessage); 105 | } else { 106 | warningsWithoutComponentStack.push(normalizedMessage); 107 | } 108 | expectedMessages.splice(index, 1); 109 | return; 110 | } 111 | } 112 | 113 | let errorMessage; 114 | if (expectedMessages.length === 0) { 115 | errorMessage = 116 | 'Unexpected warning recorded: ' + 117 | this.utils.printReceived(normalizedMessage); 118 | } else if (expectedMessages.length === 1) { 119 | errorMessage = 120 | 'Unexpected warning recorded: ' + 121 | jestDiff(expectedMessages[0], normalizedMessage); 122 | } else { 123 | errorMessage = 124 | 'Unexpected warning recorded: ' + 125 | jestDiff(expectedMessages, [normalizedMessage]); 126 | } 127 | 128 | // Record the call stack for unexpected warnings. 129 | // We don't throw an Error here though, 130 | // Because it might be suppressed by ReactFiberScheduler. 131 | unexpectedWarnings.push(new Error(errorMessage)); 132 | }; 133 | 134 | // TODO Decide whether we need to support nested toWarn* expectations. 135 | // If we don't need it, add a check here to see if this is already our spy, 136 | // And throw an error. 137 | const originalMethod = console[consoleMethod]; 138 | 139 | // Avoid using Jest's built-in spy since it can't be removed. 140 | console[consoleMethod] = consoleSpy; 141 | 142 | try { 143 | callback(); 144 | } catch (error) { 145 | caughtError = error; 146 | } finally { 147 | // Restore the unspied method so that unexpected errors fail tests. 148 | console[consoleMethod] = originalMethod; 149 | 150 | // Any unexpected Errors thrown by the callback should fail the test. 151 | // This should take precedence since unexpected errors could block warnings. 152 | if (caughtError) { 153 | throw caughtError; 154 | } 155 | 156 | // Any unexpected warnings should be treated as a failure. 157 | if (unexpectedWarnings.length > 0) { 158 | return { 159 | message: () => unexpectedWarnings[0].stack, 160 | pass: false, 161 | }; 162 | } 163 | 164 | // Any remaining messages indicate a failed expectations. 165 | if (expectedMessages.length > 0) { 166 | return { 167 | message: () => 168 | `Expected warning was not recorded:\n ${this.utils.printReceived( 169 | expectedMessages[0], 170 | )}`, 171 | pass: false, 172 | }; 173 | } 174 | 175 | if (typeof withoutStack === 'number') { 176 | // We're expecting a particular number of warnings without stacks. 177 | if (withoutStack !== warningsWithoutComponentStack.length) { 178 | return { 179 | message: () => 180 | `Expected ${withoutStack} warnings without a component stack but received ${warningsWithoutComponentStack.length}:\n` + 181 | warningsWithoutComponentStack.map(warning => 182 | this.utils.printReceived(warning), 183 | ), 184 | pass: false, 185 | }; 186 | } 187 | } else if (withoutStack === true) { 188 | // We're expecting that all warnings won't have the stack. 189 | // If some warnings have it, it's an error. 190 | if (warningsWithComponentStack.length > 0) { 191 | return { 192 | message: () => 193 | `Received warning unexpectedly includes a component stack:\n ${this.utils.printReceived( 194 | warningsWithComponentStack[0], 195 | )}\nIf this warning intentionally includes the component stack, remove ` + 196 | `{withoutStack: true} from the ${matcherName}() call. If you have a mix of ` + 197 | `warnings with and without stack in one ${matcherName}() call, pass ` + 198 | `{withoutStack: N} where N is the number of warnings without stacks.`, 199 | pass: false, 200 | }; 201 | } 202 | } else if (withoutStack === false || withoutStack === undefined) { 203 | // We're expecting that all warnings *do* have the stack (default). 204 | // If some warnings don't have it, it's an error. 205 | if (warningsWithoutComponentStack.length > 0) { 206 | return { 207 | message: () => 208 | `Received warning unexpectedly does not include a component stack:\n ${this.utils.printReceived( 209 | warningsWithoutComponentStack[0], 210 | )}\nIf this warning intentionally omits the component stack, add ` + 211 | `{withoutStack: true} to the ${matcherName} call.`, 212 | pass: false, 213 | }; 214 | } 215 | } else { 216 | throw Error( 217 | `The second argument for ${matcherName}(), when specified, must be an object. It may have a ` + 218 | `property called "withoutStack" whose value may be undefined, boolean, or a number. ` + 219 | `Instead received ${typeof withoutStack}.`, 220 | ); 221 | } 222 | 223 | if (lastWarningWithMismatchingFormat !== null) { 224 | return { 225 | message: () => 226 | `Received ${ 227 | lastWarningWithMismatchingFormat.args.length 228 | } arguments for a message with ${ 229 | lastWarningWithMismatchingFormat.expectedArgCount 230 | } placeholders:\n ${this.utils.printReceived( 231 | lastWarningWithMismatchingFormat.format, 232 | )}`, 233 | pass: false, 234 | }; 235 | } 236 | 237 | if (lastWarningWithExtraComponentStack !== null) { 238 | return { 239 | message: () => 240 | `Received more than one component stack for a warning:\n ${this.utils.printReceived( 241 | lastWarningWithExtraComponentStack.format, 242 | )}\nDid you accidentally pass a stack to warning() as the last argument? ` + 243 | `Don't forget warning() already injects the component stack automatically.`, 244 | pass: false, 245 | }; 246 | } 247 | 248 | return {pass: true}; 249 | } 250 | } else { 251 | // Any uncaught errors or warnings should fail tests in production mode. 252 | callback(); 253 | 254 | return {pass: true}; 255 | } 256 | }; 257 | 258 | module.exports = { 259 | toWarnDev: createMatcherFor('warn', 'toWarnDev'), 260 | toErrorDev: createMatcherFor('error', 'toErrorDev'), 261 | }; 262 | -------------------------------------------------------------------------------- /scripts/jest/setupTests.js: -------------------------------------------------------------------------------- 1 | expect.extend(require('./matchers/toWarnDev')); 2 | -------------------------------------------------------------------------------- /scripts/jest/shouldIgnoreConsoleError.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function shouldIgnoreConsoleError(format, args) { 4 | if (process.env.NODE_ENV !== 'production') { 5 | if (typeof format === 'string') { 6 | if (format.indexOf('Error: Uncaught [') === 0) { 7 | // This looks like an uncaught error from invokeGuardedCallback() wrapper 8 | // in development that is reported by jsdom. Ignore because it's noisy. 9 | return true; 10 | } 11 | if (format.indexOf('The above error occurred') === 0) { 12 | // This looks like an error addendum from ReactFiberErrorLogger. 13 | // Ignore it too. 14 | return true; 15 | } 16 | } 17 | } else { 18 | if ( 19 | format != null && 20 | typeof format.message === 'string' && 21 | typeof format.stack === 'string' && 22 | args.length === 0 23 | ) { 24 | // In production, ReactFiberErrorLogger logs error objects directly. 25 | // They are noisy too so we'll try to ignore them. 26 | return true; 27 | } 28 | if ( 29 | format.indexOf( 30 | 'act(...) is not supported in production builds of React', 31 | ) === 0 32 | ) { 33 | // We don't yet support act() for prod builds, and warn for it. 34 | // But we'd like to use act() ourselves for prod builds. 35 | // Let's ignore the warning and #yolo. 36 | return true; 37 | } 38 | } 39 | // Looks legit 40 | return false; 41 | }; 42 | -------------------------------------------------------------------------------- /src/ReactShallowRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | import React from 'react'; 11 | import {isForwardRef, isMemo, ForwardRef} from 'react-is'; 12 | import describeComponentFrame from './shared/describeComponentFrame'; 13 | import getComponentName from './shared/getComponentName'; 14 | import shallowEqual from './shared/shallowEqual'; 15 | import checkPropTypes from './shared/checkPropTypes'; 16 | import ReactSharedInternals from './shared/ReactSharedInternals'; 17 | import {error} from './shared/consoleWithStackDev'; 18 | import is from './shared/objectIs'; 19 | 20 | const {ReactCurrentDispatcher, ReactDebugCurrentFrame} = ReactSharedInternals; 21 | 22 | const RE_RENDER_LIMIT = 25; 23 | 24 | const emptyObject = {}; 25 | if (process.env.NODE_ENV !== 'production') { 26 | Object.freeze(emptyObject); 27 | } 28 | 29 | // In DEV, this is the name of the currently executing primitive hook 30 | let currentHookNameInDev; 31 | 32 | function areHookInputsEqual(nextDeps, prevDeps) { 33 | if (prevDeps === null) { 34 | if (process.env.NODE_ENV !== 'production') { 35 | error( 36 | '%s received a final argument during this render, but not during ' + 37 | 'the previous render. Even though the final argument is optional, ' + 38 | 'its type cannot change between renders.', 39 | currentHookNameInDev, 40 | ); 41 | } 42 | return false; 43 | } 44 | 45 | if (process.env.NODE_ENV !== 'production') { 46 | // Don't bother comparing lengths in prod because these arrays should be 47 | // passed inline. 48 | if (nextDeps.length !== prevDeps.length) { 49 | error( 50 | 'The final argument passed to %s changed size between renders. The ' + 51 | 'order and size of this array must remain constant.\n\n' + 52 | 'Previous: %s\n' + 53 | 'Incoming: %s', 54 | currentHookNameInDev, 55 | `[${nextDeps.join(', ')}]`, 56 | `[${prevDeps.join(', ')}]`, 57 | ); 58 | } 59 | } 60 | for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) { 61 | if (is(nextDeps[i], prevDeps[i])) { 62 | continue; 63 | } 64 | return false; 65 | } 66 | return true; 67 | } 68 | 69 | class Updater { 70 | constructor(renderer) { 71 | this._renderer = renderer; 72 | this._callbacks = []; 73 | } 74 | 75 | _enqueueCallback(callback, publicInstance) { 76 | if (typeof callback === 'function' && publicInstance) { 77 | this._callbacks.push({ 78 | callback, 79 | publicInstance, 80 | }); 81 | } 82 | } 83 | 84 | _invokeCallbacks() { 85 | const callbacks = this._callbacks; 86 | this._callbacks = []; 87 | 88 | callbacks.forEach(({callback, publicInstance}) => { 89 | callback.call(publicInstance); 90 | }); 91 | } 92 | 93 | isMounted(publicInstance) { 94 | return !!this._renderer._element; 95 | } 96 | 97 | enqueueForceUpdate(publicInstance, callback, callerName) { 98 | this._enqueueCallback(callback, publicInstance); 99 | this._renderer._forcedUpdate = true; 100 | this._renderer.render(this._renderer._element, this._renderer._context); 101 | } 102 | 103 | enqueueReplaceState(publicInstance, completeState, callback, callerName) { 104 | this._enqueueCallback(callback, publicInstance); 105 | this._renderer._newState = completeState; 106 | this._renderer.render(this._renderer._element, this._renderer._context); 107 | } 108 | 109 | enqueueSetState(publicInstance, partialState, callback, callerName) { 110 | this._enqueueCallback(callback, publicInstance); 111 | const currentState = this._renderer._newState || publicInstance.state; 112 | 113 | if (typeof partialState === 'function') { 114 | partialState = partialState.call( 115 | publicInstance, 116 | currentState, 117 | publicInstance.props, 118 | ); 119 | } 120 | 121 | // Null and undefined are treated as no-ops. 122 | if (partialState === null || partialState === undefined) { 123 | return; 124 | } 125 | 126 | this._renderer._newState = { 127 | ...currentState, 128 | ...partialState, 129 | }; 130 | 131 | this._renderer.render(this._renderer._element, this._renderer._context); 132 | } 133 | } 134 | 135 | function createHook() { 136 | return { 137 | memoizedState: null, 138 | queue: null, 139 | next: null, 140 | }; 141 | } 142 | 143 | function basicStateReducer(state, action) { 144 | return typeof action === 'function' ? action(state) : action; 145 | } 146 | 147 | class ReactShallowRenderer { 148 | static createRenderer = function() { 149 | return new ReactShallowRenderer(); 150 | }; 151 | 152 | constructor() { 153 | this._reset(); 154 | } 155 | 156 | _reset() { 157 | this._context = null; 158 | this._element = null; 159 | this._instance = null; 160 | this._newState = null; 161 | this._rendered = null; 162 | this._rendering = false; 163 | this._forcedUpdate = false; 164 | this._updater = new Updater(this); 165 | this._dispatcher = this._createDispatcher(); 166 | this._workInProgressHook = null; 167 | this._firstWorkInProgressHook = null; 168 | this._isReRender = false; 169 | this._didScheduleRenderPhaseUpdate = false; 170 | this._renderPhaseUpdates = null; 171 | this._numberOfReRenders = 0; 172 | this._idCounter = 0; 173 | } 174 | 175 | _validateCurrentlyRenderingComponent() { 176 | if (!(this._rendering && !this._instance)) { 177 | throw Error(`Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons: 178 | 1. You might have mismatching versions of React and the renderer (such as React DOM) 179 | 2. You might be breaking the Rules of Hooks 180 | 3. You might have more than one copy of React in the same app 181 | See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.`); 182 | } 183 | } 184 | 185 | _createDispatcher() { 186 | const useReducer = (reducer, initialArg, init) => { 187 | this._validateCurrentlyRenderingComponent(); 188 | this._createWorkInProgressHook(); 189 | const workInProgressHook = this._workInProgressHook; 190 | 191 | if (this._isReRender) { 192 | // This is a re-render. 193 | const queue = workInProgressHook.queue; 194 | const dispatch = queue.dispatch; 195 | if (this._numberOfReRenders > 0) { 196 | // Apply the new render phase updates to the previous current hook. 197 | if (this._renderPhaseUpdates !== null) { 198 | // Render phase updates are stored in a map of queue -> linked list 199 | const firstRenderPhaseUpdate = this._renderPhaseUpdates.get(queue); 200 | if (firstRenderPhaseUpdate !== undefined) { 201 | this._renderPhaseUpdates.delete(queue); 202 | let newState = workInProgressHook.memoizedState; 203 | let update = firstRenderPhaseUpdate; 204 | do { 205 | const action = update.action; 206 | newState = reducer(newState, action); 207 | update = update.next; 208 | } while (update !== null); 209 | workInProgressHook.memoizedState = newState; 210 | return [newState, dispatch]; 211 | } 212 | } 213 | return [workInProgressHook.memoizedState, dispatch]; 214 | } 215 | // Process updates outside of render 216 | let newState = workInProgressHook.memoizedState; 217 | let update = queue.first; 218 | if (update !== null) { 219 | do { 220 | const action = update.action; 221 | newState = reducer(newState, action); 222 | update = update.next; 223 | } while (update !== null); 224 | queue.first = null; 225 | workInProgressHook.memoizedState = newState; 226 | } 227 | return [newState, dispatch]; 228 | } else { 229 | let initialState; 230 | if (reducer === basicStateReducer) { 231 | // Special case for `useState`. 232 | initialState = 233 | typeof initialArg === 'function' ? initialArg() : initialArg; 234 | } else { 235 | initialState = init !== undefined ? init(initialArg) : initialArg; 236 | } 237 | workInProgressHook.memoizedState = initialState; 238 | const queue = (workInProgressHook.queue = { 239 | first: null, 240 | dispatch: null, 241 | }); 242 | const dispatch = (queue.dispatch = this._dispatchAction.bind( 243 | this, 244 | queue, 245 | )); 246 | return [workInProgressHook.memoizedState, dispatch]; 247 | } 248 | }; 249 | 250 | const useState = initialState => { 251 | return useReducer( 252 | basicStateReducer, 253 | // useReducer has a special case to support lazy useState initializers 254 | initialState, 255 | ); 256 | }; 257 | 258 | const useMemo = (nextCreate, deps) => { 259 | this._validateCurrentlyRenderingComponent(); 260 | this._createWorkInProgressHook(); 261 | 262 | const nextDeps = deps !== undefined ? deps : null; 263 | 264 | if ( 265 | this._workInProgressHook !== null && 266 | this._workInProgressHook.memoizedState !== null 267 | ) { 268 | const prevState = this._workInProgressHook.memoizedState; 269 | const prevDeps = prevState[1]; 270 | if (nextDeps !== null) { 271 | if (areHookInputsEqual(nextDeps, prevDeps)) { 272 | return prevState[0]; 273 | } 274 | } 275 | } 276 | 277 | const nextValue = nextCreate(); 278 | this._workInProgressHook.memoizedState = [nextValue, nextDeps]; 279 | return nextValue; 280 | }; 281 | 282 | const useRef = initialValue => { 283 | this._validateCurrentlyRenderingComponent(); 284 | this._createWorkInProgressHook(); 285 | const previousRef = this._workInProgressHook.memoizedState; 286 | if (previousRef === null) { 287 | const ref = {current: initialValue}; 288 | if (process.env.NODE_ENV !== 'production') { 289 | Object.seal(ref); 290 | } 291 | this._workInProgressHook.memoizedState = ref; 292 | return ref; 293 | } else { 294 | return previousRef; 295 | } 296 | }; 297 | 298 | const readContext = (context, observedBits) => { 299 | return context._currentValue; 300 | }; 301 | 302 | const noOp = () => { 303 | this._validateCurrentlyRenderingComponent(); 304 | }; 305 | 306 | const identity = fn => { 307 | return fn; 308 | }; 309 | 310 | const useResponder = (responder, props) => ({ 311 | props: props, 312 | responder, 313 | }); 314 | 315 | const useTransition = config => { 316 | this._validateCurrentlyRenderingComponent(); 317 | const startTransition = callback => { 318 | callback(); 319 | }; 320 | return [startTransition, false]; 321 | }; 322 | 323 | const useDeferredValue = (value, config) => { 324 | this._validateCurrentlyRenderingComponent(); 325 | return value; 326 | }; 327 | 328 | const useId = () => { 329 | this._validateCurrentlyRenderingComponent(); 330 | const nextId = ++this._idCounter; 331 | return ':r' + nextId + ':'; 332 | }; 333 | 334 | const useSyncExternalStore = (subscribe, getSnapshot) => { 335 | this._validateCurrentlyRenderingComponent(); 336 | return getSnapshot(); 337 | }; 338 | 339 | return { 340 | readContext, 341 | useCallback: identity, 342 | useContext: context => { 343 | this._validateCurrentlyRenderingComponent(); 344 | return readContext(context); 345 | }, 346 | useDebugValue: noOp, 347 | useEffect: noOp, 348 | useImperativeHandle: noOp, 349 | useLayoutEffect: noOp, 350 | useInsertionEffect: noOp, 351 | useMemo, 352 | useReducer, 353 | useRef, 354 | useState, 355 | useResponder, 356 | useId, 357 | useTransition, 358 | useDeferredValue, 359 | useSyncExternalStore, 360 | }; 361 | } 362 | 363 | _dispatchAction(queue, action) { 364 | if (!(this._numberOfReRenders < RE_RENDER_LIMIT)) { 365 | throw Error( 366 | `Too many re-renders. React limits the number of renders to prevent an infinite loop.`, 367 | ); 368 | } 369 | 370 | if (this._rendering) { 371 | // This is a render phase update. Stash it in a lazily-created map of 372 | // queue -> linked list of updates. After this render pass, we'll restart 373 | // and apply the stashed updates on top of the work-in-progress hook. 374 | this._didScheduleRenderPhaseUpdate = true; 375 | const update = { 376 | action, 377 | next: null, 378 | }; 379 | let renderPhaseUpdates = this._renderPhaseUpdates; 380 | if (renderPhaseUpdates === null) { 381 | this._renderPhaseUpdates = renderPhaseUpdates = new Map(); 382 | } 383 | const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); 384 | if (firstRenderPhaseUpdate === undefined) { 385 | renderPhaseUpdates.set(queue, update); 386 | } else { 387 | // Append the update to the end of the list. 388 | let lastRenderPhaseUpdate = firstRenderPhaseUpdate; 389 | while (lastRenderPhaseUpdate.next !== null) { 390 | lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; 391 | } 392 | lastRenderPhaseUpdate.next = update; 393 | } 394 | } else { 395 | const update = { 396 | action, 397 | next: null, 398 | }; 399 | 400 | // Append the update to the end of the list. 401 | let last = queue.first; 402 | if (last === null) { 403 | queue.first = update; 404 | } else { 405 | while (last.next !== null) { 406 | last = last.next; 407 | } 408 | last.next = update; 409 | } 410 | 411 | // Re-render now. 412 | this.render(this._element, this._context); 413 | } 414 | } 415 | 416 | _createWorkInProgressHook() { 417 | if (this._workInProgressHook === null) { 418 | // This is the first hook in the list 419 | if (this._firstWorkInProgressHook === null) { 420 | this._isReRender = false; 421 | this._firstWorkInProgressHook = this._workInProgressHook = createHook(); 422 | } else { 423 | // There's already a work-in-progress. Reuse it. 424 | this._isReRender = true; 425 | this._workInProgressHook = this._firstWorkInProgressHook; 426 | } 427 | } else { 428 | if (this._workInProgressHook.next === null) { 429 | this._isReRender = false; 430 | // Append to the end of the list 431 | this._workInProgressHook = this._workInProgressHook.next = createHook(); 432 | } else { 433 | // There's already a work-in-progress. Reuse it. 434 | this._isReRender = true; 435 | this._workInProgressHook = this._workInProgressHook.next; 436 | } 437 | } 438 | return this._workInProgressHook; 439 | } 440 | 441 | _finishHooks(element, context) { 442 | if (this._didScheduleRenderPhaseUpdate) { 443 | // Updates were scheduled during the render phase. They are stored in 444 | // the `renderPhaseUpdates` map. Call the component again, reusing the 445 | // work-in-progress hooks and applying the additional updates on top. Keep 446 | // restarting until no more updates are scheduled. 447 | this._didScheduleRenderPhaseUpdate = false; 448 | this._numberOfReRenders += 1; 449 | 450 | // Start over from the beginning of the list 451 | this._workInProgressHook = null; 452 | this._rendering = false; 453 | this._idCounter = 0; 454 | this.render(element, context); 455 | } else { 456 | this._workInProgressHook = null; 457 | this._renderPhaseUpdates = null; 458 | this._numberOfReRenders = 0; 459 | this._idCounter = 0; 460 | } 461 | } 462 | 463 | getMountedInstance() { 464 | return this._instance; 465 | } 466 | 467 | getRenderOutput() { 468 | return this._rendered; 469 | } 470 | 471 | render(element, context = emptyObject) { 472 | if (!React.isValidElement(element)) { 473 | throw Error( 474 | `ReactShallowRenderer render(): Invalid component element.${ 475 | typeof element === 'function' 476 | ? ' Instead of passing a component class, make sure to instantiate ' + 477 | 'it by passing it to React.createElement.' 478 | : '' 479 | }`, 480 | ); 481 | } 482 | // Show a special message for host elements since it's a common case. 483 | if (!(typeof element.type !== 'string')) { 484 | throw Error( 485 | `ReactShallowRenderer render(): Shallow rendering works only with custom components, not primitives (${element.type}). Instead of calling \`.render(el)\` and inspecting the rendered output, look at \`el.props\` directly instead.`, 486 | ); 487 | } 488 | if ( 489 | !( 490 | isForwardRef(element) || 491 | typeof element.type === 'function' || 492 | isMemo(element) 493 | ) 494 | ) { 495 | throw Error( 496 | `ReactShallowRenderer render(): Shallow rendering works only with custom components, but the provided element type was \`${ 497 | Array.isArray(element.type) 498 | ? 'array' 499 | : element.type === null 500 | ? 'null' 501 | : typeof element.type 502 | }\`.`, 503 | ); 504 | } 505 | 506 | if (this._rendering) { 507 | return; 508 | } 509 | if (this._element != null && this._element.type !== element.type) { 510 | this._reset(); 511 | } 512 | 513 | const elementType = isMemo(element) ? element.type.type : element.type; 514 | const previousElement = this._element; 515 | 516 | this._rendering = true; 517 | this._element = element; 518 | this._context = getMaskedContext(elementType.contextTypes, context); 519 | 520 | // Inner memo component props aren't currently validated in createElement. 521 | let prevGetStack; 522 | if (process.env.NODE_ENV !== 'production') { 523 | prevGetStack = ReactDebugCurrentFrame.getCurrentStack; 524 | ReactDebugCurrentFrame.getCurrentStack = getStackAddendum; 525 | } 526 | try { 527 | if (isMemo(element) && elementType.propTypes) { 528 | currentlyValidatingElement = element; 529 | checkPropTypes( 530 | elementType.propTypes, 531 | element.props, 532 | 'prop', 533 | getComponentName(elementType), 534 | ); 535 | } 536 | 537 | if (this._instance) { 538 | this._updateClassComponent(elementType, element, this._context); 539 | } else { 540 | if (shouldConstruct(elementType)) { 541 | this._instance = new elementType( 542 | element.props, 543 | this._context, 544 | this._updater, 545 | ); 546 | if (typeof elementType.getDerivedStateFromProps === 'function') { 547 | const partialState = elementType.getDerivedStateFromProps.call( 548 | null, 549 | element.props, 550 | this._instance.state, 551 | ); 552 | if (partialState != null) { 553 | this._instance.state = Object.assign( 554 | {}, 555 | this._instance.state, 556 | partialState, 557 | ); 558 | } 559 | } 560 | 561 | if (elementType.contextTypes) { 562 | currentlyValidatingElement = element; 563 | checkPropTypes( 564 | elementType.contextTypes, 565 | this._context, 566 | 'context', 567 | getName(elementType, this._instance), 568 | ); 569 | 570 | currentlyValidatingElement = null; 571 | } 572 | 573 | this._mountClassComponent(elementType, element, this._context); 574 | } else { 575 | let shouldRender = true; 576 | if (isMemo(element) && previousElement !== null) { 577 | // This is a Memo component that is being re-rendered. 578 | const compare = element.type.compare || shallowEqual; 579 | if (compare(previousElement.props, element.props)) { 580 | shouldRender = false; 581 | } 582 | } 583 | if (shouldRender) { 584 | const prevDispatcher = ReactCurrentDispatcher.current; 585 | ReactCurrentDispatcher.current = this._dispatcher; 586 | try { 587 | // elementType could still be a ForwardRef if it was 588 | // nested inside Memo. 589 | if (elementType.$$typeof === ForwardRef) { 590 | if (!(typeof elementType.render === 'function')) { 591 | throw Error( 592 | `forwardRef requires a render function but was given ${typeof elementType.render}.`, 593 | ); 594 | } 595 | this._rendered = elementType.render.call( 596 | undefined, 597 | element.props, 598 | element.ref, 599 | ); 600 | } else { 601 | this._rendered = elementType(element.props, this._context); 602 | } 603 | } finally { 604 | ReactCurrentDispatcher.current = prevDispatcher; 605 | } 606 | this._finishHooks(element, context); 607 | } 608 | } 609 | } 610 | } finally { 611 | if (process.env.NODE_ENV !== 'production') { 612 | ReactDebugCurrentFrame.getCurrentStack = prevGetStack; 613 | } 614 | } 615 | 616 | this._rendering = false; 617 | this._updater._invokeCallbacks(); 618 | 619 | return this.getRenderOutput(); 620 | } 621 | 622 | unmount() { 623 | if (this._instance) { 624 | if (typeof this._instance.componentWillUnmount === 'function') { 625 | this._instance.componentWillUnmount(); 626 | } 627 | } 628 | this._reset(); 629 | } 630 | 631 | _mountClassComponent(elementType, element, context) { 632 | this._instance.context = context; 633 | this._instance.props = element.props; 634 | this._instance.state = this._instance.state || null; 635 | this._instance.updater = this._updater; 636 | 637 | if ( 638 | typeof this._instance.UNSAFE_componentWillMount === 'function' || 639 | typeof this._instance.componentWillMount === 'function' 640 | ) { 641 | const beforeState = this._newState; 642 | 643 | // In order to support react-lifecycles-compat polyfilled components, 644 | // Unsafe lifecycles should not be invoked for components using the new APIs. 645 | if ( 646 | typeof elementType.getDerivedStateFromProps !== 'function' && 647 | typeof this._instance.getSnapshotBeforeUpdate !== 'function' 648 | ) { 649 | if (typeof this._instance.componentWillMount === 'function') { 650 | this._instance.componentWillMount(); 651 | } 652 | if (typeof this._instance.UNSAFE_componentWillMount === 'function') { 653 | this._instance.UNSAFE_componentWillMount(); 654 | } 655 | } 656 | 657 | // setState may have been called during cWM 658 | if (beforeState !== this._newState) { 659 | this._instance.state = this._newState || emptyObject; 660 | } 661 | } 662 | 663 | this._rendered = this._instance.render(); 664 | // Intentionally do not call componentDidMount() 665 | // because DOM refs are not available. 666 | } 667 | 668 | _updateClassComponent(elementType, element, context) { 669 | const {props} = element; 670 | 671 | const oldState = this._instance.state || emptyObject; 672 | const oldProps = this._instance.props; 673 | 674 | if (oldProps !== props) { 675 | // In order to support react-lifecycles-compat polyfilled components, 676 | // Unsafe lifecycles should not be invoked for components using the new APIs. 677 | if ( 678 | typeof elementType.getDerivedStateFromProps !== 'function' && 679 | typeof this._instance.getSnapshotBeforeUpdate !== 'function' 680 | ) { 681 | if (typeof this._instance.componentWillReceiveProps === 'function') { 682 | this._instance.componentWillReceiveProps(props, context); 683 | } 684 | if ( 685 | typeof this._instance.UNSAFE_componentWillReceiveProps === 'function' 686 | ) { 687 | this._instance.UNSAFE_componentWillReceiveProps(props, context); 688 | } 689 | } 690 | } 691 | 692 | // Read state after cWRP in case it calls setState 693 | let state = this._newState || oldState; 694 | if (typeof elementType.getDerivedStateFromProps === 'function') { 695 | const partialState = elementType.getDerivedStateFromProps.call( 696 | null, 697 | props, 698 | state, 699 | ); 700 | if (partialState != null) { 701 | state = Object.assign({}, state, partialState); 702 | } 703 | } 704 | 705 | let shouldUpdate = true; 706 | if (this._forcedUpdate) { 707 | shouldUpdate = true; 708 | this._forcedUpdate = false; 709 | } else if (typeof this._instance.shouldComponentUpdate === 'function') { 710 | shouldUpdate = !!this._instance.shouldComponentUpdate( 711 | props, 712 | state, 713 | context, 714 | ); 715 | } else if ( 716 | elementType.prototype && 717 | elementType.prototype.isPureReactComponent 718 | ) { 719 | shouldUpdate = 720 | !shallowEqual(oldProps, props) || !shallowEqual(oldState, state); 721 | } 722 | 723 | if (shouldUpdate) { 724 | // In order to support react-lifecycles-compat polyfilled components, 725 | // Unsafe lifecycles should not be invoked for components using the new APIs. 726 | if ( 727 | typeof elementType.getDerivedStateFromProps !== 'function' && 728 | typeof this._instance.getSnapshotBeforeUpdate !== 'function' 729 | ) { 730 | if (typeof this._instance.componentWillUpdate === 'function') { 731 | this._instance.componentWillUpdate(props, state, context); 732 | } 733 | if (typeof this._instance.UNSAFE_componentWillUpdate === 'function') { 734 | this._instance.UNSAFE_componentWillUpdate(props, state, context); 735 | } 736 | } 737 | } 738 | 739 | this._instance.context = context; 740 | this._instance.props = props; 741 | this._instance.state = state; 742 | this._newState = null; 743 | 744 | if (shouldUpdate) { 745 | this._rendered = this._instance.render(); 746 | } 747 | // Intentionally do not call componentDidUpdate() 748 | // because DOM refs are not available. 749 | } 750 | } 751 | 752 | let currentlyValidatingElement = null; 753 | 754 | function getDisplayName(element) { 755 | if (element == null) { 756 | return '#empty'; 757 | } else if (typeof element === 'string' || typeof element === 'number') { 758 | return '#text'; 759 | } else if (typeof element.type === 'string') { 760 | return element.type; 761 | } else { 762 | const elementType = isMemo(element) ? element.type.type : element.type; 763 | return elementType.displayName || elementType.name || 'Unknown'; 764 | } 765 | } 766 | 767 | function getStackAddendum() { 768 | let stack = ''; 769 | if (currentlyValidatingElement) { 770 | const name = getDisplayName(currentlyValidatingElement); 771 | const owner = currentlyValidatingElement._owner; 772 | stack += describeComponentFrame( 773 | name, 774 | currentlyValidatingElement._source, 775 | owner && getComponentName(owner.type), 776 | ); 777 | } 778 | return stack; 779 | } 780 | 781 | function getName(type, instance) { 782 | const constructor = instance && instance.constructor; 783 | return ( 784 | type.displayName || 785 | (constructor && constructor.displayName) || 786 | type.name || 787 | (constructor && constructor.name) || 788 | null 789 | ); 790 | } 791 | 792 | function shouldConstruct(Component) { 793 | return !!(Component.prototype && Component.prototype.isReactComponent); 794 | } 795 | 796 | function getMaskedContext(contextTypes, unmaskedContext) { 797 | if (!contextTypes || !unmaskedContext) { 798 | return emptyObject; 799 | } 800 | const context = {}; 801 | for (let key in contextTypes) { 802 | context[key] = unmaskedContext[key]; 803 | } 804 | return context; 805 | } 806 | 807 | export default ReactShallowRenderer; 808 | -------------------------------------------------------------------------------- /src/__tests__/ReactShallowRenderer-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @emails react-core 8 | * @jest-environment node 9 | */ 10 | 11 | import * as PropTypes from 'prop-types'; 12 | import * as React from 'react'; 13 | import ReactShallowRenderer from 'react-shallow-renderer'; 14 | 15 | const createRenderer = ReactShallowRenderer.createRenderer; 16 | 17 | describe('ReactShallowRenderer', () => { 18 | it('should call all of the legacy lifecycle hooks', () => { 19 | const logs = []; 20 | const logger = message => () => logs.push(message) || true; 21 | 22 | class SomeComponent extends React.Component { 23 | UNSAFE_componentWillMount = logger('componentWillMount'); 24 | componentDidMount = logger('componentDidMount'); 25 | UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps'); 26 | shouldComponentUpdate = logger('shouldComponentUpdate'); 27 | UNSAFE_componentWillUpdate = logger('componentWillUpdate'); 28 | componentDidUpdate = logger('componentDidUpdate'); 29 | componentWillUnmount = logger('componentWillUnmount'); 30 | render() { 31 | return
; 32 | } 33 | } 34 | 35 | const shallowRenderer = createRenderer(); 36 | shallowRenderer.render(); 37 | 38 | // Calling cDU might lead to problems with host component references. 39 | // Since our components aren't really mounted, refs won't be available. 40 | expect(logs).toEqual(['componentWillMount']); 41 | 42 | logs.splice(0); 43 | 44 | const instance = shallowRenderer.getMountedInstance(); 45 | instance.setState({}); 46 | 47 | expect(logs).toEqual(['shouldComponentUpdate', 'componentWillUpdate']); 48 | 49 | logs.splice(0); 50 | 51 | shallowRenderer.render(); 52 | 53 | // The previous shallow renderer did not trigger cDU for props changes. 54 | expect(logs).toEqual([ 55 | 'componentWillReceiveProps', 56 | 'shouldComponentUpdate', 57 | 'componentWillUpdate', 58 | ]); 59 | }); 60 | 61 | it('should call all of the new lifecycle hooks', () => { 62 | const logs = []; 63 | const logger = message => () => logs.push(message) || true; 64 | 65 | class SomeComponent extends React.Component { 66 | state = {}; 67 | static getDerivedStateFromProps = logger('getDerivedStateFromProps'); 68 | componentDidMount = logger('componentDidMount'); 69 | shouldComponentUpdate = logger('shouldComponentUpdate'); 70 | componentDidUpdate = logger('componentDidUpdate'); 71 | componentWillUnmount = logger('componentWillUnmount'); 72 | render() { 73 | return
; 74 | } 75 | } 76 | 77 | const shallowRenderer = createRenderer(); 78 | shallowRenderer.render(); 79 | 80 | // Calling cDU might lead to problems with host component references. 81 | // Since our components aren't really mounted, refs won't be available. 82 | expect(logs).toEqual(['getDerivedStateFromProps']); 83 | 84 | logs.splice(0); 85 | 86 | const instance = shallowRenderer.getMountedInstance(); 87 | instance.setState({}); 88 | 89 | expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); 90 | 91 | logs.splice(0); 92 | 93 | shallowRenderer.render(); 94 | 95 | // The previous shallow renderer did not trigger cDU for props changes. 96 | expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); 97 | }); 98 | 99 | it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { 100 | class Component extends React.Component { 101 | state = {}; 102 | static getDerivedStateFromProps() { 103 | return null; 104 | } 105 | componentWillMount() { 106 | throw Error('unexpected'); 107 | } 108 | componentWillReceiveProps() { 109 | throw Error('unexpected'); 110 | } 111 | componentWillUpdate() { 112 | throw Error('unexpected'); 113 | } 114 | render() { 115 | return null; 116 | } 117 | } 118 | 119 | const shallowRenderer = createRenderer(); 120 | shallowRenderer.render(); 121 | }); 122 | 123 | it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => { 124 | class Component extends React.Component { 125 | getSnapshotBeforeUpdate() { 126 | return null; 127 | } 128 | componentWillMount() { 129 | throw Error('unexpected'); 130 | } 131 | componentWillReceiveProps() { 132 | throw Error('unexpected'); 133 | } 134 | componentWillUpdate() { 135 | throw Error('unexpected'); 136 | } 137 | render() { 138 | return null; 139 | } 140 | } 141 | 142 | const shallowRenderer = createRenderer(); 143 | shallowRenderer.render(); 144 | shallowRenderer.render(); 145 | }); 146 | 147 | it('should not call getSnapshotBeforeUpdate or componentDidUpdate when updating since refs wont exist', () => { 148 | class Component extends React.Component { 149 | getSnapshotBeforeUpdate() { 150 | throw Error('unexpected'); 151 | } 152 | componentDidUpdate() { 153 | throw Error('unexpected'); 154 | } 155 | render() { 156 | return null; 157 | } 158 | } 159 | 160 | const shallowRenderer = createRenderer(); 161 | shallowRenderer.render(); 162 | shallowRenderer.render(); 163 | }); 164 | 165 | it('should only render 1 level deep', () => { 166 | function Parent() { 167 | return ( 168 |
169 | 170 |
171 | ); 172 | } 173 | function Child() { 174 | throw Error('This component should not render'); 175 | } 176 | 177 | const shallowRenderer = createRenderer(); 178 | shallowRenderer.render(React.createElement(Parent)); 179 | }); 180 | 181 | it('should have shallow rendering', () => { 182 | class SomeComponent extends React.Component { 183 | render() { 184 | return ( 185 |
186 | 187 | 188 |
189 | ); 190 | } 191 | } 192 | 193 | const shallowRenderer = createRenderer(); 194 | const result = shallowRenderer.render(); 195 | 196 | expect(result.type).toBe('div'); 197 | expect(result.props.children).toEqual([ 198 | , 199 | , 200 | ]); 201 | }); 202 | 203 | it('should handle ForwardRef', () => { 204 | const testRef = React.createRef(); 205 | const SomeComponent = React.forwardRef((props, ref) => { 206 | expect(ref).toEqual(testRef); 207 | return ( 208 |
209 | 210 | 211 |
212 | ); 213 | }); 214 | 215 | const shallowRenderer = createRenderer(); 216 | const result = shallowRenderer.render(); 217 | 218 | expect(result.type).toBe('div'); 219 | expect(result.props.children).toEqual([ 220 | , 221 | , 222 | ]); 223 | }); 224 | 225 | it('should handle Profiler', () => { 226 | class SomeComponent extends React.Component { 227 | render() { 228 | return ( 229 | 230 |
231 | 232 | 233 |
234 |
235 | ); 236 | } 237 | } 238 | 239 | const shallowRenderer = createRenderer(); 240 | const result = shallowRenderer.render(); 241 | 242 | expect(result.type).toBe(React.Profiler); 243 | expect(result.props.children).toEqual( 244 |
245 | 246 | 247 |
, 248 | ); 249 | }); 250 | 251 | it('should enable shouldComponentUpdate to prevent a re-render', () => { 252 | let renderCounter = 0; 253 | class SimpleComponent extends React.Component { 254 | state = {update: false}; 255 | shouldComponentUpdate(nextProps, nextState) { 256 | return this.state.update !== nextState.update; 257 | } 258 | render() { 259 | renderCounter++; 260 | return
{`${renderCounter}`}
; 261 | } 262 | } 263 | 264 | const shallowRenderer = createRenderer(); 265 | shallowRenderer.render(); 266 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 267 | 268 | const instance = shallowRenderer.getMountedInstance(); 269 | instance.setState({update: false}); 270 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 271 | 272 | instance.setState({update: true}); 273 | expect(shallowRenderer.getRenderOutput()).toEqual(
2
); 274 | }); 275 | 276 | it('should enable PureComponent to prevent a re-render', () => { 277 | let renderCounter = 0; 278 | class SimpleComponent extends React.PureComponent { 279 | state = {update: false}; 280 | render() { 281 | renderCounter++; 282 | return
{`${renderCounter}`}
; 283 | } 284 | } 285 | 286 | const shallowRenderer = createRenderer(); 287 | shallowRenderer.render(); 288 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 289 | 290 | const instance = shallowRenderer.getMountedInstance(); 291 | instance.setState({update: false}); 292 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 293 | 294 | instance.setState({update: true}); 295 | expect(shallowRenderer.getRenderOutput()).toEqual(
2
); 296 | }); 297 | 298 | it('should not run shouldComponentUpdate during forced update', () => { 299 | let scuCounter = 0; 300 | class SimpleComponent extends React.Component { 301 | state = {count: 1}; 302 | shouldComponentUpdate() { 303 | scuCounter++; 304 | return false; 305 | } 306 | render() { 307 | return
{`${this.state.count}`}
; 308 | } 309 | } 310 | 311 | const shallowRenderer = createRenderer(); 312 | shallowRenderer.render(); 313 | expect(scuCounter).toEqual(0); 314 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 315 | 316 | // Force update the initial state. sCU should not fire. 317 | const instance = shallowRenderer.getMountedInstance(); 318 | instance.forceUpdate(); 319 | expect(scuCounter).toEqual(0); 320 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 321 | 322 | // Setting state updates the instance, but doesn't re-render 323 | // because sCU returned false. 324 | instance.setState(state => ({count: state.count + 1})); 325 | expect(scuCounter).toEqual(1); 326 | expect(instance.state.count).toEqual(2); 327 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 328 | 329 | // A force update updates the render output, but doesn't call sCU. 330 | instance.forceUpdate(); 331 | expect(scuCounter).toEqual(1); 332 | expect(instance.state.count).toEqual(2); 333 | expect(shallowRenderer.getRenderOutput()).toEqual(
2
); 334 | }); 335 | 336 | it('should rerender when calling forceUpdate', () => { 337 | let renderCounter = 0; 338 | class SimpleComponent extends React.Component { 339 | render() { 340 | renderCounter += 1; 341 | return
; 342 | } 343 | } 344 | 345 | const shallowRenderer = createRenderer(); 346 | shallowRenderer.render(); 347 | expect(renderCounter).toEqual(1); 348 | 349 | const instance = shallowRenderer.getMountedInstance(); 350 | instance.forceUpdate(); 351 | expect(renderCounter).toEqual(2); 352 | }); 353 | 354 | it('should shallow render a function component', () => { 355 | function SomeComponent(props, context) { 356 | return ( 357 |
358 |
{props.foo}
359 |
{context.bar}
360 | 361 | 362 |
363 | ); 364 | } 365 | SomeComponent.contextTypes = { 366 | bar: PropTypes.string, 367 | }; 368 | 369 | const shallowRenderer = createRenderer(); 370 | const result = shallowRenderer.render(, { 371 | bar: 'BAR', 372 | }); 373 | 374 | expect(result.type).toBe('div'); 375 | expect(result.props.children).toEqual([ 376 |
FOO
, 377 |
BAR
, 378 | , 379 | , 380 | ]); 381 | }); 382 | 383 | it('should shallow render a component returning strings directly from render', () => { 384 | const Text = ({value}) => value; 385 | 386 | const shallowRenderer = createRenderer(); 387 | const result = shallowRenderer.render(); 388 | expect(result).toEqual('foo'); 389 | }); 390 | 391 | it('should shallow render a component returning numbers directly from render', () => { 392 | const Text = ({value}) => value; 393 | 394 | const shallowRenderer = createRenderer(); 395 | const result = shallowRenderer.render(); 396 | expect(result).toEqual(10); 397 | }); 398 | 399 | it('should shallow render a fragment', () => { 400 | class SomeComponent extends React.Component { 401 | render() { 402 | return
; 403 | } 404 | } 405 | class Fragment extends React.Component { 406 | render() { 407 | return [
, , ]; 408 | } 409 | } 410 | const shallowRenderer = createRenderer(); 411 | const result = shallowRenderer.render(); 412 | expect(result).toEqual([ 413 |
, 414 | , 415 | , 416 | ]); 417 | }); 418 | 419 | it('should shallow render a React.fragment', () => { 420 | class SomeComponent extends React.Component { 421 | render() { 422 | return
; 423 | } 424 | } 425 | class Fragment extends React.Component { 426 | render() { 427 | return ( 428 | <> 429 |
430 | 431 | 432 | 433 | ); 434 | } 435 | } 436 | const shallowRenderer = createRenderer(); 437 | const result = shallowRenderer.render(); 438 | expect(result).toEqual( 439 | <> 440 |
441 | 442 | 443 | , 444 | ); 445 | }); 446 | 447 | it('should throw for invalid elements', () => { 448 | class SomeComponent extends React.Component { 449 | render() { 450 | return
; 451 | } 452 | } 453 | 454 | const shallowRenderer = createRenderer(); 455 | expect(() => shallowRenderer.render(SomeComponent)).toThrowError( 456 | 'ReactShallowRenderer render(): Invalid component element. Instead of ' + 457 | 'passing a component class, make sure to instantiate it by passing it ' + 458 | 'to React.createElement.', 459 | ); 460 | expect(() => shallowRenderer.render(
)).toThrowError( 461 | 'ReactShallowRenderer render(): Shallow rendering works only with ' + 462 | 'custom components, not primitives (div). Instead of calling ' + 463 | '`.render(el)` and inspecting the rendered output, look at `el.props` ' + 464 | 'directly instead.', 465 | ); 466 | }); 467 | 468 | it('should have shallow unmounting', () => { 469 | const componentWillUnmount = jest.fn(); 470 | 471 | class SomeComponent extends React.Component { 472 | componentWillUnmount = componentWillUnmount; 473 | render() { 474 | return
; 475 | } 476 | } 477 | 478 | const shallowRenderer = createRenderer(); 479 | shallowRenderer.render(); 480 | shallowRenderer.unmount(); 481 | 482 | expect(componentWillUnmount).toBeCalled(); 483 | }); 484 | 485 | it('can shallow render to null', () => { 486 | class SomeComponent extends React.Component { 487 | render() { 488 | return null; 489 | } 490 | } 491 | 492 | const shallowRenderer = createRenderer(); 493 | const result = shallowRenderer.render(); 494 | 495 | expect(result).toBe(null); 496 | }); 497 | 498 | it('can shallow render with a ref', () => { 499 | class SomeComponent extends React.Component { 500 | render() { 501 | return
; 502 | } 503 | } 504 | 505 | const shallowRenderer = createRenderer(); 506 | // Shouldn't crash. 507 | shallowRenderer.render(); 508 | }); 509 | 510 | it('lets you update shallowly rendered components', () => { 511 | class SomeComponent extends React.Component { 512 | state = {clicked: false}; 513 | 514 | onClick = () => { 515 | this.setState({clicked: true}); 516 | }; 517 | 518 | render() { 519 | const className = this.state.clicked ? 'was-clicked' : ''; 520 | 521 | if (this.props.aNew === 'prop') { 522 | return ( 523 | 524 | Test link 525 | 526 | ); 527 | } else { 528 | return ( 529 |
530 | 531 | 532 |
533 | ); 534 | } 535 | } 536 | } 537 | 538 | const shallowRenderer = createRenderer(); 539 | const result = shallowRenderer.render(); 540 | expect(result.type).toBe('div'); 541 | expect(result.props.children).toEqual([ 542 | , 543 | , 544 | ]); 545 | 546 | const updatedResult = shallowRenderer.render(); 547 | expect(updatedResult.type).toBe('a'); 548 | 549 | const mockEvent = {}; 550 | updatedResult.props.onClick(mockEvent); 551 | 552 | const updatedResultCausedByClick = shallowRenderer.getRenderOutput(); 553 | expect(updatedResultCausedByClick.type).toBe('a'); 554 | expect(updatedResultCausedByClick.props.className).toBe('was-clicked'); 555 | }); 556 | 557 | it('can access the mounted component instance', () => { 558 | class SimpleComponent extends React.Component { 559 | someMethod = () => { 560 | return this.props.n; 561 | }; 562 | 563 | render() { 564 | return
{this.props.n}
; 565 | } 566 | } 567 | 568 | const shallowRenderer = createRenderer(); 569 | shallowRenderer.render(); 570 | expect(shallowRenderer.getMountedInstance().someMethod()).toEqual(5); 571 | }); 572 | 573 | it('can shallowly render components with contextTypes', () => { 574 | class SimpleComponent extends React.Component { 575 | static contextTypes = { 576 | name: PropTypes.string, 577 | }; 578 | 579 | render() { 580 | return
; 581 | } 582 | } 583 | 584 | const shallowRenderer = createRenderer(); 585 | const result = shallowRenderer.render(); 586 | expect(result).toEqual(
); 587 | }); 588 | 589 | it('passes expected params to legacy component lifecycle methods', () => { 590 | const componentDidUpdateParams = []; 591 | const componentWillReceivePropsParams = []; 592 | const componentWillUpdateParams = []; 593 | const setStateParams = []; 594 | const shouldComponentUpdateParams = []; 595 | 596 | const initialProp = {prop: 'init prop'}; 597 | const initialState = {state: 'init state'}; 598 | const initialContext = {context: 'init context'}; 599 | const updatedState = {state: 'updated state'}; 600 | const updatedProp = {prop: 'updated prop'}; 601 | const updatedContext = {context: 'updated context'}; 602 | 603 | class SimpleComponent extends React.Component { 604 | constructor(props, context) { 605 | super(props, context); 606 | this.state = initialState; 607 | } 608 | static contextTypes = { 609 | context: PropTypes.string, 610 | }; 611 | componentDidUpdate(...args) { 612 | componentDidUpdateParams.push(...args); 613 | } 614 | UNSAFE_componentWillReceiveProps(...args) { 615 | componentWillReceivePropsParams.push(...args); 616 | this.setState((...innerArgs) => { 617 | setStateParams.push(...innerArgs); 618 | return updatedState; 619 | }); 620 | } 621 | UNSAFE_componentWillUpdate(...args) { 622 | componentWillUpdateParams.push(...args); 623 | } 624 | shouldComponentUpdate(...args) { 625 | shouldComponentUpdateParams.push(...args); 626 | return true; 627 | } 628 | render() { 629 | return null; 630 | } 631 | } 632 | 633 | const shallowRenderer = createRenderer(); 634 | shallowRenderer.render( 635 | React.createElement(SimpleComponent, initialProp), 636 | initialContext, 637 | ); 638 | expect(componentDidUpdateParams).toEqual([]); 639 | expect(componentWillReceivePropsParams).toEqual([]); 640 | expect(componentWillUpdateParams).toEqual([]); 641 | expect(setStateParams).toEqual([]); 642 | expect(shouldComponentUpdateParams).toEqual([]); 643 | 644 | // Lifecycle hooks should be invoked with the correct prev/next params on update. 645 | shallowRenderer.render( 646 | React.createElement(SimpleComponent, updatedProp), 647 | updatedContext, 648 | ); 649 | expect(componentWillReceivePropsParams).toEqual([ 650 | updatedProp, 651 | updatedContext, 652 | ]); 653 | expect(setStateParams).toEqual([initialState, initialProp]); 654 | expect(shouldComponentUpdateParams).toEqual([ 655 | updatedProp, 656 | updatedState, 657 | updatedContext, 658 | ]); 659 | expect(componentWillUpdateParams).toEqual([ 660 | updatedProp, 661 | updatedState, 662 | updatedContext, 663 | ]); 664 | expect(componentDidUpdateParams).toEqual([]); 665 | }); 666 | 667 | it('passes expected params to new component lifecycle methods', () => { 668 | const componentDidUpdateParams = []; 669 | const getDerivedStateFromPropsParams = []; 670 | const shouldComponentUpdateParams = []; 671 | 672 | const initialProp = {prop: 'init prop'}; 673 | const initialState = {state: 'init state'}; 674 | const initialContext = {context: 'init context'}; 675 | const updatedProp = {prop: 'updated prop'}; 676 | const updatedContext = {context: 'updated context'}; 677 | 678 | class SimpleComponent extends React.Component { 679 | constructor(props, context) { 680 | super(props, context); 681 | this.state = initialState; 682 | } 683 | static contextTypes = { 684 | context: PropTypes.string, 685 | }; 686 | componentDidUpdate(...args) { 687 | componentDidUpdateParams.push(...args); 688 | } 689 | static getDerivedStateFromProps(...args) { 690 | getDerivedStateFromPropsParams.push(args); 691 | return null; 692 | } 693 | shouldComponentUpdate(...args) { 694 | shouldComponentUpdateParams.push(...args); 695 | return true; 696 | } 697 | render() { 698 | return null; 699 | } 700 | } 701 | 702 | const shallowRenderer = createRenderer(); 703 | 704 | // The only lifecycle hook that should be invoked on initial render 705 | // Is the static getDerivedStateFromProps() methods 706 | shallowRenderer.render( 707 | React.createElement(SimpleComponent, initialProp), 708 | initialContext, 709 | ); 710 | expect(getDerivedStateFromPropsParams).toEqual([ 711 | [initialProp, initialState], 712 | ]); 713 | expect(componentDidUpdateParams).toEqual([]); 714 | expect(shouldComponentUpdateParams).toEqual([]); 715 | 716 | // Lifecycle hooks should be invoked with the correct prev/next params on update. 717 | shallowRenderer.render( 718 | React.createElement(SimpleComponent, updatedProp), 719 | updatedContext, 720 | ); 721 | expect(getDerivedStateFromPropsParams).toEqual([ 722 | [initialProp, initialState], 723 | [updatedProp, initialState], 724 | ]); 725 | expect(shouldComponentUpdateParams).toEqual([ 726 | updatedProp, 727 | initialState, 728 | updatedContext, 729 | ]); 730 | expect(componentDidUpdateParams).toEqual([]); 731 | }); 732 | 733 | it('can shallowly render components with ref as function', () => { 734 | class SimpleComponent extends React.Component { 735 | state = {clicked: false}; 736 | 737 | handleUserClick = () => { 738 | this.setState({clicked: true}); 739 | }; 740 | 741 | render() { 742 | return ( 743 |
{}} 745 | onClick={this.handleUserClick} 746 | className={this.state.clicked ? 'clicked' : ''} 747 | /> 748 | ); 749 | } 750 | } 751 | 752 | const shallowRenderer = createRenderer(); 753 | shallowRenderer.render(); 754 | let result = shallowRenderer.getRenderOutput(); 755 | expect(result.type).toEqual('div'); 756 | expect(result.props.className).toEqual(''); 757 | result.props.onClick(); 758 | 759 | result = shallowRenderer.getRenderOutput(); 760 | expect(result.type).toEqual('div'); 761 | expect(result.props.className).toEqual('clicked'); 762 | }); 763 | 764 | it('can initialize state via static getDerivedStateFromProps', () => { 765 | class SimpleComponent extends React.Component { 766 | state = { 767 | count: 1, 768 | }; 769 | 770 | static getDerivedStateFromProps(props, prevState) { 771 | return { 772 | count: prevState.count + props.incrementBy, 773 | other: 'foobar', 774 | }; 775 | } 776 | 777 | render() { 778 | return ( 779 |
{`count:${this.state.count}, other:${this.state.other}`}
780 | ); 781 | } 782 | } 783 | 784 | const shallowRenderer = createRenderer(); 785 | const result = shallowRenderer.render(); 786 | expect(result).toEqual(
count:3, other:foobar
); 787 | }); 788 | 789 | it('can setState in componentWillMount when shallow rendering', () => { 790 | class SimpleComponent extends React.Component { 791 | UNSAFE_componentWillMount() { 792 | this.setState({groovy: 'doovy'}); 793 | } 794 | 795 | render() { 796 | return
{this.state.groovy}
; 797 | } 798 | } 799 | 800 | const shallowRenderer = createRenderer(); 801 | const result = shallowRenderer.render(); 802 | expect(result).toEqual(
doovy
); 803 | }); 804 | 805 | it('can setState in componentWillMount repeatedly when shallow rendering', () => { 806 | class SimpleComponent extends React.Component { 807 | state = { 808 | separator: '-', 809 | }; 810 | 811 | UNSAFE_componentWillMount() { 812 | this.setState({groovy: 'doovy'}); 813 | this.setState({doovy: 'groovy'}); 814 | } 815 | 816 | render() { 817 | const {groovy, doovy, separator} = this.state; 818 | 819 | return
{`${groovy}${separator}${doovy}`}
; 820 | } 821 | } 822 | 823 | const shallowRenderer = createRenderer(); 824 | const result = shallowRenderer.render(); 825 | expect(result).toEqual(
doovy-groovy
); 826 | }); 827 | 828 | it('can setState in componentWillMount with an updater function repeatedly when shallow rendering', () => { 829 | class SimpleComponent extends React.Component { 830 | state = { 831 | separator: '-', 832 | }; 833 | 834 | UNSAFE_componentWillMount() { 835 | this.setState(state => ({groovy: 'doovy'})); 836 | this.setState(state => ({doovy: state.groovy})); 837 | } 838 | 839 | render() { 840 | const {groovy, doovy, separator} = this.state; 841 | 842 | return
{`${groovy}${separator}${doovy}`}
; 843 | } 844 | } 845 | 846 | const shallowRenderer = createRenderer(); 847 | const result = shallowRenderer.render(); 848 | expect(result).toEqual(
doovy-doovy
); 849 | }); 850 | 851 | it('can setState in componentWillReceiveProps when shallow rendering', () => { 852 | class SimpleComponent extends React.Component { 853 | state = {count: 0}; 854 | 855 | UNSAFE_componentWillReceiveProps(nextProps) { 856 | if (nextProps.updateState) { 857 | this.setState({count: 1}); 858 | } 859 | } 860 | 861 | render() { 862 | return
{this.state.count}
; 863 | } 864 | } 865 | 866 | const shallowRenderer = createRenderer(); 867 | let result = shallowRenderer.render( 868 | , 869 | ); 870 | expect(result.props.children).toEqual(0); 871 | 872 | result = shallowRenderer.render(); 873 | expect(result.props.children).toEqual(1); 874 | }); 875 | 876 | it('can update state with static getDerivedStateFromProps when shallow rendering', () => { 877 | class SimpleComponent extends React.Component { 878 | state = {count: 1}; 879 | 880 | static getDerivedStateFromProps(nextProps, prevState) { 881 | if (nextProps.updateState) { 882 | return {count: nextProps.incrementBy + prevState.count}; 883 | } 884 | 885 | return null; 886 | } 887 | 888 | render() { 889 | return
{this.state.count}
; 890 | } 891 | } 892 | 893 | const shallowRenderer = createRenderer(); 894 | let result = shallowRenderer.render( 895 | , 896 | ); 897 | expect(result.props.children).toEqual(1); 898 | 899 | result = shallowRenderer.render( 900 | , 901 | ); 902 | expect(result.props.children).toEqual(3); 903 | 904 | result = shallowRenderer.render( 905 | , 906 | ); 907 | expect(result.props.children).toEqual(3); 908 | }); 909 | 910 | it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => { 911 | class SimpleComponent extends React.Component { 912 | state = {value: 0}; 913 | 914 | static getDerivedStateFromProps(nextProps, prevState) { 915 | return {...prevState}; 916 | } 917 | 918 | updateState = () => { 919 | this.setState(state => ({value: state.value + 1})); 920 | }; 921 | 922 | render() { 923 | return
{`value:${this.state.value}`}
; 924 | } 925 | } 926 | 927 | const shallowRenderer = createRenderer(); 928 | let result = shallowRenderer.render(); 929 | expect(result).toEqual(
value:0
); 930 | 931 | let instance = shallowRenderer.getMountedInstance(); 932 | instance.updateState(); 933 | result = shallowRenderer.getRenderOutput(); 934 | expect(result).toEqual(
value:1
); 935 | }); 936 | 937 | it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', () => { 938 | class SimpleComponent extends React.Component { 939 | constructor(props) { 940 | super(props); 941 | this.state = { 942 | value: props.value, 943 | }; 944 | } 945 | 946 | static getDerivedStateFromProps(nextProps, prevState) { 947 | if (nextProps.value === prevState.value) { 948 | return null; 949 | } 950 | return {value: nextProps.value}; 951 | } 952 | 953 | shouldComponentUpdate(nextProps, nextState) { 954 | return nextState.value !== this.state.value; 955 | } 956 | 957 | render() { 958 | return
{`value:${this.state.value}`}
; 959 | } 960 | } 961 | 962 | const shallowRenderer = createRenderer(); 963 | const initialResult = shallowRenderer.render( 964 | , 965 | ); 966 | expect(initialResult).toEqual(
value:initial
); 967 | const updatedResult = shallowRenderer.render( 968 | , 969 | ); 970 | expect(updatedResult).toEqual(
value:updated
); 971 | }); 972 | 973 | it('can setState with an updater function', () => { 974 | let instance; 975 | 976 | class SimpleComponent extends React.Component { 977 | state = { 978 | counter: 0, 979 | }; 980 | 981 | render() { 982 | instance = this; 983 | return ( 984 | 987 | ); 988 | } 989 | } 990 | 991 | const shallowRenderer = createRenderer(); 992 | let result = shallowRenderer.render(); 993 | expect(result.props.children).toEqual(0); 994 | 995 | instance.setState((state, props) => { 996 | return {counter: props.defaultCount + 1}; 997 | }); 998 | 999 | result = shallowRenderer.getRenderOutput(); 1000 | expect(result.props.children).toEqual(2); 1001 | }); 1002 | 1003 | it('can access component instance from setState updater function', done => { 1004 | let instance; 1005 | 1006 | class SimpleComponent extends React.Component { 1007 | state = {}; 1008 | 1009 | render() { 1010 | instance = this; 1011 | return null; 1012 | } 1013 | } 1014 | 1015 | const shallowRenderer = createRenderer(); 1016 | shallowRenderer.render(); 1017 | 1018 | instance.setState(function updater(state, props) { 1019 | expect(this).toBe(instance); 1020 | done(); 1021 | }); 1022 | }); 1023 | 1024 | it('can setState with a callback', () => { 1025 | let instance; 1026 | 1027 | class SimpleComponent extends React.Component { 1028 | state = { 1029 | counter: 0, 1030 | }; 1031 | render() { 1032 | instance = this; 1033 | return

{this.state.counter}

; 1034 | } 1035 | } 1036 | 1037 | const shallowRenderer = createRenderer(); 1038 | const result = shallowRenderer.render(); 1039 | expect(result.props.children).toBe(0); 1040 | 1041 | const callback = jest.fn(function() { 1042 | expect(this).toBe(instance); 1043 | }); 1044 | 1045 | instance.setState({counter: 1}, callback); 1046 | 1047 | const updated = shallowRenderer.getRenderOutput(); 1048 | expect(updated.props.children).toBe(1); 1049 | expect(callback).toHaveBeenCalled(); 1050 | }); 1051 | 1052 | it('can replaceState with a callback', () => { 1053 | let instance; 1054 | 1055 | class SimpleComponent extends React.Component { 1056 | state = { 1057 | counter: 0, 1058 | }; 1059 | render() { 1060 | instance = this; 1061 | return

{this.state.counter}

; 1062 | } 1063 | } 1064 | 1065 | const shallowRenderer = createRenderer(); 1066 | const result = shallowRenderer.render(); 1067 | expect(result.props.children).toBe(0); 1068 | 1069 | const callback = jest.fn(function() { 1070 | expect(this).toBe(instance); 1071 | }); 1072 | 1073 | // No longer a public API, but we can test that it works internally by 1074 | // reaching into the updater. 1075 | shallowRenderer._updater.enqueueReplaceState( 1076 | instance, 1077 | {counter: 1}, 1078 | callback, 1079 | ); 1080 | 1081 | const updated = shallowRenderer.getRenderOutput(); 1082 | expect(updated.props.children).toBe(1); 1083 | expect(callback).toHaveBeenCalled(); 1084 | }); 1085 | 1086 | it('can forceUpdate with a callback', () => { 1087 | let instance; 1088 | 1089 | class SimpleComponent extends React.Component { 1090 | state = { 1091 | counter: 0, 1092 | }; 1093 | render() { 1094 | instance = this; 1095 | return

{this.state.counter}

; 1096 | } 1097 | } 1098 | 1099 | const shallowRenderer = createRenderer(); 1100 | const result = shallowRenderer.render(); 1101 | expect(result.props.children).toBe(0); 1102 | 1103 | const callback = jest.fn(function() { 1104 | expect(this).toBe(instance); 1105 | }); 1106 | 1107 | instance.forceUpdate(callback); 1108 | 1109 | const updated = shallowRenderer.getRenderOutput(); 1110 | expect(updated.props.children).toBe(0); 1111 | expect(callback).toHaveBeenCalled(); 1112 | }); 1113 | 1114 | it('can pass context when shallowly rendering', () => { 1115 | class SimpleComponent extends React.Component { 1116 | static contextTypes = { 1117 | name: PropTypes.string, 1118 | }; 1119 | 1120 | render() { 1121 | return
{this.context.name}
; 1122 | } 1123 | } 1124 | 1125 | const shallowRenderer = createRenderer(); 1126 | const result = shallowRenderer.render(, { 1127 | name: 'foo', 1128 | }); 1129 | expect(result).toEqual(
foo
); 1130 | }); 1131 | 1132 | it('should track context across updates', () => { 1133 | class SimpleComponent extends React.Component { 1134 | static contextTypes = { 1135 | foo: PropTypes.string, 1136 | }; 1137 | 1138 | state = { 1139 | bar: 'bar', 1140 | }; 1141 | 1142 | render() { 1143 | return
{`${this.context.foo}:${this.state.bar}`}
; 1144 | } 1145 | } 1146 | 1147 | const shallowRenderer = createRenderer(); 1148 | let result = shallowRenderer.render(, { 1149 | foo: 'foo', 1150 | }); 1151 | expect(result).toEqual(
foo:bar
); 1152 | 1153 | const instance = shallowRenderer.getMountedInstance(); 1154 | instance.setState({bar: 'baz'}); 1155 | 1156 | result = shallowRenderer.getRenderOutput(); 1157 | expect(result).toEqual(
foo:baz
); 1158 | }); 1159 | 1160 | it('should filter context by contextTypes', () => { 1161 | class SimpleComponent extends React.Component { 1162 | static contextTypes = { 1163 | foo: PropTypes.string, 1164 | }; 1165 | render() { 1166 | return
{`${this.context.foo}:${this.context.bar}`}
; 1167 | } 1168 | } 1169 | 1170 | const shallowRenderer = createRenderer(); 1171 | let result = shallowRenderer.render(, { 1172 | foo: 'foo', 1173 | bar: 'bar', 1174 | }); 1175 | expect(result).toEqual(
foo:undefined
); 1176 | }); 1177 | 1178 | it('can fail context when shallowly rendering', () => { 1179 | class SimpleComponent extends React.Component { 1180 | static contextTypes = { 1181 | name: PropTypes.string.isRequired, 1182 | }; 1183 | 1184 | render() { 1185 | return
{this.context.name}
; 1186 | } 1187 | } 1188 | 1189 | const shallowRenderer = createRenderer(); 1190 | expect(() => shallowRenderer.render()).toErrorDev( 1191 | 'Warning: Failed context type: The context `name` is marked as ' + 1192 | 'required in `SimpleComponent`, but its value is `undefined`.\n' + 1193 | ' in SimpleComponent (at **)', 1194 | ); 1195 | }); 1196 | 1197 | it('should warn about propTypes (but only once)', () => { 1198 | class SimpleComponent extends React.Component { 1199 | render() { 1200 | return React.createElement('div', null, this.props.name); 1201 | } 1202 | } 1203 | 1204 | SimpleComponent.propTypes = { 1205 | name: PropTypes.string.isRequired, 1206 | }; 1207 | 1208 | const shallowRenderer = createRenderer(); 1209 | expect(() => 1210 | shallowRenderer.render(React.createElement(SimpleComponent, {name: 123})), 1211 | ).toErrorDev( 1212 | 'Warning: Failed prop type: Invalid prop `name` of type `number` ' + 1213 | 'supplied to `SimpleComponent`, expected `string`.', 1214 | {withoutStack: true}, 1215 | ); 1216 | }); 1217 | 1218 | it('should enable rendering of cloned element', () => { 1219 | class SimpleComponent extends React.Component { 1220 | constructor(props) { 1221 | super(props); 1222 | 1223 | this.state = { 1224 | bar: 'bar', 1225 | }; 1226 | } 1227 | 1228 | render() { 1229 | return
{`${this.props.foo}:${this.state.bar}`}
; 1230 | } 1231 | } 1232 | 1233 | const shallowRenderer = createRenderer(); 1234 | const el = ; 1235 | let result = shallowRenderer.render(el); 1236 | expect(result).toEqual(
foo:bar
); 1237 | 1238 | const cloned = React.cloneElement(el, {foo: 'baz'}); 1239 | result = shallowRenderer.render(cloned); 1240 | expect(result).toEqual(
baz:bar
); 1241 | }); 1242 | 1243 | it('this.state should be updated on setState callback inside componentWillMount', () => { 1244 | let stateSuccessfullyUpdated = false; 1245 | 1246 | class Component extends React.Component { 1247 | constructor(props, context) { 1248 | super(props, context); 1249 | this.state = { 1250 | hasUpdatedState: false, 1251 | }; 1252 | } 1253 | 1254 | UNSAFE_componentWillMount() { 1255 | this.setState( 1256 | {hasUpdatedState: true}, 1257 | () => (stateSuccessfullyUpdated = this.state.hasUpdatedState), 1258 | ); 1259 | } 1260 | 1261 | render() { 1262 | return
{this.props.children}
; 1263 | } 1264 | } 1265 | 1266 | const shallowRenderer = createRenderer(); 1267 | shallowRenderer.render(); 1268 | expect(stateSuccessfullyUpdated).toBe(true); 1269 | }); 1270 | 1271 | it('should handle multiple callbacks', () => { 1272 | const mockFn = jest.fn(); 1273 | const shallowRenderer = createRenderer(); 1274 | 1275 | class Component extends React.Component { 1276 | constructor(props, context) { 1277 | super(props, context); 1278 | this.state = { 1279 | foo: 'foo', 1280 | }; 1281 | } 1282 | 1283 | UNSAFE_componentWillMount() { 1284 | this.setState({foo: 'bar'}, () => mockFn()); 1285 | this.setState({foo: 'foobar'}, () => mockFn()); 1286 | } 1287 | 1288 | render() { 1289 | return
{this.state.foo}
; 1290 | } 1291 | } 1292 | 1293 | shallowRenderer.render(); 1294 | 1295 | expect(mockFn).toHaveBeenCalledTimes(2); 1296 | 1297 | // Ensure the callback queue is cleared after the callbacks are invoked 1298 | const mountedInstance = shallowRenderer.getMountedInstance(); 1299 | mountedInstance.setState({foo: 'bar'}, () => mockFn()); 1300 | expect(mockFn).toHaveBeenCalledTimes(3); 1301 | }); 1302 | 1303 | it('should call the setState callback even if shouldComponentUpdate = false', done => { 1304 | const mockFn = jest.fn().mockReturnValue(false); 1305 | 1306 | class Component extends React.Component { 1307 | constructor(props, context) { 1308 | super(props, context); 1309 | this.state = { 1310 | hasUpdatedState: false, 1311 | }; 1312 | } 1313 | 1314 | shouldComponentUpdate() { 1315 | return mockFn(); 1316 | } 1317 | 1318 | render() { 1319 | return
{this.state.hasUpdatedState}
; 1320 | } 1321 | } 1322 | 1323 | const shallowRenderer = createRenderer(); 1324 | shallowRenderer.render(); 1325 | 1326 | const mountedInstance = shallowRenderer.getMountedInstance(); 1327 | mountedInstance.setState({hasUpdatedState: true}, () => { 1328 | expect(mockFn).toBeCalled(); 1329 | expect(mountedInstance.state.hasUpdatedState).toBe(true); 1330 | done(); 1331 | }); 1332 | }); 1333 | 1334 | it('throws usefully when rendering badly-typed elements', () => { 1335 | const shallowRenderer = createRenderer(); 1336 | 1337 | const renderAndVerifyWarningAndError = (Component, typeString) => { 1338 | expect(() => { 1339 | expect(() => shallowRenderer.render()).toErrorDev( 1340 | 'React.createElement: type is invalid -- expected a string ' + 1341 | '(for built-in components) or a class/function (for composite components) ' + 1342 | `but got: ${typeString}.`, 1343 | ); 1344 | }).toThrowError( 1345 | 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 1346 | `components, but the provided element type was \`${typeString}\`.`, 1347 | ); 1348 | }; 1349 | 1350 | renderAndVerifyWarningAndError(undefined, 'undefined'); 1351 | renderAndVerifyWarningAndError(null, 'null'); 1352 | renderAndVerifyWarningAndError([], 'array'); 1353 | renderAndVerifyWarningAndError({}, 'object'); 1354 | }); 1355 | 1356 | it('should have initial state of null if not defined', () => { 1357 | class SomeComponent extends React.Component { 1358 | render() { 1359 | return ; 1360 | } 1361 | } 1362 | 1363 | const shallowRenderer = createRenderer(); 1364 | shallowRenderer.render(); 1365 | 1366 | expect(shallowRenderer.getMountedInstance().state).toBeNull(); 1367 | }); 1368 | 1369 | it('should invoke both deprecated and new lifecycles if both are present', () => { 1370 | const log = []; 1371 | 1372 | class Component extends React.Component { 1373 | componentWillMount() { 1374 | log.push('componentWillMount'); 1375 | } 1376 | componentWillReceiveProps() { 1377 | log.push('componentWillReceiveProps'); 1378 | } 1379 | componentWillUpdate() { 1380 | log.push('componentWillUpdate'); 1381 | } 1382 | UNSAFE_componentWillMount() { 1383 | log.push('UNSAFE_componentWillMount'); 1384 | } 1385 | UNSAFE_componentWillReceiveProps() { 1386 | log.push('UNSAFE_componentWillReceiveProps'); 1387 | } 1388 | UNSAFE_componentWillUpdate() { 1389 | log.push('UNSAFE_componentWillUpdate'); 1390 | } 1391 | render() { 1392 | return null; 1393 | } 1394 | } 1395 | 1396 | const shallowRenderer = createRenderer(); 1397 | shallowRenderer.render(); 1398 | expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']); 1399 | 1400 | log.length = 0; 1401 | 1402 | shallowRenderer.render(); 1403 | expect(log).toEqual([ 1404 | 'componentWillReceiveProps', 1405 | 'UNSAFE_componentWillReceiveProps', 1406 | 'componentWillUpdate', 1407 | 'UNSAFE_componentWillUpdate', 1408 | ]); 1409 | }); 1410 | 1411 | it('should stop the update when setState returns null or undefined', () => { 1412 | const log = []; 1413 | let instance; 1414 | class Component extends React.Component { 1415 | constructor(props) { 1416 | super(props); 1417 | this.state = { 1418 | count: 0, 1419 | }; 1420 | } 1421 | render() { 1422 | log.push('render'); 1423 | instance = this; 1424 | return null; 1425 | } 1426 | } 1427 | const shallowRenderer = createRenderer(); 1428 | shallowRenderer.render(); 1429 | log.length = 0; 1430 | instance.setState(() => null); 1431 | instance.setState(() => undefined); 1432 | instance.setState(null); 1433 | instance.setState(undefined); 1434 | expect(log).toEqual([]); 1435 | instance.setState(state => ({count: state.count + 1})); 1436 | expect(log).toEqual(['render']); 1437 | }); 1438 | 1439 | it('should not get this in a function component', () => { 1440 | const logs = []; 1441 | function Foo() { 1442 | logs.push(this); 1443 | return
foo
; 1444 | } 1445 | const shallowRenderer = createRenderer(); 1446 | shallowRenderer.render(); 1447 | expect(logs).toEqual([undefined]); 1448 | }); 1449 | 1450 | it('should handle memo', () => { 1451 | function Foo() { 1452 | return
foo
; 1453 | } 1454 | const MemoFoo = React.memo(Foo); 1455 | const shallowRenderer = createRenderer(); 1456 | shallowRenderer.render(); 1457 | }); 1458 | 1459 | it('should enable React.memo to prevent a re-render', () => { 1460 | const logs = []; 1461 | const Foo = React.memo(({count}) => { 1462 | logs.push(`Foo: ${count}`); 1463 | return
{count}
; 1464 | }); 1465 | const Bar = React.memo(({count}) => { 1466 | logs.push(`Bar: ${count}`); 1467 | return
{count}
; 1468 | }); 1469 | const shallowRenderer = createRenderer(); 1470 | shallowRenderer.render(); 1471 | expect(logs).toEqual(['Foo: 1']); 1472 | logs.length = 0; 1473 | // Rendering the same element with the same props should be prevented 1474 | shallowRenderer.render(); 1475 | expect(logs).toEqual([]); 1476 | // A different element with the same props should cause a re-render 1477 | shallowRenderer.render(); 1478 | expect(logs).toEqual(['Bar: 1']); 1479 | }); 1480 | 1481 | it('should respect a custom comparison function with React.memo', () => { 1482 | let renderCount = 0; 1483 | function areEqual(props, nextProps) { 1484 | return props.foo === nextProps.foo; 1485 | } 1486 | const Foo = React.memo(({foo, bar}) => { 1487 | renderCount++; 1488 | return ( 1489 |
1490 | {foo} {bar} 1491 |
1492 | ); 1493 | }, areEqual); 1494 | 1495 | const shallowRenderer = createRenderer(); 1496 | shallowRenderer.render(); 1497 | expect(renderCount).toBe(1); 1498 | // Change a prop that the comparison funciton ignores 1499 | shallowRenderer.render(); 1500 | expect(renderCount).toBe(1); 1501 | shallowRenderer.render(); 1502 | expect(renderCount).toBe(2); 1503 | }); 1504 | 1505 | it('should not call the comparison function with React.memo on the initial render', () => { 1506 | const areEqual = jest.fn(() => false); 1507 | const SomeComponent = React.memo(({foo}) => { 1508 | return
{foo}
; 1509 | }, areEqual); 1510 | const shallowRenderer = createRenderer(); 1511 | shallowRenderer.render(); 1512 | expect(areEqual).not.toHaveBeenCalled(); 1513 | expect(shallowRenderer.getRenderOutput()).toEqual(
{1}
); 1514 | }); 1515 | 1516 | it('should handle memo(forwardRef())', () => { 1517 | const testRef = React.createRef(); 1518 | const SomeComponent = React.forwardRef((props, ref) => { 1519 | expect(ref).toEqual(testRef); 1520 | return ( 1521 |
1522 | 1523 | 1524 |
1525 | ); 1526 | }); 1527 | 1528 | const SomeMemoComponent = React.memo(SomeComponent); 1529 | 1530 | const shallowRenderer = createRenderer(); 1531 | const result = shallowRenderer.render(); 1532 | 1533 | expect(result.type).toBe('div'); 1534 | expect(result.props.children).toEqual([ 1535 | , 1536 | , 1537 | ]); 1538 | }); 1539 | 1540 | it('should warn for forwardRef(memo())', () => { 1541 | const testRef = React.createRef(); 1542 | const SomeMemoComponent = React.memo(({foo}) => { 1543 | return
{foo}
; 1544 | }); 1545 | const shallowRenderer = createRenderer(); 1546 | expect(() => { 1547 | expect(() => { 1548 | const SomeComponent = React.forwardRef(SomeMemoComponent); 1549 | shallowRenderer.render(); 1550 | }).toErrorDev( 1551 | 'Warning: forwardRef requires a render function but received ' + 1552 | 'a `memo` component. Instead of forwardRef(memo(...)), use ' + 1553 | 'memo(forwardRef(...))', 1554 | {withoutStack: true}, 1555 | ); 1556 | }).toThrowError( 1557 | 'forwardRef requires a render function but was given object.', 1558 | ); 1559 | }); 1560 | 1561 | it('should let you change type', () => { 1562 | function Foo({prop}) { 1563 | return
Foo {prop}
; 1564 | } 1565 | function Bar({prop}) { 1566 | return
Bar {prop}
; 1567 | } 1568 | 1569 | const shallowRenderer = createRenderer(); 1570 | shallowRenderer.render(); 1571 | expect(shallowRenderer.getRenderOutput()).toEqual(
Foo {'foo1'}
); 1572 | shallowRenderer.render(); 1573 | expect(shallowRenderer.getRenderOutput()).toEqual(
Foo {'foo2'}
); 1574 | shallowRenderer.render(); 1575 | expect(shallowRenderer.getRenderOutput()).toEqual(
Bar {'bar1'}
); 1576 | shallowRenderer.render(); 1577 | expect(shallowRenderer.getRenderOutput()).toEqual(
Bar {'bar2'}
); 1578 | }); 1579 | 1580 | it('should let you change class type', () => { 1581 | class Foo extends React.Component { 1582 | render() { 1583 | return
Foo {this.props.prop}
; 1584 | } 1585 | } 1586 | class Bar extends React.Component { 1587 | render() { 1588 | return
Bar {this.props.prop}
; 1589 | } 1590 | } 1591 | 1592 | const shallowRenderer = createRenderer(); 1593 | shallowRenderer.render(); 1594 | expect(shallowRenderer.getRenderOutput()).toEqual(
Foo {'foo1'}
); 1595 | shallowRenderer.render(); 1596 | expect(shallowRenderer.getRenderOutput()).toEqual(
Foo {'foo2'}
); 1597 | shallowRenderer.render(); 1598 | expect(shallowRenderer.getRenderOutput()).toEqual(
Bar {'bar1'}
); 1599 | shallowRenderer.render(); 1600 | expect(shallowRenderer.getRenderOutput()).toEqual(
Bar {'bar2'}
); 1601 | }); 1602 | }); 1603 | -------------------------------------------------------------------------------- /src/__tests__/ReactShallowRendererHooks-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @emails react-core 8 | * @jest-environment node 9 | */ 10 | 11 | import * as React from 'react'; 12 | import ReactShallowRenderer from 'react-shallow-renderer'; 13 | 14 | const createRenderer = ReactShallowRenderer.createRenderer; 15 | 16 | describe('ReactShallowRenderer with hooks', () => { 17 | it('should work with useState', () => { 18 | function SomeComponent({defaultName}) { 19 | const [name] = React.useState(defaultName); 20 | 21 | return ( 22 |
23 |

24 | Your name is: {name} 25 |

26 |
27 | ); 28 | } 29 | 30 | const shallowRenderer = createRenderer(); 31 | let result = shallowRenderer.render( 32 | , 33 | ); 34 | 35 | expect(result).toEqual( 36 |
37 |

38 | Your name is: Dominic 39 |

40 |
, 41 | ); 42 | 43 | result = shallowRenderer.render( 44 | , 45 | ); 46 | 47 | expect(result).toEqual( 48 |
49 |

50 | Your name is: Dominic 51 |

52 |
, 53 | ); 54 | }); 55 | 56 | it('should work with updating a value from useState', () => { 57 | function SomeComponent({defaultName}) { 58 | const [name, updateName] = React.useState(defaultName); 59 | 60 | if (name !== 'Dan') { 61 | updateName('Dan'); 62 | } 63 | 64 | return ( 65 |
66 |

67 | Your name is: {name} 68 |

69 |
70 | ); 71 | } 72 | 73 | const shallowRenderer = createRenderer(); 74 | const result = shallowRenderer.render( 75 | , 76 | ); 77 | 78 | expect(result).toEqual( 79 |
80 |

81 | Your name is: Dan 82 |

83 |
, 84 | ); 85 | }); 86 | 87 | it('should work with updating a derived value from useState', () => { 88 | let _updateName; 89 | 90 | function SomeComponent({defaultName}) { 91 | const [name, updateName] = React.useState(defaultName); 92 | const [prevName, updatePrevName] = React.useState(defaultName); 93 | const [letter, updateLetter] = React.useState(name[0]); 94 | 95 | _updateName = updateName; 96 | 97 | if (name !== prevName) { 98 | updatePrevName(name); 99 | updateLetter(name[0]); 100 | } 101 | 102 | return ( 103 |
104 |

105 | Your name is: {name + ' (' + letter + ')'} 106 |

107 |
108 | ); 109 | } 110 | 111 | const shallowRenderer = createRenderer(); 112 | let result = shallowRenderer.render( 113 | , 114 | ); 115 | expect(result).toEqual( 116 |
117 |

118 | Your name is: Sophie (S) 119 |

120 |
, 121 | ); 122 | 123 | result = shallowRenderer.render(); 124 | expect(result).toEqual( 125 |
126 |

127 | Your name is: Sophie (S) 128 |

129 |
, 130 | ); 131 | 132 | _updateName('Dan'); 133 | expect(shallowRenderer.getRenderOutput()).toEqual( 134 |
135 |

136 | Your name is: Dan (D) 137 |

138 |
, 139 | ); 140 | }); 141 | 142 | it('should work with useReducer', () => { 143 | function reducer(state, action) { 144 | switch (action.type) { 145 | case 'increment': 146 | return {count: state.count + 1}; 147 | case 'decrement': 148 | return {count: state.count - 1}; 149 | } 150 | } 151 | 152 | function SomeComponent(props) { 153 | const [state] = React.useReducer(reducer, props, p => ({ 154 | count: p.initialCount, 155 | })); 156 | 157 | return ( 158 |
159 |

160 | The counter is at: {state.count.toString()} 161 |

162 |
163 | ); 164 | } 165 | 166 | const shallowRenderer = createRenderer(); 167 | let result = shallowRenderer.render(); 168 | 169 | expect(result).toEqual( 170 |
171 |

172 | The counter is at: 0 173 |

174 |
, 175 | ); 176 | 177 | result = shallowRenderer.render(); 178 | 179 | expect(result).toEqual( 180 |
181 |

182 | The counter is at: 0 183 |

184 |
, 185 | ); 186 | }); 187 | 188 | it('should work with a dispatched state change for a useReducer', () => { 189 | function reducer(state, action) { 190 | switch (action.type) { 191 | case 'increment': 192 | return {count: state.count + 1}; 193 | case 'decrement': 194 | return {count: state.count - 1}; 195 | } 196 | } 197 | 198 | function SomeComponent(props) { 199 | const [state, dispatch] = React.useReducer(reducer, props, p => ({ 200 | count: p.initialCount, 201 | })); 202 | 203 | if (state.count === 0) { 204 | dispatch({type: 'increment'}); 205 | } 206 | 207 | return ( 208 |
209 |

210 | The counter is at: {state.count.toString()} 211 |

212 |
213 | ); 214 | } 215 | 216 | const shallowRenderer = createRenderer(); 217 | let result = shallowRenderer.render(); 218 | 219 | expect(result).toEqual( 220 |
221 |

222 | The counter is at: 1 223 |

224 |
, 225 | ); 226 | }); 227 | 228 | it('should not trigger effects', () => { 229 | let effectsCalled = []; 230 | 231 | function SomeComponent({defaultName}) { 232 | React.useEffect(() => { 233 | effectsCalled.push('useEffect'); 234 | }); 235 | 236 | React.useInsertionEffect(() => { 237 | effectsCalled.push('useInsertionEffect'); 238 | }); 239 | 240 | React.useLayoutEffect(() => { 241 | effectsCalled.push('useLayoutEffect'); 242 | }); 243 | 244 | return
Hello world
; 245 | } 246 | 247 | const shallowRenderer = createRenderer(); 248 | shallowRenderer.render(); 249 | 250 | expect(effectsCalled).toEqual([]); 251 | }); 252 | 253 | it('should work with useRef', () => { 254 | function SomeComponent() { 255 | const randomNumberRef = React.useRef({number: Math.random()}); 256 | 257 | return ( 258 |
259 |

The random number is: {randomNumberRef.current.number}

260 |
261 | ); 262 | } 263 | 264 | const shallowRenderer = createRenderer(); 265 | let firstResult = shallowRenderer.render(); 266 | let secondResult = shallowRenderer.render(); 267 | 268 | expect(firstResult).toEqual(secondResult); 269 | }); 270 | 271 | it('should work with useMemo', () => { 272 | function SomeComponent() { 273 | const randomNumber = React.useMemo(() => { 274 | return {number: Math.random()}; 275 | }, []); 276 | 277 | return ( 278 |
279 |

The random number is: {randomNumber.number}

280 |
281 | ); 282 | } 283 | 284 | const shallowRenderer = createRenderer(); 285 | let firstResult = shallowRenderer.render(); 286 | let secondResult = shallowRenderer.render(); 287 | 288 | expect(firstResult).toEqual(secondResult); 289 | }); 290 | 291 | it('should work with useContext', () => { 292 | const SomeContext = React.createContext('default'); 293 | 294 | function SomeComponent() { 295 | const value = React.useContext(SomeContext); 296 | 297 | return ( 298 |
299 |

{value}

300 |
301 | ); 302 | } 303 | 304 | const shallowRenderer = createRenderer(); 305 | let result = shallowRenderer.render(); 306 | 307 | expect(result).toEqual( 308 |
309 |

default

310 |
, 311 | ); 312 | }); 313 | 314 | it('should not leak state when component type changes', () => { 315 | function SomeComponent({defaultName}) { 316 | const [name] = React.useState(defaultName); 317 | 318 | return ( 319 |
320 |

321 | Your name is: {name} 322 |

323 |
324 | ); 325 | } 326 | 327 | function SomeOtherComponent({defaultName}) { 328 | const [name] = React.useState(defaultName); 329 | 330 | return ( 331 |
332 |

333 | Your name is: {name} 334 |

335 |
336 | ); 337 | } 338 | 339 | const shallowRenderer = createRenderer(); 340 | let result = shallowRenderer.render( 341 | , 342 | ); 343 | expect(result).toEqual( 344 |
345 |

346 | Your name is: Dominic 347 |

348 |
, 349 | ); 350 | 351 | result = shallowRenderer.render(); 352 | expect(result).toEqual( 353 |
354 |

355 | Your name is: Dan 356 |

357 |
, 358 | ); 359 | }); 360 | 361 | it('should work with with forwardRef + any hook', () => { 362 | const SomeComponent = React.forwardRef((props, ref) => { 363 | const randomNumberRef = React.useRef({number: Math.random()}); 364 | 365 | return ( 366 |
367 |

The random number is: {randomNumberRef.current.number}

368 |
369 | ); 370 | }); 371 | 372 | const shallowRenderer = createRenderer(); 373 | let firstResult = shallowRenderer.render(); 374 | let secondResult = shallowRenderer.render(); 375 | 376 | expect(firstResult).toEqual(secondResult); 377 | }); 378 | 379 | it('should update a value from useState outside the render', () => { 380 | let _dispatch; 381 | 382 | function SomeComponent({defaultName}) { 383 | const [count, dispatch] = React.useReducer( 384 | (s, a) => (a === 'inc' ? s + 1 : s), 385 | 0, 386 | ); 387 | const [name, updateName] = React.useState(defaultName); 388 | _dispatch = () => dispatch('inc'); 389 | 390 | return ( 391 |
updateName('Dan')}> 392 |

393 | Your name is: {name} ({count}) 394 |

395 |
396 | ); 397 | } 398 | 399 | const shallowRenderer = createRenderer(); 400 | const element = ; 401 | const result = shallowRenderer.render(element); 402 | expect(result.props.children).toEqual( 403 |

404 | Your name is: Dominic ({0}) 405 |

, 406 | ); 407 | 408 | result.props.onClick(); 409 | let updated = shallowRenderer.render(element); 410 | expect(updated.props.children).toEqual( 411 |

412 | Your name is: Dan ({0}) 413 |

, 414 | ); 415 | 416 | _dispatch('foo'); 417 | updated = shallowRenderer.render(element); 418 | expect(updated.props.children).toEqual( 419 |

420 | Your name is: Dan ({1}) 421 |

, 422 | ); 423 | 424 | _dispatch('inc'); 425 | updated = shallowRenderer.render(element); 426 | expect(updated.props.children).toEqual( 427 |

428 | Your name is: Dan ({2}) 429 |

, 430 | ); 431 | }); 432 | 433 | it('should ignore a foreign update outside the render', () => { 434 | let _updateCountForFirstRender; 435 | 436 | function SomeComponent() { 437 | const [count, updateCount] = React.useState(0); 438 | if (!_updateCountForFirstRender) { 439 | _updateCountForFirstRender = updateCount; 440 | } 441 | return count; 442 | } 443 | 444 | const shallowRenderer = createRenderer(); 445 | const element = ; 446 | let result = shallowRenderer.render(element); 447 | expect(result).toEqual(0); 448 | _updateCountForFirstRender(1); 449 | result = shallowRenderer.render(element); 450 | expect(result).toEqual(1); 451 | 452 | shallowRenderer.unmount(); 453 | result = shallowRenderer.render(element); 454 | expect(result).toEqual(0); 455 | _updateCountForFirstRender(1); // Should be ignored. 456 | result = shallowRenderer.render(element); 457 | expect(result).toEqual(0); 458 | }); 459 | 460 | it('should not forget render phase updates', () => { 461 | let _updateCount; 462 | 463 | function SomeComponent() { 464 | const [count, updateCount] = React.useState(0); 465 | _updateCount = updateCount; 466 | if (count < 5) { 467 | updateCount(x => x + 1); 468 | } 469 | return count; 470 | } 471 | 472 | const shallowRenderer = createRenderer(); 473 | const element = ; 474 | let result = shallowRenderer.render(element); 475 | expect(result).toEqual(5); 476 | 477 | _updateCount(10); 478 | result = shallowRenderer.render(element); 479 | expect(result).toEqual(10); 480 | 481 | _updateCount(x => x + 1); 482 | result = shallowRenderer.render(element); 483 | expect(result).toEqual(11); 484 | 485 | _updateCount(x => x - 10); 486 | result = shallowRenderer.render(element); 487 | expect(result).toEqual(5); 488 | }); 489 | 490 | it('should work with useId', () => { 491 | function SomeComponent({defaultName}) { 492 | const id = React.useId(); 493 | const id2 = React.useId(); 494 | 495 | return ( 496 |
497 |
498 |
499 |
500 | ); 501 | } 502 | 503 | const shallowRenderer = createRenderer(); 504 | let result = shallowRenderer.render(); 505 | 506 | expect(result).toEqual( 507 |
508 |
509 |
510 |
, 511 | ); 512 | 513 | result = shallowRenderer.render(); 514 | 515 | expect(result).toEqual( 516 |
517 |
518 |
519 |
, 520 | ); 521 | }); 522 | 523 | it('should work with useSyncExternalStore', () => { 524 | function createExternalStore(initialState) { 525 | const listeners = new Set(); 526 | let currentState = initialState; 527 | return { 528 | set(text) { 529 | currentState = text; 530 | listeners.forEach(listener => listener()); 531 | }, 532 | subscribe(listener) { 533 | listeners.add(listener); 534 | return () => listeners.delete(listener); 535 | }, 536 | getState() { 537 | return currentState; 538 | }, 539 | getSubscriberCount() { 540 | return listeners.size; 541 | }, 542 | }; 543 | } 544 | 545 | const store = createExternalStore('hello'); 546 | 547 | function SomeComponent() { 548 | const value = React.useSyncExternalStore(store.subscribe, store.getState); 549 | return
{value}
; 550 | } 551 | 552 | const shallowRenderer = createRenderer(); 553 | let result = shallowRenderer.render(); 554 | expect(result).toEqual(
hello
); 555 | store.set('goodbye'); 556 | result = shallowRenderer.render(); 557 | expect(result).toEqual(
goodbye
); 558 | }); 559 | }); 560 | -------------------------------------------------------------------------------- /src/__tests__/ReactShallowRendererMemo-test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * @emails react-core 8 | * @jest-environment node 9 | */ 10 | 11 | import * as PropTypes from 'prop-types'; 12 | import * as React from 'react'; 13 | import ReactShallowRenderer from 'react-shallow-renderer'; 14 | 15 | const createRenderer = ReactShallowRenderer.createRenderer; 16 | 17 | describe('ReactShallowRendererMemo', () => { 18 | it('should call all of the legacy lifecycle hooks', () => { 19 | const logs = []; 20 | const logger = message => () => logs.push(message) || true; 21 | 22 | const SomeComponent = React.memo( 23 | class SomeComponent extends React.Component { 24 | UNSAFE_componentWillMount = logger('componentWillMount'); 25 | componentDidMount = logger('componentDidMount'); 26 | UNSAFE_componentWillReceiveProps = logger('componentWillReceiveProps'); 27 | shouldComponentUpdate = logger('shouldComponentUpdate'); 28 | UNSAFE_componentWillUpdate = logger('componentWillUpdate'); 29 | componentDidUpdate = logger('componentDidUpdate'); 30 | componentWillUnmount = logger('componentWillUnmount'); 31 | render() { 32 | return
; 33 | } 34 | }, 35 | ); 36 | 37 | const shallowRenderer = createRenderer(); 38 | shallowRenderer.render(); 39 | 40 | // Calling cDU might lead to problems with host component references. 41 | // Since our components aren't really mounted, refs won't be available. 42 | expect(logs).toEqual(['componentWillMount']); 43 | 44 | logs.splice(0); 45 | 46 | const instance = shallowRenderer.getMountedInstance(); 47 | instance.setState({}); 48 | 49 | expect(logs).toEqual(['shouldComponentUpdate', 'componentWillUpdate']); 50 | 51 | logs.splice(0); 52 | 53 | shallowRenderer.render(); 54 | 55 | // The previous shallow renderer did not trigger cDU for props changes. 56 | expect(logs).toEqual([ 57 | 'componentWillReceiveProps', 58 | 'shouldComponentUpdate', 59 | 'componentWillUpdate', 60 | ]); 61 | }); 62 | 63 | it('should call all of the new lifecycle hooks', () => { 64 | const logs = []; 65 | const logger = message => () => logs.push(message) || true; 66 | 67 | const SomeComponent = React.memo( 68 | class SomeComponent extends React.Component { 69 | state = {}; 70 | static getDerivedStateFromProps = logger('getDerivedStateFromProps'); 71 | componentDidMount = logger('componentDidMount'); 72 | shouldComponentUpdate = logger('shouldComponentUpdate'); 73 | componentDidUpdate = logger('componentDidUpdate'); 74 | componentWillUnmount = logger('componentWillUnmount'); 75 | render() { 76 | return
; 77 | } 78 | }, 79 | ); 80 | 81 | const shallowRenderer = createRenderer(); 82 | shallowRenderer.render(); 83 | 84 | // Calling cDU might lead to problems with host component references. 85 | // Since our components aren't really mounted, refs won't be available. 86 | expect(logs).toEqual(['getDerivedStateFromProps']); 87 | 88 | logs.splice(0); 89 | 90 | const instance = shallowRenderer.getMountedInstance(); 91 | instance.setState({}); 92 | 93 | expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); 94 | 95 | logs.splice(0); 96 | 97 | shallowRenderer.render(); 98 | 99 | // The previous shallow renderer did not trigger cDU for props changes. 100 | expect(logs).toEqual(['getDerivedStateFromProps', 'shouldComponentUpdate']); 101 | }); 102 | 103 | it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new static gDSFP is present', () => { 104 | const Component = React.memo( 105 | class Component extends React.Component { 106 | state = {}; 107 | static getDerivedStateFromProps() { 108 | return null; 109 | } 110 | componentWillMount() { 111 | throw Error('unexpected'); 112 | } 113 | componentWillReceiveProps() { 114 | throw Error('unexpected'); 115 | } 116 | componentWillUpdate() { 117 | throw Error('unexpected'); 118 | } 119 | render() { 120 | return null; 121 | } 122 | }, 123 | ); 124 | 125 | const shallowRenderer = createRenderer(); 126 | shallowRenderer.render(); 127 | }); 128 | 129 | it('should not invoke deprecated lifecycles (cWM/cWRP/cWU) if new getSnapshotBeforeUpdate is present', () => { 130 | const Component = React.memo( 131 | class Component extends React.Component { 132 | getSnapshotBeforeUpdate() { 133 | return null; 134 | } 135 | componentWillMount() { 136 | throw Error('unexpected'); 137 | } 138 | componentWillReceiveProps() { 139 | throw Error('unexpected'); 140 | } 141 | componentWillUpdate() { 142 | throw Error('unexpected'); 143 | } 144 | render() { 145 | return null; 146 | } 147 | }, 148 | ); 149 | 150 | const shallowRenderer = createRenderer(); 151 | shallowRenderer.render(); 152 | shallowRenderer.render(); 153 | }); 154 | 155 | it('should not call getSnapshotBeforeUpdate or componentDidUpdate when updating since refs wont exist', () => { 156 | const Component = React.memo( 157 | class Component extends React.Component { 158 | getSnapshotBeforeUpdate() { 159 | throw Error('unexpected'); 160 | } 161 | componentDidUpdate() { 162 | throw Error('unexpected'); 163 | } 164 | render() { 165 | return null; 166 | } 167 | }, 168 | ); 169 | 170 | const shallowRenderer = createRenderer(); 171 | shallowRenderer.render(); 172 | shallowRenderer.render(); 173 | }); 174 | 175 | it('should only render 1 level deep', () => { 176 | const Parent = React.memo(function Parent() { 177 | return ( 178 |
179 | 180 |
181 | ); 182 | }); 183 | 184 | function Child() { 185 | throw Error('This component should not render'); 186 | } 187 | 188 | const shallowRenderer = createRenderer(); 189 | shallowRenderer.render(React.createElement(Parent)); 190 | }); 191 | 192 | it('should have shallow rendering', () => { 193 | const SomeComponent = React.memo( 194 | class SomeComponent extends React.Component { 195 | render() { 196 | return ( 197 |
198 | 199 | 200 |
201 | ); 202 | } 203 | }, 204 | ); 205 | 206 | const shallowRenderer = createRenderer(); 207 | const result = shallowRenderer.render(); 208 | 209 | expect(result.type).toBe('div'); 210 | expect(result.props.children).toEqual([ 211 | , 212 | , 213 | ]); 214 | }); 215 | 216 | it('should handle Profiler', () => { 217 | const SomeComponent = React.memo( 218 | class SomeComponent extends React.Component { 219 | render() { 220 | return ( 221 | 222 |
223 | 224 | 225 |
226 |
227 | ); 228 | } 229 | }, 230 | ); 231 | 232 | const shallowRenderer = createRenderer(); 233 | const result = shallowRenderer.render(); 234 | 235 | expect(result.type).toBe(React.Profiler); 236 | expect(result.props.children).toEqual( 237 |
238 | 239 | 240 |
, 241 | ); 242 | }); 243 | 244 | it('should enable shouldComponentUpdate to prevent a re-render', () => { 245 | let renderCounter = 0; 246 | const SimpleComponent = React.memo( 247 | class SimpleComponent extends React.Component { 248 | state = {update: false}; 249 | shouldComponentUpdate(nextProps, nextState) { 250 | return this.state.update !== nextState.update; 251 | } 252 | render() { 253 | renderCounter++; 254 | return
{`${renderCounter}`}
; 255 | } 256 | }, 257 | ); 258 | 259 | const shallowRenderer = createRenderer(); 260 | shallowRenderer.render(); 261 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 262 | 263 | const instance = shallowRenderer.getMountedInstance(); 264 | instance.setState({update: false}); 265 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 266 | 267 | instance.setState({update: true}); 268 | expect(shallowRenderer.getRenderOutput()).toEqual(
2
); 269 | }); 270 | 271 | it('should enable PureComponent to prevent a re-render', () => { 272 | let renderCounter = 0; 273 | const SimpleComponent = React.memo( 274 | class SimpleComponent extends React.PureComponent { 275 | state = {update: false}; 276 | render() { 277 | renderCounter++; 278 | return
{`${renderCounter}`}
; 279 | } 280 | }, 281 | ); 282 | 283 | const shallowRenderer = createRenderer(); 284 | shallowRenderer.render(); 285 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 286 | 287 | const instance = shallowRenderer.getMountedInstance(); 288 | instance.setState({update: false}); 289 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 290 | 291 | instance.setState({update: true}); 292 | expect(shallowRenderer.getRenderOutput()).toEqual(
2
); 293 | }); 294 | 295 | it('should not run shouldComponentUpdate during forced update', () => { 296 | let scuCounter = 0; 297 | const SimpleComponent = React.memo( 298 | class SimpleComponent extends React.Component { 299 | state = {count: 1}; 300 | shouldComponentUpdate() { 301 | scuCounter++; 302 | return false; 303 | } 304 | render() { 305 | return
{`${this.state.count}`}
; 306 | } 307 | }, 308 | ); 309 | 310 | const shallowRenderer = createRenderer(); 311 | shallowRenderer.render(); 312 | expect(scuCounter).toEqual(0); 313 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 314 | 315 | // Force update the initial state. sCU should not fire. 316 | const instance = shallowRenderer.getMountedInstance(); 317 | instance.forceUpdate(); 318 | expect(scuCounter).toEqual(0); 319 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 320 | 321 | // Setting state updates the instance, but doesn't re-render 322 | // because sCU returned false. 323 | instance.setState(state => ({count: state.count + 1})); 324 | expect(scuCounter).toEqual(1); 325 | expect(instance.state.count).toEqual(2); 326 | expect(shallowRenderer.getRenderOutput()).toEqual(
1
); 327 | 328 | // A force update updates the render output, but doesn't call sCU. 329 | instance.forceUpdate(); 330 | expect(scuCounter).toEqual(1); 331 | expect(instance.state.count).toEqual(2); 332 | expect(shallowRenderer.getRenderOutput()).toEqual(
2
); 333 | }); 334 | 335 | it('should rerender when calling forceUpdate', () => { 336 | let renderCounter = 0; 337 | const SimpleComponent = React.memo( 338 | class SimpleComponent extends React.Component { 339 | render() { 340 | renderCounter += 1; 341 | return
; 342 | } 343 | }, 344 | ); 345 | 346 | const shallowRenderer = createRenderer(); 347 | shallowRenderer.render(); 348 | expect(renderCounter).toEqual(1); 349 | 350 | const instance = shallowRenderer.getMountedInstance(); 351 | instance.forceUpdate(); 352 | expect(renderCounter).toEqual(2); 353 | }); 354 | 355 | it('should shallow render a function component', () => { 356 | function SomeComponent(props, context) { 357 | return ( 358 |
359 |
{props.foo}
360 |
{context.bar}
361 | 362 | 363 |
364 | ); 365 | } 366 | const SomeMemoComponent = React.memo(SomeComponent); 367 | 368 | SomeComponent.contextTypes = { 369 | bar: PropTypes.string, 370 | }; 371 | 372 | const shallowRenderer = createRenderer(); 373 | const result = shallowRenderer.render(, { 374 | bar: 'BAR', 375 | }); 376 | 377 | expect(result.type).toBe('div'); 378 | expect(result.props.children).toEqual([ 379 |
FOO
, 380 |
BAR
, 381 | , 382 | , 383 | ]); 384 | }); 385 | 386 | it('should shallow render a component returning strings directly from render', () => { 387 | const Text = React.memo(({value}) => value); 388 | 389 | const shallowRenderer = createRenderer(); 390 | const result = shallowRenderer.render(); 391 | expect(result).toEqual('foo'); 392 | }); 393 | 394 | it('should shallow render a component returning numbers directly from render', () => { 395 | const Text = React.memo(({value}) => value); 396 | 397 | const shallowRenderer = createRenderer(); 398 | const result = shallowRenderer.render(); 399 | expect(result).toEqual(10); 400 | }); 401 | 402 | it('should shallow render a fragment', () => { 403 | class SomeComponent extends React.Component { 404 | render() { 405 | return
; 406 | } 407 | } 408 | class Fragment extends React.Component { 409 | render() { 410 | return [
, , ]; 411 | } 412 | } 413 | const shallowRenderer = createRenderer(); 414 | const result = shallowRenderer.render(); 415 | expect(result).toEqual([ 416 |
, 417 | , 418 | , 419 | ]); 420 | }); 421 | 422 | it('should shallow render a React.fragment', () => { 423 | class SomeComponent extends React.Component { 424 | render() { 425 | return
; 426 | } 427 | } 428 | class Fragment extends React.Component { 429 | render() { 430 | return ( 431 | <> 432 |
433 | 434 | 435 | 436 | ); 437 | } 438 | } 439 | const shallowRenderer = createRenderer(); 440 | const result = shallowRenderer.render(); 441 | expect(result).toEqual( 442 | <> 443 |
444 | 445 | 446 | , 447 | ); 448 | }); 449 | 450 | it('should throw for invalid elements', () => { 451 | class SomeComponent extends React.Component { 452 | render() { 453 | return
; 454 | } 455 | } 456 | 457 | const shallowRenderer = createRenderer(); 458 | expect(() => shallowRenderer.render(SomeComponent)).toThrowError( 459 | 'ReactShallowRenderer render(): Invalid component element. Instead of ' + 460 | 'passing a component class, make sure to instantiate it by passing it ' + 461 | 'to React.createElement.', 462 | ); 463 | expect(() => shallowRenderer.render(
)).toThrowError( 464 | 'ReactShallowRenderer render(): Shallow rendering works only with ' + 465 | 'custom components, not primitives (div). Instead of calling ' + 466 | '`.render(el)` and inspecting the rendered output, look at `el.props` ' + 467 | 'directly instead.', 468 | ); 469 | }); 470 | 471 | it('should have shallow unmounting', () => { 472 | const componentWillUnmount = jest.fn(); 473 | 474 | class SomeComponent extends React.Component { 475 | componentWillUnmount = componentWillUnmount; 476 | render() { 477 | return
; 478 | } 479 | } 480 | 481 | const shallowRenderer = createRenderer(); 482 | shallowRenderer.render(); 483 | shallowRenderer.unmount(); 484 | 485 | expect(componentWillUnmount).toBeCalled(); 486 | }); 487 | 488 | it('can shallow render to null', () => { 489 | class SomeComponent extends React.Component { 490 | render() { 491 | return null; 492 | } 493 | } 494 | 495 | const shallowRenderer = createRenderer(); 496 | const result = shallowRenderer.render(); 497 | 498 | expect(result).toBe(null); 499 | }); 500 | 501 | it('can shallow render with a ref', () => { 502 | class SomeComponent extends React.Component { 503 | render() { 504 | return
; 505 | } 506 | } 507 | 508 | const shallowRenderer = createRenderer(); 509 | // Shouldn't crash. 510 | shallowRenderer.render(); 511 | }); 512 | 513 | it('lets you update shallowly rendered components', () => { 514 | class SomeComponent extends React.Component { 515 | state = {clicked: false}; 516 | 517 | onClick = () => { 518 | this.setState({clicked: true}); 519 | }; 520 | 521 | render() { 522 | const className = this.state.clicked ? 'was-clicked' : ''; 523 | 524 | if (this.props.aNew === 'prop') { 525 | return ( 526 | 527 | Test link 528 | 529 | ); 530 | } else { 531 | return ( 532 |
533 | 534 | 535 |
536 | ); 537 | } 538 | } 539 | } 540 | 541 | const shallowRenderer = createRenderer(); 542 | const result = shallowRenderer.render(); 543 | expect(result.type).toBe('div'); 544 | expect(result.props.children).toEqual([ 545 | , 546 | , 547 | ]); 548 | 549 | const updatedResult = shallowRenderer.render(); 550 | expect(updatedResult.type).toBe('a'); 551 | 552 | const mockEvent = {}; 553 | updatedResult.props.onClick(mockEvent); 554 | 555 | const updatedResultCausedByClick = shallowRenderer.getRenderOutput(); 556 | expect(updatedResultCausedByClick.type).toBe('a'); 557 | expect(updatedResultCausedByClick.props.className).toBe('was-clicked'); 558 | }); 559 | 560 | it('can access the mounted component instance', () => { 561 | const SimpleComponent = React.memo( 562 | class SimpleComponent extends React.Component { 563 | someMethod = () => { 564 | return this.props.n; 565 | }; 566 | 567 | render() { 568 | return
{this.props.n}
; 569 | } 570 | }, 571 | ); 572 | 573 | const shallowRenderer = createRenderer(); 574 | shallowRenderer.render(); 575 | expect(shallowRenderer.getMountedInstance().someMethod()).toEqual(5); 576 | }); 577 | 578 | it('can shallowly render components with contextTypes', () => { 579 | const SimpleComponent = React.memo( 580 | class SimpleComponent extends React.Component { 581 | static contextTypes = { 582 | name: PropTypes.string, 583 | }; 584 | 585 | render() { 586 | return
; 587 | } 588 | }, 589 | ); 590 | 591 | const shallowRenderer = createRenderer(); 592 | const result = shallowRenderer.render(); 593 | expect(result).toEqual(
); 594 | }); 595 | 596 | it('passes expected params to legacy component lifecycle methods', () => { 597 | const componentDidUpdateParams = []; 598 | const componentWillReceivePropsParams = []; 599 | const componentWillUpdateParams = []; 600 | const setStateParams = []; 601 | const shouldComponentUpdateParams = []; 602 | 603 | const initialProp = {prop: 'init prop'}; 604 | const initialState = {state: 'init state'}; 605 | const initialContext = {context: 'init context'}; 606 | const updatedState = {state: 'updated state'}; 607 | const updatedProp = {prop: 'updated prop'}; 608 | const updatedContext = {context: 'updated context'}; 609 | 610 | const SimpleComponent = React.memo( 611 | class SimpleComponent extends React.Component { 612 | constructor(props, context) { 613 | super(props, context); 614 | this.state = initialState; 615 | } 616 | static contextTypes = { 617 | context: PropTypes.string, 618 | }; 619 | componentDidUpdate(...args) { 620 | componentDidUpdateParams.push(...args); 621 | } 622 | UNSAFE_componentWillReceiveProps(...args) { 623 | componentWillReceivePropsParams.push(...args); 624 | this.setState((...innerArgs) => { 625 | setStateParams.push(...innerArgs); 626 | return updatedState; 627 | }); 628 | } 629 | UNSAFE_componentWillUpdate(...args) { 630 | componentWillUpdateParams.push(...args); 631 | } 632 | shouldComponentUpdate(...args) { 633 | shouldComponentUpdateParams.push(...args); 634 | return true; 635 | } 636 | render() { 637 | return null; 638 | } 639 | }, 640 | ); 641 | 642 | const shallowRenderer = createRenderer(); 643 | shallowRenderer.render( 644 | React.createElement(SimpleComponent, initialProp), 645 | initialContext, 646 | ); 647 | expect(componentDidUpdateParams).toEqual([]); 648 | expect(componentWillReceivePropsParams).toEqual([]); 649 | expect(componentWillUpdateParams).toEqual([]); 650 | expect(setStateParams).toEqual([]); 651 | expect(shouldComponentUpdateParams).toEqual([]); 652 | 653 | // Lifecycle hooks should be invoked with the correct prev/next params on update. 654 | shallowRenderer.render( 655 | React.createElement(SimpleComponent, updatedProp), 656 | updatedContext, 657 | ); 658 | expect(componentWillReceivePropsParams).toEqual([ 659 | updatedProp, 660 | updatedContext, 661 | ]); 662 | expect(setStateParams).toEqual([initialState, initialProp]); 663 | expect(shouldComponentUpdateParams).toEqual([ 664 | updatedProp, 665 | updatedState, 666 | updatedContext, 667 | ]); 668 | expect(componentWillUpdateParams).toEqual([ 669 | updatedProp, 670 | updatedState, 671 | updatedContext, 672 | ]); 673 | expect(componentDidUpdateParams).toEqual([]); 674 | }); 675 | 676 | it('passes expected params to new component lifecycle methods', () => { 677 | const componentDidUpdateParams = []; 678 | const getDerivedStateFromPropsParams = []; 679 | const shouldComponentUpdateParams = []; 680 | 681 | const initialProp = {prop: 'init prop'}; 682 | const initialState = {state: 'init state'}; 683 | const initialContext = {context: 'init context'}; 684 | const updatedProp = {prop: 'updated prop'}; 685 | const updatedContext = {context: 'updated context'}; 686 | 687 | const SimpleComponent = React.memo( 688 | class SimpleComponent extends React.Component { 689 | constructor(props, context) { 690 | super(props, context); 691 | this.state = initialState; 692 | } 693 | static contextTypes = { 694 | context: PropTypes.string, 695 | }; 696 | componentDidUpdate(...args) { 697 | componentDidUpdateParams.push(...args); 698 | } 699 | static getDerivedStateFromProps(...args) { 700 | getDerivedStateFromPropsParams.push(args); 701 | return null; 702 | } 703 | shouldComponentUpdate(...args) { 704 | shouldComponentUpdateParams.push(...args); 705 | return true; 706 | } 707 | render() { 708 | return null; 709 | } 710 | }, 711 | ); 712 | 713 | const shallowRenderer = createRenderer(); 714 | 715 | // The only lifecycle hook that should be invoked on initial render 716 | // Is the static getDerivedStateFromProps() methods 717 | shallowRenderer.render( 718 | React.createElement(SimpleComponent, initialProp), 719 | initialContext, 720 | ); 721 | expect(getDerivedStateFromPropsParams).toEqual([ 722 | [initialProp, initialState], 723 | ]); 724 | expect(componentDidUpdateParams).toEqual([]); 725 | expect(shouldComponentUpdateParams).toEqual([]); 726 | 727 | // Lifecycle hooks should be invoked with the correct prev/next params on update. 728 | shallowRenderer.render( 729 | React.createElement(SimpleComponent, updatedProp), 730 | updatedContext, 731 | ); 732 | expect(getDerivedStateFromPropsParams).toEqual([ 733 | [initialProp, initialState], 734 | [updatedProp, initialState], 735 | ]); 736 | expect(shouldComponentUpdateParams).toEqual([ 737 | updatedProp, 738 | initialState, 739 | updatedContext, 740 | ]); 741 | expect(componentDidUpdateParams).toEqual([]); 742 | }); 743 | 744 | it('can shallowly render components with ref as function', () => { 745 | const SimpleComponent = React.memo( 746 | class SimpleComponent extends React.Component { 747 | state = {clicked: false}; 748 | 749 | handleUserClick = () => { 750 | this.setState({clicked: true}); 751 | }; 752 | 753 | render() { 754 | return ( 755 |
{}} 757 | onClick={this.handleUserClick} 758 | className={this.state.clicked ? 'clicked' : ''} 759 | /> 760 | ); 761 | } 762 | }, 763 | ); 764 | 765 | const shallowRenderer = createRenderer(); 766 | shallowRenderer.render(); 767 | let result = shallowRenderer.getRenderOutput(); 768 | expect(result.type).toEqual('div'); 769 | expect(result.props.className).toEqual(''); 770 | result.props.onClick(); 771 | 772 | result = shallowRenderer.getRenderOutput(); 773 | expect(result.type).toEqual('div'); 774 | expect(result.props.className).toEqual('clicked'); 775 | }); 776 | 777 | it('can initialize state via static getDerivedStateFromProps', () => { 778 | const SimpleComponent = React.memo( 779 | class SimpleComponent extends React.Component { 780 | state = { 781 | count: 1, 782 | }; 783 | 784 | static getDerivedStateFromProps(props, prevState) { 785 | return { 786 | count: prevState.count + props.incrementBy, 787 | other: 'foobar', 788 | }; 789 | } 790 | 791 | render() { 792 | return ( 793 |
{`count:${this.state.count}, other:${this.state.other}`}
794 | ); 795 | } 796 | }, 797 | ); 798 | 799 | const shallowRenderer = createRenderer(); 800 | const result = shallowRenderer.render(); 801 | expect(result).toEqual(
count:3, other:foobar
); 802 | }); 803 | 804 | it('can setState in componentWillMount when shallow rendering', () => { 805 | const SimpleComponent = React.memo( 806 | class SimpleComponent extends React.Component { 807 | UNSAFE_componentWillMount() { 808 | this.setState({groovy: 'doovy'}); 809 | } 810 | 811 | render() { 812 | return
{this.state.groovy}
; 813 | } 814 | }, 815 | ); 816 | 817 | const shallowRenderer = createRenderer(); 818 | const result = shallowRenderer.render(); 819 | expect(result).toEqual(
doovy
); 820 | }); 821 | 822 | it('can setState in componentWillMount repeatedly when shallow rendering', () => { 823 | const SimpleComponent = React.memo( 824 | class SimpleComponent extends React.Component { 825 | state = { 826 | separator: '-', 827 | }; 828 | 829 | UNSAFE_componentWillMount() { 830 | this.setState({groovy: 'doovy'}); 831 | this.setState({doovy: 'groovy'}); 832 | } 833 | 834 | render() { 835 | const {groovy, doovy, separator} = this.state; 836 | 837 | return
{`${groovy}${separator}${doovy}`}
; 838 | } 839 | }, 840 | ); 841 | 842 | const shallowRenderer = createRenderer(); 843 | const result = shallowRenderer.render(); 844 | expect(result).toEqual(
doovy-groovy
); 845 | }); 846 | 847 | it('can setState in componentWillMount with an updater function repeatedly when shallow rendering', () => { 848 | const SimpleComponent = React.memo( 849 | class SimpleComponent extends React.Component { 850 | state = { 851 | separator: '-', 852 | }; 853 | 854 | UNSAFE_componentWillMount() { 855 | this.setState(state => ({groovy: 'doovy'})); 856 | this.setState(state => ({doovy: state.groovy})); 857 | } 858 | 859 | render() { 860 | const {groovy, doovy, separator} = this.state; 861 | 862 | return
{`${groovy}${separator}${doovy}`}
; 863 | } 864 | }, 865 | ); 866 | 867 | const shallowRenderer = createRenderer(); 868 | const result = shallowRenderer.render(); 869 | expect(result).toEqual(
doovy-doovy
); 870 | }); 871 | 872 | it('can setState in componentWillReceiveProps when shallow rendering', () => { 873 | const SimpleComponent = React.memo( 874 | class SimpleComponent extends React.Component { 875 | state = {count: 0}; 876 | 877 | UNSAFE_componentWillReceiveProps(nextProps) { 878 | if (nextProps.updateState) { 879 | this.setState({count: 1}); 880 | } 881 | } 882 | 883 | render() { 884 | return
{this.state.count}
; 885 | } 886 | }, 887 | ); 888 | 889 | const shallowRenderer = createRenderer(); 890 | let result = shallowRenderer.render( 891 | , 892 | ); 893 | expect(result.props.children).toEqual(0); 894 | 895 | result = shallowRenderer.render(); 896 | expect(result.props.children).toEqual(1); 897 | }); 898 | 899 | it('can update state with static getDerivedStateFromProps when shallow rendering', () => { 900 | const SimpleComponent = React.memo( 901 | class SimpleComponent extends React.Component { 902 | state = {count: 1}; 903 | 904 | static getDerivedStateFromProps(nextProps, prevState) { 905 | if (nextProps.updateState) { 906 | return {count: nextProps.incrementBy + prevState.count}; 907 | } 908 | 909 | return null; 910 | } 911 | 912 | render() { 913 | return
{this.state.count}
; 914 | } 915 | }, 916 | ); 917 | 918 | const shallowRenderer = createRenderer(); 919 | let result = shallowRenderer.render( 920 | , 921 | ); 922 | expect(result.props.children).toEqual(1); 923 | 924 | result = shallowRenderer.render( 925 | , 926 | ); 927 | expect(result.props.children).toEqual(3); 928 | 929 | result = shallowRenderer.render( 930 | , 931 | ); 932 | expect(result.props.children).toEqual(3); 933 | }); 934 | 935 | it('should not override state with stale values if prevState is spread within getDerivedStateFromProps', () => { 936 | const SimpleComponent = React.memo( 937 | class SimpleComponent extends React.Component { 938 | state = {value: 0}; 939 | 940 | static getDerivedStateFromProps(nextProps, prevState) { 941 | return {...prevState}; 942 | } 943 | 944 | updateState = () => { 945 | this.setState(state => ({value: state.value + 1})); 946 | }; 947 | 948 | render() { 949 | return
{`value:${this.state.value}`}
; 950 | } 951 | }, 952 | ); 953 | 954 | const shallowRenderer = createRenderer(); 955 | let result = shallowRenderer.render(); 956 | expect(result).toEqual(
value:0
); 957 | 958 | let instance = shallowRenderer.getMountedInstance(); 959 | instance.updateState(); 960 | result = shallowRenderer.getRenderOutput(); 961 | expect(result).toEqual(
value:1
); 962 | }); 963 | 964 | it('should pass previous state to shouldComponentUpdate even with getDerivedStateFromProps', () => { 965 | const SimpleComponent = React.memo( 966 | class SimpleComponent extends React.Component { 967 | constructor(props) { 968 | super(props); 969 | this.state = { 970 | value: props.value, 971 | }; 972 | } 973 | 974 | static getDerivedStateFromProps(nextProps, prevState) { 975 | if (nextProps.value === prevState.value) { 976 | return null; 977 | } 978 | return {value: nextProps.value}; 979 | } 980 | 981 | shouldComponentUpdate(nextProps, nextState) { 982 | return nextState.value !== this.state.value; 983 | } 984 | 985 | render() { 986 | return
{`value:${this.state.value}`}
; 987 | } 988 | }, 989 | ); 990 | 991 | const shallowRenderer = createRenderer(); 992 | const initialResult = shallowRenderer.render( 993 | , 994 | ); 995 | expect(initialResult).toEqual(
value:initial
); 996 | const updatedResult = shallowRenderer.render( 997 | , 998 | ); 999 | expect(updatedResult).toEqual(
value:updated
); 1000 | }); 1001 | 1002 | it('can setState with an updater function', () => { 1003 | let instance; 1004 | 1005 | const SimpleComponent = React.memo( 1006 | class SimpleComponent extends React.Component { 1007 | state = { 1008 | counter: 0, 1009 | }; 1010 | 1011 | render() { 1012 | instance = this; 1013 | return ( 1014 | 1017 | ); 1018 | } 1019 | }, 1020 | ); 1021 | 1022 | const shallowRenderer = createRenderer(); 1023 | let result = shallowRenderer.render(); 1024 | expect(result.props.children).toEqual(0); 1025 | 1026 | instance.setState((state, props) => { 1027 | return {counter: props.defaultCount + 1}; 1028 | }); 1029 | 1030 | result = shallowRenderer.getRenderOutput(); 1031 | expect(result.props.children).toEqual(2); 1032 | }); 1033 | 1034 | it('can access component instance from setState updater function', done => { 1035 | let instance; 1036 | 1037 | const SimpleComponent = React.memo( 1038 | class SimpleComponent extends React.Component { 1039 | state = {}; 1040 | 1041 | render() { 1042 | instance = this; 1043 | return null; 1044 | } 1045 | }, 1046 | ); 1047 | 1048 | const shallowRenderer = createRenderer(); 1049 | shallowRenderer.render(); 1050 | 1051 | instance.setState(function updater(state, props) { 1052 | expect(this).toBe(instance); 1053 | done(); 1054 | }); 1055 | }); 1056 | 1057 | it('can setState with a callback', () => { 1058 | let instance; 1059 | 1060 | const SimpleComponent = React.memo( 1061 | class SimpleComponent extends React.Component { 1062 | state = { 1063 | counter: 0, 1064 | }; 1065 | render() { 1066 | instance = this; 1067 | return

{this.state.counter}

; 1068 | } 1069 | }, 1070 | ); 1071 | 1072 | const shallowRenderer = createRenderer(); 1073 | const result = shallowRenderer.render(); 1074 | expect(result.props.children).toBe(0); 1075 | 1076 | const callback = jest.fn(function() { 1077 | expect(this).toBe(instance); 1078 | }); 1079 | 1080 | instance.setState({counter: 1}, callback); 1081 | 1082 | const updated = shallowRenderer.getRenderOutput(); 1083 | expect(updated.props.children).toBe(1); 1084 | expect(callback).toHaveBeenCalled(); 1085 | }); 1086 | 1087 | it('can replaceState with a callback', () => { 1088 | let instance; 1089 | 1090 | const SimpleComponent = React.memo( 1091 | class SimpleComponent extends React.Component { 1092 | state = { 1093 | counter: 0, 1094 | }; 1095 | render() { 1096 | instance = this; 1097 | return

{this.state.counter}

; 1098 | } 1099 | }, 1100 | ); 1101 | 1102 | const shallowRenderer = createRenderer(); 1103 | const result = shallowRenderer.render(); 1104 | expect(result.props.children).toBe(0); 1105 | 1106 | const callback = jest.fn(function() { 1107 | expect(this).toBe(instance); 1108 | }); 1109 | 1110 | // No longer a public API, but we can test that it works internally by 1111 | // reaching into the updater. 1112 | shallowRenderer._updater.enqueueReplaceState( 1113 | instance, 1114 | {counter: 1}, 1115 | callback, 1116 | ); 1117 | 1118 | const updated = shallowRenderer.getRenderOutput(); 1119 | expect(updated.props.children).toBe(1); 1120 | expect(callback).toHaveBeenCalled(); 1121 | }); 1122 | 1123 | it('can forceUpdate with a callback', () => { 1124 | let instance; 1125 | 1126 | const SimpleComponent = React.memo( 1127 | class SimpleComponent extends React.Component { 1128 | state = { 1129 | counter: 0, 1130 | }; 1131 | render() { 1132 | instance = this; 1133 | return

{this.state.counter}

; 1134 | } 1135 | }, 1136 | ); 1137 | 1138 | const shallowRenderer = createRenderer(); 1139 | const result = shallowRenderer.render(); 1140 | expect(result.props.children).toBe(0); 1141 | 1142 | const callback = jest.fn(function() { 1143 | expect(this).toBe(instance); 1144 | }); 1145 | 1146 | instance.forceUpdate(callback); 1147 | 1148 | const updated = shallowRenderer.getRenderOutput(); 1149 | expect(updated.props.children).toBe(0); 1150 | expect(callback).toHaveBeenCalled(); 1151 | }); 1152 | 1153 | it('can pass context when shallowly rendering', () => { 1154 | const SimpleComponent = React.memo( 1155 | class SimpleComponent extends React.Component { 1156 | static contextTypes = { 1157 | name: PropTypes.string, 1158 | }; 1159 | 1160 | render() { 1161 | return
{this.context.name}
; 1162 | } 1163 | }, 1164 | ); 1165 | 1166 | const shallowRenderer = createRenderer(); 1167 | const result = shallowRenderer.render(, { 1168 | name: 'foo', 1169 | }); 1170 | expect(result).toEqual(
foo
); 1171 | }); 1172 | 1173 | it('should track context across updates', () => { 1174 | const SimpleComponent = React.memo( 1175 | class SimpleComponent extends React.Component { 1176 | static contextTypes = { 1177 | foo: PropTypes.string, 1178 | }; 1179 | 1180 | state = { 1181 | bar: 'bar', 1182 | }; 1183 | 1184 | render() { 1185 | return
{`${this.context.foo}:${this.state.bar}`}
; 1186 | } 1187 | }, 1188 | ); 1189 | 1190 | const shallowRenderer = createRenderer(); 1191 | let result = shallowRenderer.render(, { 1192 | foo: 'foo', 1193 | }); 1194 | expect(result).toEqual(
foo:bar
); 1195 | 1196 | const instance = shallowRenderer.getMountedInstance(); 1197 | instance.setState({bar: 'baz'}); 1198 | 1199 | result = shallowRenderer.getRenderOutput(); 1200 | expect(result).toEqual(
foo:baz
); 1201 | }); 1202 | 1203 | it('should filter context by contextTypes', () => { 1204 | const SimpleComponent = React.memo( 1205 | class SimpleComponent extends React.Component { 1206 | static contextTypes = { 1207 | foo: PropTypes.string, 1208 | }; 1209 | render() { 1210 | return
{`${this.context.foo}:${this.context.bar}`}
; 1211 | } 1212 | }, 1213 | ); 1214 | 1215 | const shallowRenderer = createRenderer(); 1216 | let result = shallowRenderer.render(, { 1217 | foo: 'foo', 1218 | bar: 'bar', 1219 | }); 1220 | expect(result).toEqual(
foo:undefined
); 1221 | }); 1222 | 1223 | it('can fail context when shallowly rendering', () => { 1224 | const SimpleComponent = React.memo( 1225 | class SimpleComponent extends React.Component { 1226 | static contextTypes = { 1227 | name: PropTypes.string.isRequired, 1228 | }; 1229 | 1230 | render() { 1231 | return
{this.context.name}
; 1232 | } 1233 | }, 1234 | ); 1235 | 1236 | const shallowRenderer = createRenderer(); 1237 | expect(() => shallowRenderer.render()).toErrorDev( 1238 | 'Warning: Failed context type: The context `name` is marked as ' + 1239 | 'required in `SimpleComponent`, but its value is `undefined`.\n' + 1240 | ' in SimpleComponent (at **)', 1241 | ); 1242 | }); 1243 | 1244 | it('should warn about propTypes (but only once)', () => { 1245 | const SimpleComponent = React.memo( 1246 | class SimpleComponent extends React.Component { 1247 | static propTypes = { 1248 | name: PropTypes.string.isRequired, 1249 | }; 1250 | 1251 | render() { 1252 | return React.createElement('div', null, this.props.name); 1253 | } 1254 | }, 1255 | ); 1256 | 1257 | const shallowRenderer = createRenderer(); 1258 | expect(() => 1259 | shallowRenderer.render(React.createElement(SimpleComponent, {name: 123})), 1260 | ).toErrorDev( 1261 | 'Warning: Failed prop type: Invalid prop `name` of type `number` ' + 1262 | 'supplied to `SimpleComponent`, expected `string`.\n' + 1263 | ' in SimpleComponent', 1264 | ); 1265 | }); 1266 | 1267 | it('should enable rendering of cloned element', () => { 1268 | const SimpleComponent = React.memo( 1269 | class SimpleComponent extends React.Component { 1270 | constructor(props) { 1271 | super(props); 1272 | 1273 | this.state = { 1274 | bar: 'bar', 1275 | }; 1276 | } 1277 | 1278 | render() { 1279 | return
{`${this.props.foo}:${this.state.bar}`}
; 1280 | } 1281 | }, 1282 | ); 1283 | 1284 | const shallowRenderer = createRenderer(); 1285 | const el = ; 1286 | let result = shallowRenderer.render(el); 1287 | expect(result).toEqual(
foo:bar
); 1288 | 1289 | const cloned = React.cloneElement(el, {foo: 'baz'}); 1290 | result = shallowRenderer.render(cloned); 1291 | expect(result).toEqual(
baz:bar
); 1292 | }); 1293 | 1294 | it('this.state should be updated on setState callback inside componentWillMount', () => { 1295 | let stateSuccessfullyUpdated = false; 1296 | 1297 | const Component = React.memo( 1298 | class Component extends React.Component { 1299 | constructor(props, context) { 1300 | super(props, context); 1301 | this.state = { 1302 | hasUpdatedState: false, 1303 | }; 1304 | } 1305 | 1306 | UNSAFE_componentWillMount() { 1307 | this.setState( 1308 | {hasUpdatedState: true}, 1309 | () => (stateSuccessfullyUpdated = this.state.hasUpdatedState), 1310 | ); 1311 | } 1312 | 1313 | render() { 1314 | return
{this.props.children}
; 1315 | } 1316 | }, 1317 | ); 1318 | 1319 | const shallowRenderer = createRenderer(); 1320 | shallowRenderer.render(); 1321 | expect(stateSuccessfullyUpdated).toBe(true); 1322 | }); 1323 | 1324 | it('should handle multiple callbacks', () => { 1325 | const mockFn = jest.fn(); 1326 | const shallowRenderer = createRenderer(); 1327 | 1328 | const Component = React.memo( 1329 | class Component extends React.Component { 1330 | constructor(props, context) { 1331 | super(props, context); 1332 | this.state = { 1333 | foo: 'foo', 1334 | }; 1335 | } 1336 | 1337 | UNSAFE_componentWillMount() { 1338 | this.setState({foo: 'bar'}, () => mockFn()); 1339 | this.setState({foo: 'foobar'}, () => mockFn()); 1340 | } 1341 | 1342 | render() { 1343 | return
{this.state.foo}
; 1344 | } 1345 | }, 1346 | ); 1347 | 1348 | shallowRenderer.render(); 1349 | 1350 | expect(mockFn).toHaveBeenCalledTimes(2); 1351 | 1352 | // Ensure the callback queue is cleared after the callbacks are invoked 1353 | const mountedInstance = shallowRenderer.getMountedInstance(); 1354 | mountedInstance.setState({foo: 'bar'}, () => mockFn()); 1355 | expect(mockFn).toHaveBeenCalledTimes(3); 1356 | }); 1357 | 1358 | it('should call the setState callback even if shouldComponentUpdate = false', done => { 1359 | const mockFn = jest.fn().mockReturnValue(false); 1360 | 1361 | const Component = React.memo( 1362 | class Component extends React.Component { 1363 | constructor(props, context) { 1364 | super(props, context); 1365 | this.state = { 1366 | hasUpdatedState: false, 1367 | }; 1368 | } 1369 | 1370 | shouldComponentUpdate() { 1371 | return mockFn(); 1372 | } 1373 | 1374 | render() { 1375 | return
{this.state.hasUpdatedState}
; 1376 | } 1377 | }, 1378 | ); 1379 | 1380 | const shallowRenderer = createRenderer(); 1381 | shallowRenderer.render(); 1382 | 1383 | const mountedInstance = shallowRenderer.getMountedInstance(); 1384 | mountedInstance.setState({hasUpdatedState: true}, () => { 1385 | expect(mockFn).toBeCalled(); 1386 | expect(mountedInstance.state.hasUpdatedState).toBe(true); 1387 | done(); 1388 | }); 1389 | }); 1390 | 1391 | it('throws usefully when rendering badly-typed elements', () => { 1392 | const shallowRenderer = createRenderer(); 1393 | 1394 | const renderAndVerifyWarningAndError = (Component, typeString) => { 1395 | expect(() => { 1396 | expect(() => shallowRenderer.render()).toErrorDev( 1397 | 'React.createElement: type is invalid -- expected a string ' + 1398 | '(for built-in components) or a class/function (for composite components) ' + 1399 | `but got: ${typeString}.`, 1400 | ); 1401 | }).toThrowError( 1402 | 'ReactShallowRenderer render(): Shallow rendering works only with custom ' + 1403 | `components, but the provided element type was \`${typeString}\`.`, 1404 | ); 1405 | }; 1406 | 1407 | renderAndVerifyWarningAndError(undefined, 'undefined'); 1408 | renderAndVerifyWarningAndError(null, 'null'); 1409 | renderAndVerifyWarningAndError([], 'array'); 1410 | renderAndVerifyWarningAndError({}, 'object'); 1411 | }); 1412 | 1413 | it('should have initial state of null if not defined', () => { 1414 | const SomeComponent = React.memo( 1415 | class SomeComponent extends React.Component { 1416 | render() { 1417 | return ; 1418 | } 1419 | }, 1420 | ); 1421 | 1422 | const shallowRenderer = createRenderer(); 1423 | shallowRenderer.render(); 1424 | 1425 | expect(shallowRenderer.getMountedInstance().state).toBeNull(); 1426 | }); 1427 | 1428 | it('should invoke both deprecated and new lifecycles if both are present', () => { 1429 | const log = []; 1430 | 1431 | const Component = React.memo( 1432 | class Component extends React.Component { 1433 | componentWillMount() { 1434 | log.push('componentWillMount'); 1435 | } 1436 | componentWillReceiveProps() { 1437 | log.push('componentWillReceiveProps'); 1438 | } 1439 | componentWillUpdate() { 1440 | log.push('componentWillUpdate'); 1441 | } 1442 | UNSAFE_componentWillMount() { 1443 | log.push('UNSAFE_componentWillMount'); 1444 | } 1445 | UNSAFE_componentWillReceiveProps() { 1446 | log.push('UNSAFE_componentWillReceiveProps'); 1447 | } 1448 | UNSAFE_componentWillUpdate() { 1449 | log.push('UNSAFE_componentWillUpdate'); 1450 | } 1451 | render() { 1452 | return null; 1453 | } 1454 | }, 1455 | ); 1456 | 1457 | const shallowRenderer = createRenderer(); 1458 | shallowRenderer.render(); 1459 | expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']); 1460 | 1461 | log.length = 0; 1462 | 1463 | shallowRenderer.render(); 1464 | expect(log).toEqual([ 1465 | 'componentWillReceiveProps', 1466 | 'UNSAFE_componentWillReceiveProps', 1467 | 'componentWillUpdate', 1468 | 'UNSAFE_componentWillUpdate', 1469 | ]); 1470 | }); 1471 | 1472 | it('should stop the update when setState returns null or undefined', () => { 1473 | const log = []; 1474 | let instance; 1475 | const Component = React.memo( 1476 | class Component extends React.Component { 1477 | constructor(props) { 1478 | super(props); 1479 | this.state = { 1480 | count: 0, 1481 | }; 1482 | } 1483 | render() { 1484 | log.push('render'); 1485 | instance = this; 1486 | return null; 1487 | } 1488 | }, 1489 | ); 1490 | const shallowRenderer = createRenderer(); 1491 | shallowRenderer.render(); 1492 | log.length = 0; 1493 | instance.setState(() => null); 1494 | instance.setState(() => undefined); 1495 | instance.setState(null); 1496 | instance.setState(undefined); 1497 | expect(log).toEqual([]); 1498 | instance.setState(state => ({count: state.count + 1})); 1499 | expect(log).toEqual(['render']); 1500 | }); 1501 | 1502 | it('should not get this in a function component', () => { 1503 | const logs = []; 1504 | const Foo = React.memo(function Foo() { 1505 | logs.push(this); 1506 | return
foo
; 1507 | }); 1508 | const shallowRenderer = createRenderer(); 1509 | shallowRenderer.render(); 1510 | expect(logs).toEqual([undefined]); 1511 | }); 1512 | }); 1513 | -------------------------------------------------------------------------------- /src/shared/ReactLazyComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | import {error} from './consoleWithStackDev'; 11 | 12 | export const Uninitialized = -1; 13 | export const Pending = 0; 14 | export const Resolved = 1; 15 | export const Rejected = 2; 16 | 17 | export function refineResolvedLazyComponent(lazyComponent) { 18 | return lazyComponent._status === Resolved ? lazyComponent._result : null; 19 | } 20 | 21 | export function initializeLazyComponentType(lazyComponent) { 22 | if (lazyComponent._status === Uninitialized) { 23 | lazyComponent._status = Pending; 24 | const ctor = lazyComponent._ctor; 25 | const thenable = ctor(); 26 | lazyComponent._result = thenable; 27 | thenable.then( 28 | moduleObject => { 29 | if (lazyComponent._status === Pending) { 30 | const defaultExport = moduleObject.default; 31 | if (process.env.NODE_ENV !== 'production') { 32 | if (defaultExport === undefined) { 33 | error( 34 | 'lazy: Expected the result of a dynamic import() call. ' + 35 | 'Instead received: %s\n\nYour code should look like: \n ' + 36 | "const MyComponent = lazy(() => import('./MyComponent'))", 37 | moduleObject, 38 | ); 39 | } 40 | } 41 | lazyComponent._status = Resolved; 42 | lazyComponent._result = defaultExport; 43 | } 44 | }, 45 | error => { 46 | if (lazyComponent._status === Pending) { 47 | lazyComponent._status = Rejected; 48 | lazyComponent._result = error; 49 | } 50 | }, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/shared/ReactSharedInternals.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import React from 'react'; 9 | 10 | const ReactSharedInternals = 11 | React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED; 12 | 13 | const hasOwnProperty = Object.prototype.hasOwnProperty; 14 | 15 | // Prevent newer renderers from RTE when used with older react package versions. 16 | // Current owner and dispatcher used to share the same ref, 17 | // but PR #14548 split them out to better support the react-debug-tools package. 18 | if (!hasOwnProperty.call(ReactSharedInternals, 'ReactCurrentDispatcher')) { 19 | ReactSharedInternals.ReactCurrentDispatcher = { 20 | current: null, 21 | }; 22 | } 23 | if (!hasOwnProperty.call(ReactSharedInternals, 'ReactCurrentBatchConfig')) { 24 | ReactSharedInternals.ReactCurrentBatchConfig = { 25 | suspense: null, 26 | }; 27 | } 28 | 29 | export default ReactSharedInternals; 30 | -------------------------------------------------------------------------------- /src/shared/ReactSymbols.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | // The Symbol used to tag the ReactElement-like types. If there is no native Symbol 11 | // nor polyfill, then a plain number is used for performance. 12 | const hasSymbol = typeof Symbol === 'function' && Symbol.for; 13 | 14 | export const REACT_ELEMENT_TYPE = hasSymbol 15 | ? Symbol.for('react.element') 16 | : 0xeac7; 17 | export const REACT_PORTAL_TYPE = hasSymbol 18 | ? Symbol.for('react.portal') 19 | : 0xeaca; 20 | export const REACT_FRAGMENT_TYPE = hasSymbol 21 | ? Symbol.for('react.fragment') 22 | : 0xeacb; 23 | export const REACT_STRICT_MODE_TYPE = hasSymbol 24 | ? Symbol.for('react.strict_mode') 25 | : 0xeacc; 26 | export const REACT_PROFILER_TYPE = hasSymbol 27 | ? Symbol.for('react.profiler') 28 | : 0xead2; 29 | export const REACT_PROVIDER_TYPE = hasSymbol 30 | ? Symbol.for('react.provider') 31 | : 0xeacd; 32 | export const REACT_CONTEXT_TYPE = hasSymbol 33 | ? Symbol.for('react.context') 34 | : 0xeace; 35 | // TODO: We don't use AsyncMode or ConcurrentMode anymore. They were temporary 36 | // (unstable) APIs that have been removed. Can we remove the symbols? 37 | export const REACT_ASYNC_MODE_TYPE = hasSymbol 38 | ? Symbol.for('react.async_mode') 39 | : 0xeacf; 40 | export const REACT_CONCURRENT_MODE_TYPE = hasSymbol 41 | ? Symbol.for('react.concurrent_mode') 42 | : 0xeacf; 43 | export const REACT_FORWARD_REF_TYPE = hasSymbol 44 | ? Symbol.for('react.forward_ref') 45 | : 0xead0; 46 | export const REACT_SUSPENSE_TYPE = hasSymbol 47 | ? Symbol.for('react.suspense') 48 | : 0xead1; 49 | export const REACT_SUSPENSE_LIST_TYPE = hasSymbol 50 | ? Symbol.for('react.suspense_list') 51 | : 0xead8; 52 | export const REACT_MEMO_TYPE = hasSymbol ? Symbol.for('react.memo') : 0xead3; 53 | export const REACT_LAZY_TYPE = hasSymbol ? Symbol.for('react.lazy') : 0xead4; 54 | export const REACT_CHUNK_TYPE = hasSymbol ? Symbol.for('react.chunk') : 0xead9; 55 | export const REACT_FUNDAMENTAL_TYPE = hasSymbol 56 | ? Symbol.for('react.fundamental') 57 | : 0xead5; 58 | export const REACT_RESPONDER_TYPE = hasSymbol 59 | ? Symbol.for('react.responder') 60 | : 0xead6; 61 | export const REACT_SCOPE_TYPE = hasSymbol ? Symbol.for('react.scope') : 0xead7; 62 | 63 | const MAYBE_ITERATOR_SYMBOL = typeof Symbol === 'function' && Symbol.iterator; 64 | const FAUX_ITERATOR_SYMBOL = '@@iterator'; 65 | 66 | export function getIteratorFn(maybeIterable) { 67 | if (maybeIterable === null || typeof maybeIterable !== 'object') { 68 | return null; 69 | } 70 | const maybeIterator = 71 | (MAYBE_ITERATOR_SYMBOL && maybeIterable[MAYBE_ITERATOR_SYMBOL]) || 72 | maybeIterable[FAUX_ITERATOR_SYMBOL]; 73 | if (typeof maybeIterator === 'function') { 74 | return maybeIterator; 75 | } 76 | return null; 77 | } 78 | -------------------------------------------------------------------------------- /src/shared/checkPropTypes.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import {error as consoleError} from './consoleWithStackDev'; 10 | 11 | let loggedTypeFailures = {}; 12 | 13 | export default function checkPropTypes( 14 | typeSpecs, 15 | values, 16 | location, 17 | componentName, 18 | ) { 19 | if (process.env.NODE_ENV !== 'production') { 20 | let has = Function.call.bind(Object.prototype.hasOwnProperty); 21 | for (let typeSpecName in typeSpecs) { 22 | if (has(typeSpecs, typeSpecName)) { 23 | let error; 24 | // Prop type validation may throw. In case they do, we don't want to 25 | // fail the render phase where it didn't fail before. So we log it. 26 | // After these have been cleaned up, we'll let them throw. 27 | try { 28 | // This is intentionally an invariant that gets caught. It's the same 29 | // behavior as without this statement except with a better message. 30 | if (typeof typeSpecs[typeSpecName] !== 'function') { 31 | let err = Error( 32 | (componentName || 'React class') + 33 | ': ' + 34 | location + 35 | ' type `' + 36 | typeSpecName + 37 | '` is invalid; ' + 38 | 'it must be a function, usually from the `prop-types` package, but received `' + 39 | typeof typeSpecs[typeSpecName] + 40 | '`.' + 41 | 'This often happens because of typos such as `PropTypes.function` instead of `PropTypes.func`.', 42 | ); 43 | err.name = 'Invariant Violation'; 44 | throw err; 45 | } 46 | error = typeSpecs[typeSpecName]( 47 | values, 48 | typeSpecName, 49 | componentName, 50 | location, 51 | null, 52 | 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED', 53 | ); 54 | } catch (ex) { 55 | error = ex; 56 | } 57 | if (error && !(error instanceof Error)) { 58 | consoleError( 59 | '%s: type specification of %s' + 60 | ' `%s` is invalid; the type checker ' + 61 | 'function must return `null` or an `Error` but returned a %s. ' + 62 | 'You may have forgotten to pass an argument to the type checker ' + 63 | 'creator (arrayOf, instanceOf, objectOf, oneOf, oneOfType, and ' + 64 | 'shape all require an argument).', 65 | componentName || 'React class', 66 | location, 67 | typeSpecName, 68 | typeof error, 69 | ); 70 | } 71 | if (error instanceof Error && !(error.message in loggedTypeFailures)) { 72 | // Only monitor this failure once because there tends to be a lot of the 73 | // same error. 74 | loggedTypeFailures[error.message] = true; 75 | consoleError('Failed %s type: %s', location, error.message); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/shared/consoleWithStackDev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | */ 7 | 8 | import ReactSharedInternals from './ReactSharedInternals'; 9 | 10 | export function warn(format, ...args) { 11 | if (process.env.NODE_ENV !== 'production') { 12 | printWarning('warn', format, args); 13 | } 14 | } 15 | 16 | export function error(format, ...args) { 17 | if (process.env.NODE_ENV !== 'production') { 18 | printWarning('error', format, args); 19 | } 20 | } 21 | 22 | function printWarning(level, format, args) { 23 | if (process.env.NODE_ENV !== 'production') { 24 | const hasExistingStack = 25 | args.length > 0 && 26 | typeof args[args.length - 1] === 'string' && 27 | args[args.length - 1].indexOf('\n in') === 0; 28 | 29 | if (!hasExistingStack) { 30 | const ReactDebugCurrentFrame = 31 | ReactSharedInternals.ReactDebugCurrentFrame; 32 | const stack = ReactDebugCurrentFrame.getStackAddendum(); 33 | if (stack !== '') { 34 | format += '%s'; 35 | args = args.concat([stack]); 36 | } 37 | } 38 | 39 | const argsWithFormat = args.map(item => '' + item); 40 | // Careful: RN currently depends on this prefix 41 | argsWithFormat.unshift('Warning: ' + format); 42 | // We intentionally don't use spread (or .apply) directly because it 43 | // breaks IE9: https://github.com/facebook/react/issues/13610 44 | // eslint-disable-next-line no-console 45 | Function.prototype.apply.call(console[level], console, argsWithFormat); 46 | 47 | try { 48 | // --- Welcome to debugging React --- 49 | // This error was thrown as a convenience so that you can use this stack 50 | // to find the callsite that caused this warning to fire. 51 | let argIndex = 0; 52 | const message = 53 | 'Warning: ' + format.replace(/%s/g, () => args[argIndex++]); 54 | throw new Error(message); 55 | } catch (x) {} 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/shared/describeComponentFrame.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | const BEFORE_SLASH_RE = /^(.*)[\\\/]/; 11 | 12 | export default function(name, source, ownerName) { 13 | let sourceInfo = ''; 14 | if (source) { 15 | let path = source.fileName; 16 | let fileName = path.replace(BEFORE_SLASH_RE, ''); 17 | if (process.env.NODE_ENV !== 'production') { 18 | // In DEV, include code for a common special case: 19 | // prefer "folder/index.js" instead of just "index.js". 20 | if (/^index\./.test(fileName)) { 21 | const match = path.match(BEFORE_SLASH_RE); 22 | if (match) { 23 | const pathBeforeSlash = match[1]; 24 | if (pathBeforeSlash) { 25 | const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, ''); 26 | fileName = folderName + '/' + fileName; 27 | } 28 | } 29 | } 30 | } 31 | sourceInfo = ' (at ' + fileName + ':' + source.lineNumber + ')'; 32 | } else if (ownerName) { 33 | sourceInfo = ' (created by ' + ownerName + ')'; 34 | } 35 | return '\n in ' + (name || 'Unknown') + sourceInfo; 36 | } 37 | -------------------------------------------------------------------------------- /src/shared/getComponentName.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | import {error} from './consoleWithStackDev'; 11 | import { 12 | REACT_CONTEXT_TYPE, 13 | REACT_FORWARD_REF_TYPE, 14 | REACT_FRAGMENT_TYPE, 15 | REACT_PORTAL_TYPE, 16 | REACT_MEMO_TYPE, 17 | REACT_PROFILER_TYPE, 18 | REACT_PROVIDER_TYPE, 19 | REACT_STRICT_MODE_TYPE, 20 | REACT_SUSPENSE_TYPE, 21 | REACT_SUSPENSE_LIST_TYPE, 22 | REACT_LAZY_TYPE, 23 | REACT_CHUNK_TYPE, 24 | } from './ReactSymbols'; 25 | import {refineResolvedLazyComponent} from './ReactLazyComponent'; 26 | 27 | function getWrappedName(outerType, innerType, wrapperName) { 28 | const functionName = innerType.displayName || innerType.name || ''; 29 | return ( 30 | outerType.displayName || 31 | (functionName !== '' ? `${wrapperName}(${functionName})` : wrapperName) 32 | ); 33 | } 34 | 35 | function getComponentName(type) { 36 | if (type == null) { 37 | // Host root, text node or just invalid type. 38 | return null; 39 | } 40 | if (process.env.NODE_ENV !== 'production') { 41 | if (typeof type.tag === 'number') { 42 | error( 43 | 'Received an unexpected object in getComponentName(). ' + 44 | 'This is likely a bug in React. Please file an issue.', 45 | ); 46 | } 47 | } 48 | if (typeof type === 'function') { 49 | return type.displayName || type.name || null; 50 | } 51 | if (typeof type === 'string') { 52 | return type; 53 | } 54 | switch (type) { 55 | case REACT_FRAGMENT_TYPE: 56 | return 'Fragment'; 57 | case REACT_PORTAL_TYPE: 58 | return 'Portal'; 59 | case REACT_PROFILER_TYPE: 60 | return `Profiler`; 61 | case REACT_STRICT_MODE_TYPE: 62 | return 'StrictMode'; 63 | case REACT_SUSPENSE_TYPE: 64 | return 'Suspense'; 65 | case REACT_SUSPENSE_LIST_TYPE: 66 | return 'SuspenseList'; 67 | } 68 | if (typeof type === 'object') { 69 | switch (type.$$typeof) { 70 | case REACT_CONTEXT_TYPE: 71 | return 'Context.Consumer'; 72 | case REACT_PROVIDER_TYPE: 73 | return 'Context.Provider'; 74 | case REACT_FORWARD_REF_TYPE: 75 | return getWrappedName(type, type.render, 'ForwardRef'); 76 | case REACT_MEMO_TYPE: 77 | return getComponentName(type.type); 78 | case REACT_CHUNK_TYPE: 79 | return getComponentName(type.render); 80 | case REACT_LAZY_TYPE: { 81 | const thenable = type; 82 | const resolvedThenable = refineResolvedLazyComponent(thenable); 83 | if (resolvedThenable) { 84 | return getComponentName(resolvedThenable); 85 | } 86 | break; 87 | } 88 | } 89 | } 90 | return null; 91 | } 92 | 93 | export default getComponentName; 94 | -------------------------------------------------------------------------------- /src/shared/objectIs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | /** 11 | * inlined Object.is polyfill to avoid requiring consumers ship their own 12 | * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is 13 | */ 14 | function is(x, y) { 15 | return ( 16 | (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y) // eslint-disable-line no-self-compare 17 | ); 18 | } 19 | 20 | const objectIs = typeof Object.is === 'function' ? Object.is : is; 21 | 22 | export default objectIs; 23 | -------------------------------------------------------------------------------- /src/shared/shallowEqual.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) Facebook, Inc. and its affiliates. 3 | * 4 | * This source code is licensed under the MIT license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | * 8 | */ 9 | 10 | import is from './objectIs'; 11 | 12 | const hasOwnProperty = Object.prototype.hasOwnProperty; 13 | 14 | /** 15 | * Performs equality by iterating through keys on an object and returning false 16 | * when any key has values which are not strictly equal between the arguments. 17 | * Returns true when the values of all keys are strictly equal. 18 | */ 19 | function shallowEqual(objA, objB) { 20 | if (is(objA, objB)) { 21 | return true; 22 | } 23 | 24 | if ( 25 | typeof objA !== 'object' || 26 | objA === null || 27 | typeof objB !== 'object' || 28 | objB === null 29 | ) { 30 | return false; 31 | } 32 | 33 | const keysA = Object.keys(objA); 34 | const keysB = Object.keys(objB); 35 | 36 | if (keysA.length !== keysB.length) { 37 | return false; 38 | } 39 | 40 | // Test for A's keys different from B. 41 | for (let i = 0; i < keysA.length; i++) { 42 | if ( 43 | !hasOwnProperty.call(objB, keysA[i]) || 44 | !is(objA[keysA[i]], objB[keysA[i]]) 45 | ) { 46 | return false; 47 | } 48 | } 49 | 50 | return true; 51 | } 52 | 53 | export default shallowEqual; 54 | --------------------------------------------------------------------------------