├── .babelrc.js ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── codecov.yml ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Provider.js ├── connect.js ├── index.js ├── useRedux.js ├── useReduxActions.js ├── useReduxState.js └── utils.js └── test ├── .eslintrc ├── babel-transformer.jest.js ├── components ├── Provider.spec.js └── connect.spec.js ├── install-test-deps.js ├── integration └── server-rendering.spec.js ├── react └── 16.8 │ ├── package-lock.json │ └── package.json ├── run-tests.js └── utils └── shallowEqual.spec.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | const { NODE_ENV, BABEL_ENV } = process.env 2 | const cjs = NODE_ENV === 'test' || BABEL_ENV === 'commonjs' 3 | const loose = true 4 | 5 | module.exports = { 6 | presets: [['@babel/env', { loose, modules: false }]], 7 | plugins: [ 8 | ['@babel/proposal-decorators', { legacy: true }], 9 | ['@babel/proposal-object-rest-spread', { loose }], 10 | '@babel/transform-react-jsx', 11 | cjs && ['@babel/transform-modules-commonjs', { loose }], 12 | ['@babel/transform-runtime', { useESModules: !cjs }], 13 | ].filter(Boolean), 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/recommended", 6 | "plugin:react/recommended", 7 | "plugin:prettier/recommended" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": 6, 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true, 14 | "experimentalObjectRestSpread": true 15 | } 16 | }, 17 | "env": { 18 | "browser": true, 19 | "mocha": true, 20 | "node": true 21 | }, 22 | "rules": { 23 | "valid-jsdoc": 2, 24 | "react/jsx-uses-react": 1, 25 | "react/jsx-no-undef": 2, 26 | "react/jsx-wrap-multilines": 2, 27 | "react/no-string-refs": 0 28 | }, 29 | "plugins": [ 30 | "import", 31 | "react" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | dist 5 | lib 6 | .nyc_output 7 | coverage 8 | es 9 | test/**/lcov.info 10 | test/**/lcov-report 11 | test/react/*/test/**/*.spec.js 12 | test/react/**/src 13 | test/jest-config.json 14 | lcov.info 15 | 16 | lib/core/metadata.js 17 | lib/core/MetadataBlog.js 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "tabWidth": 2 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | env: 5 | matrix: 6 | - REACT=16.8 7 | sudo: false 8 | script: 9 | - npm test 10 | after_success: 11 | - npm run coverage 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present Dan Abramov 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 Redux Lean 2 | ========================= 3 | 4 | Unofficial React bindings for [Redux](https://github.com/reduxjs/redux). 5 | 6 | It is like `react-redux`, but removing a few features that most developers don't use. It also exposes 3 hooks, in case that you don't want to use the `connect` HOC. 7 | 8 | It is a fork of react-redux and it uses all the tests that are still relevant. 9 | 10 | The main differences between the `connect` functions of react-redux and react-redux-lean are that the later: 11 | 12 | - Does not accept factory selectors. 13 | - Knows how to handle "usable" selectors. So, if you decide to use redux-views with react-redux-lean then the cache of your shared-selectors will be automatically invalidated when there are no mounted components left using that cache entry. 14 | - Does not support the "impure" option. 15 | - It exposes the following hooks: `useReduxState`, `useReduxActions` and `useRedux` 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-redux-lean", 3 | "version": "0.2.1", 4 | "description": "Unofficial React bindings for Redux", 5 | "keywords": [ 6 | "react", 7 | "reactjs", 8 | "redux" 9 | ], 10 | "license": "MIT", 11 | "author": "Josep M Sobrepere (https://github.com/josepot)", 12 | "homepage": "https://github.com/josepot/react-redux-lean", 13 | "repository": "github:josepot/react-redux-lean", 14 | "bugs": "https://github.com/josepot/react-redux-lean/issues", 15 | "main": "./lib/index.js", 16 | "unpkg": "dist/react-redux.js", 17 | "module": "es/index.js", 18 | "files": [ 19 | "dist", 20 | "lib", 21 | "src", 22 | "es" 23 | ], 24 | "scripts": { 25 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib", 26 | "build:es": "babel src --out-dir es", 27 | "build:umd": "cross-env NODE_ENV=development rollup -c -o dist/react-redux.js", 28 | "build:umd:min": "cross-env NODE_ENV=production rollup -c -o dist/react-redux.min.js", 29 | "build": "npm run build:commonjs && npm run build:es && npm run build:umd && npm run build:umd:min", 30 | "clean": "rimraf lib dist es coverage", 31 | "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"docs/**/*.md\"", 32 | "lint": "eslint src test/utils test/components", 33 | "prepare": "npm run clean && npm run build", 34 | "pretest": "npm run lint", 35 | "test": "node ./test/run-tests.js", 36 | "coverage": "codecov" 37 | }, 38 | "peerDependencies": { 39 | "react": "^16.8.0-0", 40 | "redux": "^2.0.0 || ^3.0.0 || ^4.0.0-0" 41 | }, 42 | "dependencies": { 43 | "@babel/runtime": "^7.3.1", 44 | "hoist-non-react-statics": "^3.3.0", 45 | "invariant": "^2.2.4", 46 | "loose-envify": "^1.4.0" 47 | }, 48 | "devDependencies": { 49 | "@babel/cli": "^7.2.3", 50 | "@babel/core": "^7.3.3", 51 | "@babel/plugin-proposal-decorators": "^7.3.0", 52 | "@babel/plugin-proposal-object-rest-spread": "^7.3.2", 53 | "@babel/plugin-transform-react-display-name": "^7.2.0", 54 | "@babel/plugin-transform-react-jsx": "^7.3.0", 55 | "@babel/plugin-transform-runtime": "^7.2.0", 56 | "@babel/preset-env": "^7.3.1", 57 | "babel-core": "^7.0.0-bridge.0", 58 | "babel-eslint": "^10.0.1", 59 | "babel-jest": "^24.1.0", 60 | "codecov": "^3.2.0", 61 | "create-react-class": "^15.6.3", 62 | "cross-env": "^5.2.0", 63 | "cross-spawn": "^6.0.5", 64 | "es3ify": "^0.2.0", 65 | "eslint": "^5.14.1", 66 | "eslint-config-prettier": "^4.0.0", 67 | "eslint-plugin-import": "^2.16.0", 68 | "eslint-plugin-prettier": "^3.0.1", 69 | "eslint-plugin-react": "^7.12.4", 70 | "glob": "^7.1.3", 71 | "jest": "^24.1.0", 72 | "jest-dom": "^3.1.2", 73 | "npm-run": "^5.0.1", 74 | "prettier": "^1.16.4", 75 | "prop-types": "^15.7.2", 76 | "react": "^16.8.4", 77 | "react-dom": "^16.8.4", 78 | "react-testing-library": "^5.9.0", 79 | "redux": "^4.0.1", 80 | "rimraf": "^2.6.3", 81 | "rollup": "^1.2.2", 82 | "rollup-plugin-babel": "^4.3.2", 83 | "rollup-plugin-commonjs": "^9.2.0", 84 | "rollup-plugin-node-resolve": "^4.0.0", 85 | "rollup-plugin-replace": "^2.1.0", 86 | "rollup-plugin-terser": "^4.0.4", 87 | "semver": "^5.6.0" 88 | }, 89 | "browserify": { 90 | "transform": [ 91 | "loose-envify" 92 | ] 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import nodeResolve 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 { terser } from 'rollup-plugin-terser' 6 | import pkg from './package.json' 7 | 8 | const env = process.env.NODE_ENV 9 | 10 | const config = { 11 | input: 'src/index.js', 12 | external: Object.keys(pkg.peerDependencies || {}), 13 | output: { 14 | format: 'umd', 15 | name: 'ReactReduxLean', 16 | globals: { 17 | react: 'React', 18 | redux: 'Redux' 19 | } 20 | }, 21 | plugins: [ 22 | nodeResolve(), 23 | babel({ 24 | exclude: '**/node_modules/**', 25 | runtimeHelpers: true 26 | }), 27 | replace({ 28 | 'process.env.NODE_ENV': JSON.stringify(env) 29 | }), 30 | commonjs({ 31 | namedExports: { 32 | 'node_modules/react-is/index.js': [ 33 | 'isValidElementType', 34 | 'isContextConsumer' 35 | ] 36 | } 37 | }) 38 | ] 39 | } 40 | 41 | if (env === 'production') { 42 | config.plugins.push( 43 | terser({ 44 | compress: { 45 | pure_getters: true, 46 | unsafe: true, 47 | unsafe_comps: true, 48 | warnings: false 49 | } 50 | }) 51 | ) 52 | } 53 | 54 | export default config 55 | -------------------------------------------------------------------------------- /src/Provider.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types' 2 | import React, { createContext, useState, useEffect } from 'react' 3 | 4 | export const stateContext = createContext() 5 | export const dispatchContext = createContext() 6 | 7 | const { Provider: StateProvider } = stateContext 8 | const { Provider: DispatchProvider } = dispatchContext 9 | 10 | const StateProviderComp = ({ store, children }) => { 11 | const [stateStore, setStateStore] = useState(store.getState()) 12 | 13 | useEffect(() => { 14 | setStateStore(store.getState()) 15 | return store.subscribe(() => setStateStore(store.getState())) 16 | }, [store]) 17 | 18 | return {children} 19 | } 20 | 21 | export const ReduxProvider = ({ store, children }) => ( 22 | 23 | {children} 24 | 25 | ) 26 | 27 | ReduxProvider.propTypes = { 28 | children: PropTypes.any, 29 | store: PropTypes.shape({ 30 | subscribe: PropTypes.func.isRequired, 31 | dispatch: PropTypes.func.isRequired, 32 | getState: PropTypes.func.isRequired 33 | }) 34 | } 35 | StateProviderComp.propTypes = ReduxProvider.propTypes 36 | 37 | export const ServerProvider = ({ store, children }) => ( 38 | 39 | {children} 40 | 41 | ) 42 | 43 | ServerProvider.propTypes = ReduxProvider.propTypes 44 | 45 | export default ReduxProvider 46 | -------------------------------------------------------------------------------- /src/connect.js: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from 'react' 2 | import hoistNonReactStatics from 'hoist-non-react-statics' 3 | import invariant from 'invariant' 4 | import { isValidElementType } from 'react-is' 5 | import useRedux from './useRedux' 6 | 7 | const emptyObj = {} 8 | const alwaysEmpty = x => emptyObj // eslint-disable-line 9 | const defaultMapper = (stateProps, actionProps, externalProps) => ({ 10 | ...externalProps, 11 | ...stateProps, 12 | ...actionProps 13 | }) 14 | 15 | const stringifyComponent = Comp => { 16 | try { 17 | return JSON.stringify(Comp) 18 | } catch (err) { 19 | return String(Comp) 20 | } 21 | } 22 | 23 | export default ( 24 | fromStateProps_, 25 | fromActionProps_, 26 | mapper_, 27 | { getDisplayName, forwardRef } = emptyObj 28 | ) => { 29 | const fromStateProps = fromStateProps_ || alwaysEmpty 30 | const fromActionProps = fromActionProps_ || null 31 | const mapper = mapper_ || defaultMapper 32 | 33 | return BaseComponent => { 34 | if (process.env.NODE_ENV !== 'production') { 35 | invariant( 36 | isValidElementType(BaseComponent), 37 | `You must pass a component to the function returned by ` + 38 | `connect. Instead received ${stringifyComponent(BaseComponent)}` 39 | ) 40 | } 41 | 42 | const name = BaseComponent.name || BaseComponent.displayName || 'Component' 43 | const displayName = getDisplayName 44 | ? getDisplayName(name) 45 | : `Connect(${name})` 46 | 47 | const Connect = props => { 48 | const finalProps = useRedux( 49 | fromStateProps, 50 | fromActionProps, 51 | mapper, 52 | props, 53 | displayName 54 | ) 55 | 56 | return useMemo(() => { 57 | let propsWithRef = finalProps 58 | if (finalProps.forwardedRef) { 59 | propsWithRef = { ...finalProps } 60 | propsWithRef.ref = finalProps.forwardedRef 61 | delete propsWithRef.forwardedRef 62 | } 63 | return 64 | }, [finalProps]) 65 | } 66 | 67 | Connect.WrappedComponent = BaseComponent 68 | Connect.displayName = displayName 69 | 70 | if (forwardRef) { 71 | const forwarded = React.forwardRef(function forwardConnectRef( 72 | props, 73 | ref 74 | ) { 75 | return 76 | }) 77 | 78 | forwarded.displayName = displayName 79 | forwarded.WrappedComponent = Connect 80 | return hoistNonReactStatics(forwarded, Connect) 81 | } 82 | 83 | return hoistNonReactStatics(Connect, BaseComponent) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Provider, { ServerProvider } from './Provider' 2 | import connect from './connect' 3 | import useRedux from './useRedux' 4 | import useReduxActions from './useReduxActions' 5 | import useReduxState from './useReduxState' 6 | 7 | export { 8 | Provider, 9 | ServerProvider, 10 | connect, 11 | useRedux, 12 | useReduxActions, 13 | useReduxState 14 | } 15 | -------------------------------------------------------------------------------- /src/useRedux.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useMemo } from 'react' 2 | import useReduxState from './useReduxState' 3 | import useReduxActions from './useReduxActions' 4 | import { ensurePlainObject, shallowCompare } from './utils' 5 | 6 | export default ( 7 | selector, 8 | actionCreators, 9 | mapper, 10 | props, 11 | displayName = 'Component' 12 | ) => { 13 | const prevPropsRef = useRef(null) 14 | const stateProps = useReduxState(selector, props) 15 | const actionProps = useReduxActions(actionCreators, props) 16 | if (process.env.NODE_ENV !== 'production') { 17 | ensurePlainObject(stateProps, displayName, 'mapStateToProps') 18 | ensurePlainObject(actionProps, displayName, 'mapDispatchToProps') 19 | } 20 | 21 | const result = useMemo(() => { 22 | const allProps = mapper(stateProps, actionProps, props) 23 | if (process.env.NODE_ENV !== 'production') { 24 | ensurePlainObject(allProps, displayName, 'mergeProps') 25 | } 26 | const isTheSame = 27 | prevPropsRef.current && shallowCompare(allProps, prevPropsRef.current) 28 | return isTheSame ? prevPropsRef.current : allProps 29 | }, [stateProps, actionProps, props, mapper]) 30 | 31 | useEffect(() => { 32 | prevPropsRef.current = result 33 | }, [result]) 34 | 35 | return result 36 | } 37 | -------------------------------------------------------------------------------- /src/useReduxActions.js: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react' 2 | import invariant from 'invariant' 3 | import { dispatchContext } from './Provider' 4 | 5 | const emptyObj = {} 6 | 7 | export default (actionCreators = emptyObj, props = emptyObj) => { 8 | const dispatch = useContext(dispatchContext) 9 | if (process.env.NODE_ENV !== 'production') { 10 | invariant(dispatch, 'Could not find "store"') 11 | } 12 | const relevantProps = 13 | typeof actionCreators === 'function' && actionCreators.length !== 1 14 | ? props 15 | : emptyObj 16 | 17 | return useMemo(() => { 18 | if (actionCreators === null) return { dispatch } 19 | if (typeof actionCreators === 'function') 20 | return actionCreators(dispatch, relevantProps) 21 | 22 | const res = {} 23 | Object.entries(actionCreators).forEach(([name, aCreator]) => { 24 | res[name] = (...args) => dispatch(aCreator(...args)) 25 | }) 26 | return res 27 | }, [actionCreators, dispatch, relevantProps]) 28 | } 29 | -------------------------------------------------------------------------------- /src/useReduxState.js: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useMemo } from 'react' 2 | import invariant from 'invariant' 3 | import { stateContext } from './Provider' 4 | 5 | const emptyObj = {} 6 | 7 | export default (selector, props = emptyObj) => { 8 | const { keySelector = Function.prototype, use } = selector 9 | const key = keySelector(null, props) 10 | 11 | useEffect(() => use && use(key), [key, use]) 12 | 13 | const finalProps = selector.length === 1 ? emptyObj : props 14 | const state = useContext(stateContext) 15 | if (process.env.NODE_ENV !== 'production') { 16 | invariant(state !== undefined, 'Could not find "store"') 17 | } 18 | 19 | return useMemo(() => selector(state, finalProps), [ 20 | selector, 21 | state, 22 | finalProps 23 | ]) 24 | } 25 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const hasOwn = Object.prototype.hasOwnProperty 2 | 3 | function is(x, y) { 4 | if (x === y) { 5 | return x !== 0 || y !== 0 || 1 / x === 1 / y 6 | } else { 7 | return x !== x && y !== y 8 | } 9 | } 10 | 11 | export function shallowCompare(objA, objB) { 12 | if (is(objA, objB)) return true 13 | 14 | if ( 15 | typeof objA !== 'object' || 16 | objA === null || 17 | typeof objB !== 'object' || 18 | objB === null 19 | ) { 20 | return false 21 | } 22 | 23 | const keysA = Object.keys(objA) 24 | const keysB = Object.keys(objB) 25 | 26 | if (keysA.length !== keysB.length) return false 27 | 28 | for (let i = 0; i < keysA.length; i++) { 29 | if (!hasOwn.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]])) { 30 | return false 31 | } 32 | } 33 | 34 | return true 35 | } 36 | 37 | function isPlainObject(obj) { 38 | if (typeof obj !== 'object' || obj === null) return false 39 | 40 | let proto = Object.getPrototypeOf(obj) 41 | if (proto === null) return true 42 | 43 | let baseProto = proto 44 | while (Object.getPrototypeOf(baseProto) !== null) { 45 | baseProto = Object.getPrototypeOf(baseProto) 46 | } 47 | 48 | return proto === baseProto 49 | } 50 | 51 | /** 52 | * Prints a warning in the console if it exists. 53 | * 54 | * @param {String} message The warning message. 55 | * @returns {void} 56 | */ 57 | function warning(message) { 58 | /* eslint-disable no-console */ 59 | if (typeof console !== 'undefined' && typeof console.error === 'function') { 60 | console.error(message) 61 | } 62 | /* eslint-enable no-console */ 63 | try { 64 | // This error was thrown as a convenience so that if you enable 65 | // "break on all exceptions" in your console, 66 | // it would pause the execution at this line. 67 | throw new Error(message) 68 | /* eslint-disable no-empty */ 69 | } catch (e) {} 70 | /* eslint-enable no-empty */ 71 | } 72 | 73 | export const ensurePlainObject = (val, displayName, methodName) => { 74 | if (!isPlainObject(val)) 75 | warning( 76 | `${methodName}() in ${displayName} must return a plain object. Instead received ${val}.` 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/babel-transformer.jest.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const { createTransformer } = require('babel-jest') 3 | 4 | module.exports = createTransformer({ 5 | configFile: path.resolve(__dirname, '../.babelrc.js') 6 | }) 7 | -------------------------------------------------------------------------------- /test/components/Provider.spec.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable react/prop-types*/ 2 | 3 | import React, { useContext, Component } from 'react' 4 | import ReactDOM from 'react-dom' 5 | import { createStore } from 'redux' 6 | import { Provider, connect } from '../../src/index.js' 7 | import { stateContext } from '../../src/Provider' 8 | import * as rtl from 'react-testing-library' 9 | import 'jest-dom/extend-expect' 10 | 11 | const createExampleTextReducer = () => (state = 'example text') => state 12 | 13 | describe('React', () => { 14 | describe('Provider', () => { 15 | afterEach(() => rtl.cleanup()) 16 | 17 | const createChild = (storeKey = 'store') => 18 | function Child() { 19 | const state = useContext(stateContext) 20 | return
{`${storeKey} - ${state}`}
21 | } 22 | 23 | const Child = createChild() 24 | 25 | it('should not enforce a single child', () => { 26 | const store = createStore(() => ({})) 27 | 28 | // Ignore propTypes warnings 29 | const propTypes = Provider.propTypes 30 | Provider.propTypes = {} 31 | 32 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 33 | 34 | expect(() => 35 | rtl.render( 36 | 37 |
38 | 39 | ) 40 | ).not.toThrow() 41 | 42 | expect(() => rtl.render()).not.toThrow( 43 | /children with exactly one child/ 44 | ) 45 | 46 | expect(() => 47 | rtl.render( 48 | 49 |
50 |
51 | 52 | ) 53 | ).not.toThrow(/a single React element child/) 54 | spy.mockRestore() 55 | Provider.propTypes = propTypes 56 | }) 57 | 58 | it('should add the store state to context', () => { 59 | const store = createStore(createExampleTextReducer()) 60 | 61 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 62 | const tester = rtl.render( 63 | 64 | 65 | 66 | ) 67 | expect(spy).toHaveBeenCalledTimes(0) 68 | spy.mockRestore() 69 | 70 | expect(tester.getByTestId('store')).toHaveTextContent( 71 | 'store - example text' 72 | ) 73 | }) 74 | 75 | it('accepts new store in props', () => { 76 | const store1 = createStore((state = 10) => state + 1) 77 | const store2 = createStore((state = 10) => state * 2) 78 | const store3 = createStore((state = 10) => state * state + 1) 79 | 80 | let externalSetState 81 | class ProviderContainer extends Component { 82 | constructor() { 83 | super() 84 | this.state = { store: store1 } 85 | externalSetState = this.setState.bind(this) 86 | } 87 | 88 | render() { 89 | return ( 90 | 91 | 92 | 93 | ) 94 | } 95 | } 96 | 97 | const tester = rtl.render() 98 | expect(tester.getByTestId('store')).toHaveTextContent('store - 11') 99 | rtl.act(() => { 100 | store1.dispatch({ type: 'hi' }) 101 | }) 102 | expect(tester.getByTestId('store')).toHaveTextContent('store - 12') 103 | 104 | rtl.act(() => { 105 | externalSetState({ store: store2 }) 106 | }) 107 | expect(tester.getByTestId('store')).toHaveTextContent('store - 20') 108 | rtl.act(() => { 109 | store1.dispatch({ type: 'hi' }) 110 | }) 111 | expect(tester.getByTestId('store')).toHaveTextContent('store - 20') 112 | rtl.act(() => { 113 | store2.dispatch({ type: 'hi' }) 114 | }) 115 | expect(tester.getByTestId('store')).toHaveTextContent('store - 40') 116 | 117 | rtl.act(() => { 118 | externalSetState({ store: store3 }) 119 | }) 120 | expect(tester.getByTestId('store')).toHaveTextContent('store - 101') 121 | rtl.act(() => { 122 | store1.dispatch({ type: 'hi' }) 123 | }) 124 | expect(tester.getByTestId('store')).toHaveTextContent('store - 101') 125 | rtl.act(() => { 126 | store2.dispatch({ type: 'hi' }) 127 | }) 128 | expect(tester.getByTestId('store')).toHaveTextContent('store - 101') 129 | rtl.act(() => { 130 | store3.dispatch({ type: 'hi' }) 131 | }) 132 | expect(tester.getByTestId('store')).toHaveTextContent('store - 10202') 133 | }) 134 | 135 | it('should handle subscriptions correctly when there is nested Providers', () => { 136 | const reducer = (state = 0, action) => 137 | action.type === 'INC' ? state + 1 : state 138 | 139 | const innerStore = createStore(reducer) 140 | const innerMapStateToProps = jest.fn(state => ({ count: state })) 141 | @connect(innerMapStateToProps) 142 | class Inner extends Component { 143 | render() { 144 | return
{this.props.count}
145 | } 146 | } 147 | 148 | const outerStore = createStore(reducer) 149 | @connect(state => ({ count: state })) 150 | class Outer extends Component { 151 | render() { 152 | return ( 153 | 154 | 155 | 156 | ) 157 | } 158 | } 159 | 160 | rtl.render( 161 | 162 | 163 | 164 | ) 165 | expect(innerMapStateToProps).toHaveBeenCalledTimes(1) 166 | 167 | rtl.act(() => { 168 | innerStore.dispatch({ type: 'INC' }) 169 | }) 170 | 171 | expect(innerMapStateToProps).toHaveBeenCalledTimes(2) 172 | }) 173 | 174 | it('should pass state consistently to mapState', () => { 175 | function stringBuilder(prev = '', action) { 176 | return action.type === 'APPEND' ? prev + action.body : prev 177 | } 178 | 179 | const store = createStore(stringBuilder) 180 | 181 | store.dispatch({ type: 'APPEND', body: 'a' }) 182 | let childMapStateInvokes = 0 183 | 184 | @connect(state => ({ state })) 185 | class Container extends Component { 186 | emitChange() { 187 | store.dispatch({ type: 'APPEND', body: 'b' }) 188 | } 189 | 190 | render() { 191 | return ( 192 |
193 | 194 | 195 |
196 | ) 197 | } 198 | } 199 | 200 | const childCalls = [] 201 | @connect((state, parentProps) => { 202 | childMapStateInvokes++ 203 | childCalls.push([state, parentProps.parentState]) 204 | // The state from parent props should always be consistent with the current state 205 | return {} 206 | }) 207 | class ChildContainer extends Component { 208 | render() { 209 | return
210 | } 211 | } 212 | 213 | const tester = rtl.render( 214 | 215 | 216 | 217 | ) 218 | 219 | expect(childMapStateInvokes).toBe(1) 220 | 221 | // The store state stays consistent when setState calls are batched 222 | rtl.act(() => { 223 | store.dispatch({ type: 'APPEND', body: 'c' }) 224 | }) 225 | expect(childMapStateInvokes).toBe(2) 226 | expect(childCalls).toEqual([['a', 'a'], ['ac', 'ac']]) 227 | 228 | // setState calls DOM handlers are batched 229 | const button = tester.getByText('change') 230 | rtl.fireEvent.click(button) 231 | expect(childMapStateInvokes).toBe(3) 232 | 233 | // Provider uses unstable_batchedUpdates() under the hood 234 | rtl.act(() => { 235 | store.dispatch({ type: 'APPEND', body: 'd' }) 236 | }) 237 | expect(childCalls).toEqual([ 238 | ['a', 'a'], 239 | ['ac', 'ac'], // then store update is processed 240 | ['acb', 'acb'], // then store update is processed 241 | ['acbd', 'acbd'] // then store update is processed 242 | ]) 243 | expect(childMapStateInvokes).toBe(4) 244 | }) 245 | 246 | it('works in without warnings (React 16.3+)', () => { 247 | if (!React.StrictMode) { 248 | return 249 | } 250 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 251 | const store = createStore(() => ({})) 252 | 253 | rtl.render( 254 | 255 | 256 |
257 | 258 | 259 | ) 260 | 261 | expect(spy).not.toHaveBeenCalled() 262 | }) 263 | 264 | it('should unsubscribe before unmounting', () => { 265 | const store = createStore(createExampleTextReducer()) 266 | const subscribe = store.subscribe 267 | 268 | // Keep track of unsubscribe by wrapping subscribe() 269 | const spy = jest.fn(() => ({})) 270 | store.subscribe = listener => { 271 | const unsubscribe = subscribe(listener) 272 | return () => { 273 | spy() 274 | return unsubscribe() 275 | } 276 | } 277 | 278 | const div = document.createElement('div') 279 | ReactDOM.render( 280 | 281 |
282 | , 283 | div 284 | ) 285 | 286 | expect(spy).toHaveBeenCalledTimes(0) 287 | ReactDOM.unmountComponentAtNode(div) 288 | expect(spy).toHaveBeenCalledTimes(1) 289 | }) 290 | }) 291 | }) 292 | -------------------------------------------------------------------------------- /test/components/connect.spec.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable react/prop-types*/ 2 | 3 | import React, { useEffect, Component } from 'react' 4 | import createClass from 'create-react-class' 5 | import PropTypes from 'prop-types' 6 | import ReactDOM from 'react-dom' 7 | import { createStore } from 'redux' 8 | import { Provider as ProviderMock, connect } from '../../src/index.js' 9 | import * as rtl from 'react-testing-library' 10 | import 'jest-dom/extend-expect' 11 | 12 | jest.useFakeTimers() 13 | 14 | describe('React', () => { 15 | describe('connect', () => { 16 | const propMapper = prop => { 17 | switch (typeof prop) { 18 | case 'object': 19 | case 'boolean': 20 | return JSON.stringify(prop) 21 | case 'function': 22 | return '[function ' + prop.name + ']' 23 | default: 24 | return prop 25 | } 26 | } 27 | class Passthrough extends Component { 28 | render() { 29 | return ( 30 |
    31 | {Object.keys(this.props).map(prop => ( 32 |
  • 33 | {propMapper(this.props[prop])} 34 |
  • 35 | ))} 36 |
37 | ) 38 | } 39 | } 40 | 41 | class ContextBoundStore { 42 | constructor(reducer) { 43 | this.reducer = reducer 44 | this.listeners = [] 45 | this.state = undefined 46 | this.dispatch({}) 47 | } 48 | 49 | getState() { 50 | return this.state 51 | } 52 | 53 | subscribe(listener) { 54 | this.listeners.push(listener) 55 | return () => this.listeners.filter(l => l !== listener) 56 | } 57 | 58 | dispatch(action) { 59 | this.state = this.reducer(this.getState(), action) 60 | this.listeners.forEach(l => l()) 61 | return action 62 | } 63 | } 64 | 65 | function stringBuilder(prev = '', action) { 66 | return action.type === 'APPEND' ? prev + action.body : prev 67 | } 68 | 69 | function imitateHotReloading(TargetClass, SourceClass, container) { 70 | // Crude imitation of hot reloading that does the job 71 | Object.getOwnPropertyNames(SourceClass.prototype) 72 | .filter(key => typeof SourceClass.prototype[key] === 'function') 73 | .forEach(key => { 74 | if (key !== 'render' && key !== 'constructor') { 75 | TargetClass.prototype[key] = SourceClass.prototype[key] 76 | } 77 | }) 78 | 79 | container.forceUpdate() 80 | } 81 | 82 | afterEach(() => rtl.cleanup()) 83 | 84 | it('should receive the store state in the context', () => { 85 | const store = createStore(() => ({ hi: 'there' })) 86 | 87 | @connect(state => state) 88 | class Container extends Component { 89 | render() { 90 | return 91 | } 92 | } 93 | 94 | const tester = rtl.render( 95 | 96 | 97 | 98 | ) 99 | 100 | expect(tester.getByTestId('hi')).toHaveTextContent('there') 101 | }) 102 | 103 | it('Should work with a memo component, if it exists', () => { 104 | if (React.memo) { 105 | const store = createStore(() => ({ hi: 'there' })) 106 | 107 | const Container = React.memo(props => ) // eslint-disable-line 108 | const WrappedContainer = connect(state => state)(Container) 109 | 110 | const tester = rtl.render( 111 | 112 | 113 | 114 | ) 115 | 116 | expect(tester.getByTestId('hi')).toHaveTextContent('there') 117 | } 118 | }) 119 | 120 | it('should pass state and props to the given component', () => { 121 | const store = createStore(() => ({ 122 | foo: 'bar', 123 | baz: 42, 124 | hello: 'world' 125 | })) 126 | 127 | @connect( 128 | ({ foo, baz }) => ({ foo, baz }), 129 | {} 130 | ) 131 | class Container extends Component { 132 | render() { 133 | return 134 | } 135 | } 136 | 137 | const tester = rtl.render( 138 | 139 | 140 | 141 | ) 142 | expect(tester.getByTestId('pass')).toHaveTextContent('through') 143 | expect(tester.getByTestId('foo')).toHaveTextContent('bar') 144 | expect(tester.getByTestId('baz')).toHaveTextContent('42') 145 | expect(tester.queryByTestId('hello')).toBe(null) 146 | }) 147 | 148 | it('should subscribe class components to the store changes', () => { 149 | const store = createStore(stringBuilder) 150 | 151 | @connect(state => ({ string: state })) 152 | class Container extends Component { 153 | render() { 154 | return 155 | } 156 | } 157 | 158 | const tester = rtl.render( 159 | 160 | 161 | 162 | ) 163 | expect(tester.getByTestId('string')).toHaveTextContent('') 164 | rtl.act(() => { 165 | store.dispatch({ type: 'APPEND', body: 'a' }) 166 | }) 167 | expect(tester.getByTestId('string')).toHaveTextContent('a') 168 | rtl.act(() => { 169 | store.dispatch({ type: 'APPEND', body: 'b' }) 170 | }) 171 | expect(tester.getByTestId('string')).toHaveTextContent('ab') 172 | }) 173 | 174 | it('should subscribe pure function components to the store changes', () => { 175 | const store = createStore(stringBuilder) 176 | 177 | const Container = connect(state => ({ string: state }))( 178 | function Container(props) { 179 | return 180 | } 181 | ) 182 | 183 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 184 | 185 | const tester = rtl.render( 186 | 187 | 188 | 189 | ) 190 | expect(spy).toHaveBeenCalledTimes(0) 191 | spy.mockRestore() 192 | 193 | expect(tester.getByTestId('string')).toHaveTextContent('') 194 | rtl.act(() => { 195 | store.dispatch({ type: 'APPEND', body: 'a' }) 196 | }) 197 | expect(tester.getByTestId('string')).toHaveTextContent('a') 198 | rtl.act(() => { 199 | store.dispatch({ type: 'APPEND', body: 'b' }) 200 | }) 201 | expect(tester.getByTestId('string')).toHaveTextContent('ab') 202 | }) 203 | 204 | it("should retain the store's context", () => { 205 | const store = new ContextBoundStore(stringBuilder) 206 | 207 | let Container = connect(state => ({ string: state }))(function Container( 208 | props 209 | ) { 210 | return 211 | }) 212 | 213 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 214 | const tester = rtl.render( 215 | 216 | 217 | 218 | ) 219 | expect(spy).toHaveBeenCalledTimes(0) 220 | spy.mockRestore() 221 | 222 | expect(tester.getByTestId('string')).toHaveTextContent('') 223 | rtl.act(() => { 224 | store.dispatch({ type: 'APPEND', body: 'a' }) 225 | }) 226 | expect(tester.getByTestId('string')).toHaveTextContent('a') 227 | }) 228 | 229 | it('should handle dispatches before componentDidMount', () => { 230 | const store = createStore(stringBuilder) 231 | 232 | const Container = connect(state => ({ string: state }))(props => { 233 | useEffect(() => { 234 | store.dispatch({ type: 'APPEND', body: 'a' }) 235 | }, []) 236 | 237 | return 238 | }) 239 | 240 | const tester = rtl.render( 241 | 242 | 243 | 244 | ) 245 | expect(tester.getByTestId('string')).toHaveTextContent('a') 246 | }) 247 | 248 | it('should handle additional prop changes in addition to slice', () => { 249 | const store = createStore(() => ({ 250 | foo: 'bar' 251 | })) 252 | 253 | @connect(state => state) 254 | class ConnectContainer extends Component { 255 | render() { 256 | return 257 | } 258 | } 259 | 260 | class Container extends Component { 261 | constructor() { 262 | super() 263 | this.state = { 264 | bar: { 265 | baz: '' 266 | } 267 | } 268 | } 269 | 270 | componentDidMount() { 271 | this.setState(({ bar }) => ({ 272 | bar: Object.assign({}, bar, { baz: 'through' }) 273 | })) 274 | } 275 | 276 | render() { 277 | return ( 278 | 279 | 280 | 281 | ) 282 | } 283 | } 284 | 285 | const tester = rtl.render() 286 | 287 | expect(tester.getByTestId('foo')).toHaveTextContent('bar') 288 | expect(tester.getByTestId('pass')).toHaveTextContent('through') 289 | }) 290 | 291 | it('should handle unexpected prop changes with forceUpdate()', () => { 292 | const store = createStore(() => ({})) 293 | 294 | @connect(state => state) 295 | class ConnectContainer extends Component { 296 | render() { 297 | return 298 | } 299 | } 300 | 301 | class Container extends Component { 302 | constructor() { 303 | super() 304 | this.bar = 'baz' 305 | } 306 | 307 | componentDidMount() { 308 | this.bar = 'foo' 309 | this.forceUpdate() 310 | } 311 | 312 | render() { 313 | return ( 314 | 315 | 316 | 317 | ) 318 | } 319 | } 320 | 321 | const tester = rtl.render() 322 | 323 | expect(tester.getByTestId('bar')).toHaveTextContent('foo') 324 | }) 325 | 326 | it('should remove undefined props', () => { 327 | const store = createStore(() => ({})) 328 | let props = { x: true } 329 | let container 330 | 331 | @connect( 332 | () => ({}), 333 | () => ({}) 334 | ) 335 | class ConnectContainer extends Component { 336 | render() { 337 | return 338 | } 339 | } 340 | 341 | class HolderContainer extends Component { 342 | render() { 343 | return 344 | } 345 | } 346 | 347 | const tester = rtl.render( 348 | 349 | (container = instance)} /> 350 | 351 | ) 352 | 353 | expect(tester.getByTestId('x')).toHaveTextContent('true') 354 | 355 | props = {} 356 | container.forceUpdate() 357 | 358 | expect(tester.queryByTestId('x')).toBe(null) 359 | }) 360 | 361 | it('should remove undefined props without mapDispatch', () => { 362 | const store = createStore(() => ({})) 363 | let props = { x: true } 364 | let container 365 | 366 | @connect(() => ({})) 367 | class ConnectContainer extends Component { 368 | render() { 369 | return 370 | } 371 | } 372 | 373 | class HolderContainer extends Component { 374 | render() { 375 | return 376 | } 377 | } 378 | 379 | const tester = rtl.render( 380 | 381 | (container = instance)} /> 382 | 383 | ) 384 | 385 | expect(tester.getAllByTitle('prop').length).toBe(2) 386 | expect(tester.getByTestId('dispatch')).toHaveTextContent( 387 | '[function dispatch]' 388 | ) 389 | expect(tester.getByTestId('x')).toHaveTextContent('true') 390 | 391 | props = {} 392 | container.forceUpdate() 393 | 394 | expect(tester.getAllByTitle('prop').length).toBe(1) 395 | expect(tester.getByTestId('dispatch')).toHaveTextContent( 396 | '[function dispatch]' 397 | ) 398 | }) 399 | 400 | it('should ignore deep mutations in props', () => { 401 | const store = createStore(() => ({ 402 | foo: 'bar' 403 | })) 404 | 405 | @connect(state => state) 406 | class ConnectContainer extends Component { 407 | render() { 408 | return 409 | } 410 | } 411 | 412 | class Container extends Component { 413 | constructor() { 414 | super() 415 | this.state = { 416 | bar: { 417 | baz: '' 418 | } 419 | } 420 | } 421 | 422 | componentDidMount() { 423 | // Simulate deep object mutation 424 | const bar = this.state.bar 425 | bar.baz = 'through' 426 | this.setState({ 427 | bar 428 | }) 429 | } 430 | 431 | render() { 432 | return ( 433 | 434 | 435 | 436 | ) 437 | } 438 | } 439 | 440 | const tester = rtl.render() 441 | expect(tester.getByTestId('foo')).toHaveTextContent('bar') 442 | expect(tester.getByTestId('pass')).toHaveTextContent('') 443 | }) 444 | 445 | it('should allow for merge to incorporate state and prop changes', () => { 446 | const store = createStore(stringBuilder) 447 | 448 | function doSomething(thing) { 449 | return { 450 | type: 'APPEND', 451 | body: thing 452 | } 453 | } 454 | 455 | let merged 456 | let externalSetState 457 | @connect( 458 | state => ({ stateThing: state }), 459 | dispatch => ({ 460 | doSomething: whatever => dispatch(doSomething(whatever)) 461 | }), 462 | (stateProps, actionProps, parentProps) => ({ 463 | ...stateProps, 464 | ...actionProps, 465 | mergedDoSomething: (() => { 466 | merged = function mergedDoSomething(thing) { 467 | const seed = stateProps.stateThing === '' ? 'HELLO ' : '' 468 | actionProps.doSomething(seed + thing + parentProps.extra) 469 | } 470 | return merged 471 | })() 472 | }) 473 | ) 474 | class Container extends Component { 475 | render() { 476 | return 477 | } 478 | } 479 | 480 | class OuterContainer extends Component { 481 | constructor() { 482 | super() 483 | this.state = { extra: 'z' } 484 | externalSetState = this.setState.bind(this) 485 | } 486 | 487 | render() { 488 | return ( 489 | 490 | 491 | 492 | ) 493 | } 494 | } 495 | 496 | const tester = rtl.render() 497 | 498 | expect(tester.getByTestId('stateThing')).toHaveTextContent('') 499 | rtl.act(() => { 500 | merged('a') 501 | }) 502 | expect(tester.getByTestId('stateThing')).toHaveTextContent('HELLO az') 503 | rtl.act(() => { 504 | merged('b') 505 | }) 506 | expect(tester.getByTestId('stateThing')).toHaveTextContent('HELLO azbz') 507 | externalSetState({ extra: 'Z' }) 508 | rtl.act(() => { 509 | merged('c') 510 | }) 511 | expect(tester.getByTestId('stateThing')).toHaveTextContent('HELLO azbzcZ') 512 | }) 513 | 514 | it('should merge actionProps into WrappedComponent', () => { 515 | const store = createStore(() => ({ 516 | foo: 'bar' 517 | })) 518 | 519 | const exampleActionCreator = () => {} 520 | 521 | @connect( 522 | state => state, 523 | () => ({ exampleActionCreator }) 524 | ) 525 | class Container extends Component { 526 | render() { 527 | return 528 | } 529 | } 530 | 531 | const tester = rtl.render( 532 | 533 | 534 | 535 | ) 536 | 537 | expect(tester.getByTestId('exampleActionCreator')).toHaveTextContent( 538 | '[function exampleActionCreator]' 539 | ) 540 | expect(tester.getByTestId('foo')).toHaveTextContent('bar') 541 | }) 542 | 543 | it('should not invoke mapState when props change if it only has one argument', () => { 544 | const store = createStore(stringBuilder) 545 | 546 | let invocationCount = 0 547 | 548 | /*eslint-disable no-unused-vars */ 549 | @connect(arg1 => { 550 | invocationCount++ 551 | return {} 552 | }) 553 | /*eslint-enable no-unused-vars */ 554 | class WithoutProps extends Component { 555 | render() { 556 | return 557 | } 558 | } 559 | 560 | class OuterComponent extends Component { 561 | constructor() { 562 | super() 563 | this.state = { foo: 'FOO' } 564 | } 565 | 566 | setFoo(foo) { 567 | this.setState({ foo }) 568 | } 569 | 570 | render() { 571 | return ( 572 |
573 | 574 |
575 | ) 576 | } 577 | } 578 | 579 | let outerComponent 580 | rtl.render( 581 | 582 | (outerComponent = c)} /> 583 | 584 | ) 585 | outerComponent.setFoo('BAR') 586 | outerComponent.setFoo('DID') 587 | 588 | expect(invocationCount).toEqual(1) 589 | }) 590 | 591 | it('should invoke mapState every time props are changed if it has zero arguments', () => { 592 | const store = createStore(stringBuilder) 593 | 594 | let invocationCount = 0 595 | 596 | @connect(() => { 597 | invocationCount++ 598 | return {} 599 | }) 600 | class WithoutProps extends Component { 601 | render() { 602 | return 603 | } 604 | } 605 | 606 | class OuterComponent extends Component { 607 | constructor() { 608 | super() 609 | this.state = { foo: 'FOO' } 610 | } 611 | 612 | setFoo(foo) { 613 | this.setState({ foo }) 614 | } 615 | 616 | render() { 617 | return ( 618 |
619 | 620 |
621 | ) 622 | } 623 | } 624 | 625 | let outerComponent 626 | rtl.render( 627 | 628 | (outerComponent = c)} /> 629 | 630 | ) 631 | outerComponent.setFoo('BAR') 632 | outerComponent.setFoo('DID') 633 | 634 | expect(invocationCount).toEqual(3) 635 | }) 636 | 637 | it('should invoke mapState every time props are changed if it has a second argument', () => { 638 | const store = createStore(stringBuilder) 639 | 640 | let propsPassedIn 641 | let invocationCount = 0 642 | 643 | @connect((state, props) => { 644 | invocationCount++ 645 | propsPassedIn = props 646 | return {} 647 | }) 648 | class WithProps extends Component { 649 | render() { 650 | return 651 | } 652 | } 653 | 654 | class OuterComponent extends Component { 655 | constructor() { 656 | super() 657 | this.state = { foo: 'FOO' } 658 | } 659 | 660 | setFoo(foo) { 661 | this.setState({ foo }) 662 | } 663 | 664 | render() { 665 | return ( 666 |
667 | 668 |
669 | ) 670 | } 671 | } 672 | 673 | let outerComponent 674 | rtl.render( 675 | 676 | (outerComponent = c)} /> 677 | 678 | ) 679 | 680 | outerComponent.setFoo('BAR') 681 | outerComponent.setFoo('BAZ') 682 | 683 | expect(invocationCount).toEqual(3) 684 | expect(propsPassedIn).toEqual({ 685 | foo: 'BAZ' 686 | }) 687 | }) 688 | 689 | it('should not invoke mapDispatch when props change if it only has one argument', () => { 690 | const store = createStore(stringBuilder) 691 | 692 | let invocationCount = 0 693 | 694 | /*eslint-disable no-unused-vars */ 695 | @connect( 696 | null, 697 | arg1 => { 698 | invocationCount++ 699 | return {} 700 | } 701 | ) 702 | /*eslint-enable no-unused-vars */ 703 | class WithoutProps extends Component { 704 | render() { 705 | return 706 | } 707 | } 708 | 709 | class OuterComponent extends Component { 710 | constructor() { 711 | super() 712 | this.state = { foo: 'FOO' } 713 | } 714 | 715 | setFoo(foo) { 716 | this.setState({ foo }) 717 | } 718 | 719 | render() { 720 | return ( 721 |
722 | 723 |
724 | ) 725 | } 726 | } 727 | 728 | let outerComponent 729 | rtl.render( 730 | 731 | (outerComponent = c)} /> 732 | 733 | ) 734 | 735 | outerComponent.setFoo('BAR') 736 | outerComponent.setFoo('DID') 737 | 738 | expect(invocationCount).toEqual(1) 739 | }) 740 | 741 | it('should invoke mapDispatch every time props are changed if it has zero arguments', () => { 742 | const store = createStore(stringBuilder) 743 | 744 | let invocationCount = 0 745 | 746 | @connect( 747 | null, 748 | () => { 749 | invocationCount++ 750 | return {} 751 | } 752 | ) 753 | class WithoutProps extends Component { 754 | render() { 755 | return 756 | } 757 | } 758 | 759 | class OuterComponent extends Component { 760 | constructor() { 761 | super() 762 | this.state = { foo: 'FOO' } 763 | } 764 | 765 | setFoo(foo) { 766 | this.setState({ foo }) 767 | } 768 | 769 | render() { 770 | return ( 771 |
772 | 773 |
774 | ) 775 | } 776 | } 777 | 778 | let outerComponent 779 | rtl.render( 780 | 781 | (outerComponent = c)} /> 782 | 783 | ) 784 | 785 | rtl.act(() => { 786 | outerComponent.setFoo('BAR') 787 | }) 788 | expect(invocationCount).toEqual(2) 789 | 790 | rtl.act(() => { 791 | outerComponent.setFoo('DID') 792 | }) 793 | expect(invocationCount).toEqual(3) 794 | }) 795 | 796 | it('should invoke mapDispatch every time props are changed if it has a second argument', () => { 797 | const store = createStore(stringBuilder) 798 | 799 | let propsPassedIn 800 | let invocationCount = 0 801 | 802 | @connect( 803 | null, 804 | (dispatch, props) => { 805 | invocationCount++ 806 | propsPassedIn = props 807 | return {} 808 | } 809 | ) 810 | class WithProps extends Component { 811 | render() { 812 | return 813 | } 814 | } 815 | 816 | class OuterComponent extends Component { 817 | constructor() { 818 | super() 819 | this.state = { foo: 'FOO' } 820 | } 821 | 822 | setFoo(foo) { 823 | this.setState({ foo }) 824 | } 825 | 826 | render() { 827 | return ( 828 |
829 | 830 |
831 | ) 832 | } 833 | } 834 | 835 | let outerComponent 836 | rtl.render( 837 | 838 | (outerComponent = c)} /> 839 | 840 | ) 841 | 842 | rtl.act(() => { 843 | outerComponent.setFoo('BAR') 844 | }) 845 | expect(invocationCount).toEqual(2) 846 | expect(propsPassedIn).toEqual({ 847 | foo: 'BAR' 848 | }) 849 | 850 | rtl.act(() => { 851 | outerComponent.setFoo('BAZ') 852 | }) 853 | expect(invocationCount).toEqual(3) 854 | expect(propsPassedIn).toEqual({ 855 | foo: 'BAZ' 856 | }) 857 | }) 858 | 859 | it('should pass dispatch and avoid subscription if arguments are falsy', () => { 860 | const store = createStore(() => ({ 861 | foo: 'bar' 862 | })) 863 | 864 | function runCheck(...connectArgs) { 865 | @connect(...connectArgs) 866 | class Container extends Component { 867 | render() { 868 | return 869 | } 870 | } 871 | 872 | const tester = rtl.render( 873 | 874 | 875 | 876 | ) 877 | expect(tester.getByTestId('dispatch')).toHaveTextContent( 878 | '[function dispatch]' 879 | ) 880 | expect(tester.queryByTestId('foo')).toBe(null) 881 | expect(tester.getByTestId('pass')).toHaveTextContent('through') 882 | } 883 | 884 | runCheck() 885 | runCheck(null, null, null) 886 | runCheck(false, false, false) 887 | }) 888 | 889 | it('should not attempt to set state after unmounting', () => { 890 | const store = createStore(stringBuilder) 891 | let mapStateToPropsCalls = 0 892 | 893 | @connect( 894 | () => ({ calls: ++mapStateToPropsCalls }), 895 | dispatch => ({ dispatch }) 896 | ) 897 | class Container extends Component { 898 | render() { 899 | return 900 | } 901 | } 902 | 903 | const div = document.createElement('div') 904 | store.subscribe(rtl.cleanup) 905 | rtl.render( 906 | 907 | 908 | , 909 | div 910 | ) 911 | 912 | expect(mapStateToPropsCalls).toBe(1) 913 | const spy = jest 914 | .spyOn(console, 'error') 915 | .mockImplementation(Function.prototype) 916 | rtl.act(() => { 917 | store.dispatch({ type: 'APPEND', body: 'a' }) 918 | }) 919 | expect(spy).toHaveBeenCalledTimes(1) 920 | expect(mapStateToPropsCalls).toBe(1) 921 | spy.mockRestore() 922 | }) 923 | 924 | it('should not attempt to notify unmounted child of state change', () => { 925 | const store = createStore(stringBuilder) 926 | 927 | @connect(state => ({ hide: state === 'AB' })) 928 | class App extends Component { 929 | render() { 930 | return this.props.hide ? null : 931 | } 932 | } 933 | 934 | @connect(() => ({})) 935 | class Container extends Component { 936 | render() { 937 | return 938 | } 939 | } 940 | 941 | @connect(state => ({ state })) 942 | class Child extends Component { 943 | componentDidMount() { 944 | if (this.props.state === 'A') { 945 | store.dispatch({ type: 'APPEND', body: 'B' }) 946 | } 947 | } 948 | render() { 949 | return null 950 | } 951 | } 952 | 953 | const div = document.createElement('div') 954 | rtl.render( 955 | 956 | 957 | , 958 | div 959 | ) 960 | 961 | try { 962 | rtl.act(() => { 963 | store.dispatch({ type: 'APPEND', body: 'A' }) 964 | }) 965 | } finally { 966 | rtl.cleanup() 967 | } 968 | }) 969 | 970 | it('should not attempt to set state after unmounting nested components', () => { 971 | const store = createStore(() => ({})) 972 | let mapStateToPropsCalls = 0 973 | 974 | let linkA, linkB 975 | 976 | let App = ({ children, setLocation }) => { 977 | const onClick = to => event => { 978 | event.preventDefault() 979 | setLocation(to) 980 | } 981 | /* eslint-disable react/jsx-no-bind */ 982 | return ( 983 | 1004 | ) 1005 | /* eslint-enable react/jsx-no-bind */ 1006 | } 1007 | App = connect(() => ({}))(App) 1008 | 1009 | let A = () =>

A

1010 | A = connect(() => ({ calls: ++mapStateToPropsCalls }))(A) 1011 | 1012 | const B = () =>

B

1013 | 1014 | class RouterMock extends React.Component { 1015 | constructor(...args) { 1016 | super(...args) 1017 | this.state = { location: { pathname: 'a' } } 1018 | this.setLocation = this.setLocation.bind(this) 1019 | } 1020 | 1021 | setLocation(pathname) { 1022 | this.setState({ location: { pathname } }) 1023 | store.dispatch({ type: 'TEST' }) 1024 | } 1025 | 1026 | getChildComponent(location) { 1027 | switch (location) { 1028 | case 'a': 1029 | return 1030 | case 'b': 1031 | return 1032 | default: 1033 | throw new Error('Unknown location: ' + location) 1034 | } 1035 | } 1036 | 1037 | render() { 1038 | return ( 1039 | 1040 | {this.getChildComponent(this.state.location.pathname)} 1041 | 1042 | ) 1043 | } 1044 | } 1045 | 1046 | const div = document.createElement('div') 1047 | document.body.appendChild(div) 1048 | rtl.render( 1049 | 1050 | 1051 | , 1052 | div 1053 | ) 1054 | 1055 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1056 | 1057 | linkA.click() 1058 | linkB.click() 1059 | linkB.click() 1060 | 1061 | rtl.cleanup() 1062 | expect(mapStateToPropsCalls).toBe(2) 1063 | expect(spy).toHaveBeenCalledTimes(0) 1064 | spy.mockRestore() 1065 | }) 1066 | 1067 | it('should not attempt to set state when dispatching in componentWillUnmount', () => { 1068 | const store = createStore(stringBuilder) 1069 | let mapStateToPropsCalls = 0 1070 | 1071 | /*eslint-disable no-unused-vars */ 1072 | @connect( 1073 | state => ({ calls: mapStateToPropsCalls++ }), 1074 | dispatch => ({ dispatch }) 1075 | ) 1076 | /*eslint-enable no-unused-vars */ 1077 | class Container extends Component { 1078 | componentWillUnmount() { 1079 | this.props.dispatch({ type: 'APPEND', body: 'a' }) 1080 | } 1081 | render() { 1082 | return 1083 | } 1084 | } 1085 | 1086 | const div = document.createElement('div') 1087 | rtl.render( 1088 | 1089 | 1090 | , 1091 | div 1092 | ) 1093 | expect(mapStateToPropsCalls).toBe(1) 1094 | 1095 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1096 | rtl.cleanup() 1097 | expect(spy).toHaveBeenCalledTimes(0) 1098 | expect(mapStateToPropsCalls).toBe(1) 1099 | spy.mockRestore() 1100 | }) 1101 | 1102 | it('should shallowly compare the selected state to prevent unnecessary updates', () => { 1103 | const store = createStore(stringBuilder) 1104 | const spy = jest.fn(() => ({})) 1105 | function render({ string }) { 1106 | spy() 1107 | return 1108 | } 1109 | 1110 | @connect( 1111 | state => ({ string: state }), 1112 | dispatch => ({ dispatch }) 1113 | ) 1114 | class Container extends Component { 1115 | render() { 1116 | return render(this.props) 1117 | } 1118 | } 1119 | 1120 | const tester = rtl.render( 1121 | 1122 | 1123 | 1124 | ) 1125 | expect(spy).toHaveBeenCalledTimes(1) 1126 | expect(tester.getByTestId('string')).toHaveTextContent('') 1127 | rtl.act(() => { 1128 | store.dispatch({ type: 'APPEND', body: 'a' }) 1129 | }) 1130 | expect(spy).toHaveBeenCalledTimes(2) 1131 | rtl.act(() => { 1132 | store.dispatch({ type: 'APPEND', body: 'b' }) 1133 | }) 1134 | expect(spy).toHaveBeenCalledTimes(3) 1135 | rtl.act(() => { 1136 | store.dispatch({ type: 'APPEND', body: '' }) 1137 | }) 1138 | expect(spy).toHaveBeenCalledTimes(3) 1139 | }) 1140 | 1141 | it('should shallowly compare the merged state to prevent unnecessary updates', () => { 1142 | const store = createStore(stringBuilder) 1143 | const spy = jest.fn(() => ({})) 1144 | const tree = {} 1145 | function render({ string, pass }) { 1146 | spy() 1147 | return 1148 | } 1149 | 1150 | @connect( 1151 | state => ({ string: state }), 1152 | dispatch => ({ dispatch }), 1153 | (stateProps, dispatchProps, parentProps) => ({ 1154 | ...dispatchProps, 1155 | ...stateProps, 1156 | ...parentProps 1157 | }) 1158 | ) 1159 | class Container extends Component { 1160 | render() { 1161 | return render(this.props) 1162 | } 1163 | } 1164 | 1165 | class Root extends Component { 1166 | constructor(props) { 1167 | super(props) 1168 | this.state = { pass: '' } 1169 | tree.setState = this.setState.bind(this) 1170 | } 1171 | 1172 | render() { 1173 | return ( 1174 | 1175 | 1176 | 1177 | ) 1178 | } 1179 | } 1180 | 1181 | const tester = rtl.render() 1182 | expect(spy).toHaveBeenCalledTimes(1) 1183 | expect(tester.getByTestId('string')).toHaveTextContent('') 1184 | expect(tester.getByTestId('pass')).toHaveTextContent('') 1185 | 1186 | rtl.act(() => { 1187 | store.dispatch({ type: 'APPEND', body: 'a' }) 1188 | }) 1189 | expect(spy).toHaveBeenCalledTimes(2) 1190 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1191 | expect(tester.getByTestId('pass')).toHaveTextContent('') 1192 | 1193 | rtl.act(() => { 1194 | tree.setState({ pass: '' }) 1195 | }) 1196 | expect(spy).toHaveBeenCalledTimes(2) 1197 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1198 | expect(tester.getByTestId('pass')).toHaveTextContent('') 1199 | 1200 | rtl.act(() => { 1201 | tree.setState({ pass: 'through' }) 1202 | }) 1203 | expect(spy).toHaveBeenCalledTimes(3) 1204 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1205 | expect(tester.getByTestId('pass')).toHaveTextContent('through') 1206 | 1207 | rtl.act(() => { 1208 | tree.setState({ pass: 'through' }) 1209 | }) 1210 | expect(spy).toHaveBeenCalledTimes(3) 1211 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1212 | expect(tester.getByTestId('pass')).toHaveTextContent('through') 1213 | 1214 | const obj = { prop: 'val' } 1215 | rtl.act(() => { 1216 | tree.setState({ pass: obj }) 1217 | }) 1218 | expect(spy).toHaveBeenCalledTimes(4) 1219 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1220 | expect(tester.getByTestId('pass')).toHaveTextContent('{"prop":"val"}') 1221 | 1222 | rtl.act(() => { 1223 | tree.setState({ pass: obj }) 1224 | }) 1225 | expect(spy).toHaveBeenCalledTimes(4) 1226 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1227 | expect(tester.getByTestId('pass')).toHaveTextContent('{"prop":"val"}') 1228 | 1229 | const obj2 = Object.assign({}, obj, { val: 'otherval' }) 1230 | rtl.act(() => { 1231 | tree.setState({ pass: obj2 }) 1232 | }) 1233 | expect(spy).toHaveBeenCalledTimes(5) 1234 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1235 | expect(tester.getByTestId('pass')).toHaveTextContent( 1236 | '{"prop":"val","val":"otherval"}' 1237 | ) 1238 | 1239 | obj2.val = 'mutation' 1240 | rtl.act(() => { 1241 | tree.setState({ pass: obj2 }) 1242 | }) 1243 | expect(spy).toHaveBeenCalledTimes(5) 1244 | expect(tester.getByTestId('string')).toHaveTextContent('a') 1245 | expect(tester.getByTestId('pass')).toHaveTextContent( 1246 | '{"prop":"val","val":"otherval"}' 1247 | ) 1248 | }) 1249 | 1250 | it('should throw an error if a component is not passed to the function returned by connect', () => { 1251 | expect(connect()).toThrow(/You must pass a component to the function/) 1252 | }) 1253 | 1254 | it('should throw an error if mapState, mapDispatch, or mergeProps returns anything but a plain object', () => { 1255 | const store = createStore(() => ({})) 1256 | 1257 | function makeContainer(mapState, mapDispatch, mergeProps) { 1258 | @connect( 1259 | mapState, 1260 | mapDispatch, 1261 | mergeProps 1262 | ) 1263 | class Container extends Component { 1264 | render() { 1265 | return 1266 | } 1267 | } 1268 | return React.createElement(Container) 1269 | } 1270 | 1271 | function AwesomeMap() {} 1272 | 1273 | let spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1274 | rtl.render( 1275 | 1276 | {makeContainer(() => 1, () => ({}), () => ({}))} 1277 | 1278 | ) 1279 | expect(spy).toHaveBeenCalledTimes(1) 1280 | expect(spy.mock.calls[0][0]).toMatch( 1281 | /mapStateToProps\(\) in Connect\(Container\) must return a plain object/ 1282 | ) 1283 | spy.mockRestore() 1284 | rtl.cleanup() 1285 | 1286 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1287 | rtl.render( 1288 | 1289 | {makeContainer(() => 'hey', () => ({}), () => ({}))} 1290 | 1291 | ) 1292 | expect(spy).toHaveBeenCalledTimes(1) 1293 | expect(spy.mock.calls[0][0]).toMatch( 1294 | /mapStateToProps\(\) in Connect\(Container\) must return a plain object/ 1295 | ) 1296 | spy.mockRestore() 1297 | rtl.cleanup() 1298 | 1299 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1300 | rtl.render( 1301 | 1302 | {makeContainer(() => new AwesomeMap(), () => ({}), () => ({}))} 1303 | 1304 | ) 1305 | expect(spy).toHaveBeenCalledTimes(1) 1306 | expect(spy.mock.calls[0][0]).toMatch( 1307 | /mapStateToProps\(\) in Connect\(Container\) must return a plain object/ 1308 | ) 1309 | spy.mockRestore() 1310 | rtl.cleanup() 1311 | 1312 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1313 | rtl.render( 1314 | 1315 | {makeContainer(() => ({}), () => 1, () => ({}))} 1316 | 1317 | ) 1318 | expect(spy).toHaveBeenCalledTimes(1) 1319 | expect(spy.mock.calls[0][0]).toMatch( 1320 | /mapDispatchToProps\(\) in Connect\(Container\) must return a plain object/ 1321 | ) 1322 | spy.mockRestore() 1323 | rtl.cleanup() 1324 | 1325 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1326 | rtl.render( 1327 | 1328 | {makeContainer(() => ({}), () => 'hey', () => ({}))} 1329 | 1330 | ) 1331 | expect(spy).toHaveBeenCalledTimes(1) 1332 | expect(spy.mock.calls[0][0]).toMatch( 1333 | /mapDispatchToProps\(\) in Connect\(Container\) must return a plain object/ 1334 | ) 1335 | spy.mockRestore() 1336 | rtl.cleanup() 1337 | 1338 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1339 | rtl.render( 1340 | 1341 | {makeContainer(() => ({}), () => new AwesomeMap(), () => ({}))} 1342 | 1343 | ) 1344 | expect(spy).toHaveBeenCalledTimes(1) 1345 | expect(spy.mock.calls[0][0]).toMatch( 1346 | /mapDispatchToProps\(\) in Connect\(Container\) must return a plain object/ 1347 | ) 1348 | spy.mockRestore() 1349 | rtl.cleanup() 1350 | 1351 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1352 | rtl.render( 1353 | 1354 | {makeContainer(() => ({}), () => ({}), () => 1)} 1355 | 1356 | ) 1357 | expect(spy).toHaveBeenCalledTimes(1) 1358 | expect(spy.mock.calls[0][0]).toMatch( 1359 | /mergeProps\(\) in Connect\(Container\) must return a plain object/ 1360 | ) 1361 | spy.mockRestore() 1362 | rtl.cleanup() 1363 | 1364 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1365 | rtl.render( 1366 | 1367 | {makeContainer(() => ({}), () => ({}), () => 'hey')} 1368 | 1369 | ) 1370 | expect(spy).toHaveBeenCalledTimes(1) 1371 | expect(spy.mock.calls[0][0]).toMatch( 1372 | /mergeProps\(\) in Connect\(Container\) must return a plain object/ 1373 | ) 1374 | spy.mockRestore() 1375 | rtl.cleanup() 1376 | 1377 | spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1378 | rtl.render( 1379 | 1380 | {makeContainer(() => ({}), () => ({}), () => new AwesomeMap())} 1381 | 1382 | ) 1383 | expect(spy).toHaveBeenCalledTimes(1) 1384 | expect(spy.mock.calls[0][0]).toMatch( 1385 | /mergeProps\(\) in Connect\(Container\) must return a plain object/ 1386 | ) 1387 | spy.mockRestore() 1388 | }) 1389 | it.skip('should recalculate the state and rebind the actions on hot update', () => { 1390 | const store = createStore(() => {}) 1391 | @connect( 1392 | null, 1393 | () => ({ scooby: 'doo' }) 1394 | ) 1395 | class ContainerBefore extends Component { 1396 | render() { 1397 | return 1398 | } 1399 | } 1400 | @connect( 1401 | () => ({ foo: 'baz' }), 1402 | () => ({ scooby: 'foo' }) 1403 | ) 1404 | class ContainerAfter extends Component { 1405 | render() { 1406 | return 1407 | } 1408 | } 1409 | @connect( 1410 | () => ({ foo: 'bar' }), 1411 | () => ({ scooby: 'boo' }) 1412 | ) 1413 | class ContainerNext extends Component { 1414 | render() { 1415 | return 1416 | } 1417 | } 1418 | let container 1419 | const tester = rtl.render( 1420 | 1421 | (container = instance)} /> 1422 | 1423 | ) 1424 | expect(tester.queryByTestId('foo')).toBe(null) 1425 | expect(tester.getByTestId('scooby')).toHaveTextContent('doo') 1426 | imitateHotReloading(ContainerBefore, ContainerAfter, container) 1427 | expect(tester.getByTestId('foo')).toHaveTextContent('baz') 1428 | expect(tester.getByTestId('scooby')).toHaveTextContent('foo') 1429 | imitateHotReloading(ContainerBefore, ContainerNext, container) 1430 | expect(tester.getByTestId('foo')).toHaveTextContent('bar') 1431 | expect(tester.getByTestId('scooby')).toHaveTextContent('boo') 1432 | }) 1433 | 1434 | it.skip('should persist listeners through hot update', () => { 1435 | const ACTION_TYPE = 'ACTION' 1436 | const store = createStore((state = { actions: 0 }, action) => { 1437 | switch (action.type) { 1438 | case ACTION_TYPE: { 1439 | return { 1440 | actions: state.actions + 1 1441 | } 1442 | } 1443 | default: 1444 | return state 1445 | } 1446 | }) 1447 | 1448 | @connect(state => ({ actions: state.actions })) 1449 | class Child extends Component { 1450 | render() { 1451 | return 1452 | } 1453 | } 1454 | 1455 | @connect(() => ({ scooby: 'doo' })) 1456 | class ParentBefore extends Component { 1457 | render() { 1458 | return 1459 | } 1460 | } 1461 | 1462 | @connect(() => ({ scooby: 'boo' })) 1463 | class ParentAfter extends Component { 1464 | render() { 1465 | return 1466 | } 1467 | } 1468 | 1469 | let container 1470 | const tester = rtl.render( 1471 | 1472 | (container = instance)} /> 1473 | 1474 | ) 1475 | 1476 | imitateHotReloading(ParentBefore, ParentAfter, container) 1477 | 1478 | store.dispatch({ type: ACTION_TYPE }) 1479 | 1480 | expect(tester.getByTestId('actions')).toHaveTextContent('1') 1481 | }) 1482 | 1483 | it('should set the displayName correctly', () => { 1484 | expect( 1485 | connect(state => state)( 1486 | class Foo extends Component { 1487 | render() { 1488 | return
1489 | } 1490 | } 1491 | ).displayName 1492 | ).toBe('Connect(Foo)') 1493 | 1494 | expect( 1495 | connect(state => state)( 1496 | createClass({ 1497 | displayName: 'Bar', 1498 | render() { 1499 | return
1500 | } 1501 | }) 1502 | ).displayName 1503 | ).toBe('Connect(Bar)') 1504 | 1505 | expect( 1506 | connect(state => state)( 1507 | // eslint: In this case, we don't want to specify a displayName because we're testing what 1508 | // happens when one isn't defined. 1509 | /* eslint-disable react/display-name */ 1510 | createClass({ 1511 | render() { 1512 | return
1513 | } 1514 | }) 1515 | /* eslint-enable react/display-name */ 1516 | ).displayName 1517 | ).toBe('Connect(Component)') 1518 | }) 1519 | 1520 | it('should expose the wrapped component as WrappedComponent', () => { 1521 | class Container extends Component { 1522 | render() { 1523 | return 1524 | } 1525 | } 1526 | 1527 | const decorator = connect(state => state) 1528 | const decorated = decorator(Container) 1529 | 1530 | expect(decorated.WrappedComponent).toBe(Container) 1531 | }) 1532 | 1533 | it('should hoist non-react statics from wrapped component', () => { 1534 | class Container extends Component { 1535 | render() { 1536 | return 1537 | } 1538 | } 1539 | 1540 | Container.howIsRedux = () => 'Awesome!' 1541 | Container.foo = 'bar' 1542 | 1543 | const decorator = connect(state => state) 1544 | const decorated = decorator(Container) 1545 | 1546 | expect(decorated.howIsRedux).toBeInstanceOf(Function) 1547 | expect(decorated.howIsRedux()).toBe('Awesome!') 1548 | expect(decorated.foo).toBe('bar') 1549 | }) 1550 | 1551 | xit('should use a custom context provider and consumer if given as an option to connect', () => { 1552 | class Container extends Component { 1553 | render() { 1554 | return 1555 | } 1556 | } 1557 | 1558 | const context = React.createContext(null) 1559 | 1560 | let actualState 1561 | 1562 | const expectedState = { foos: {} } 1563 | const ignoredState = { bars: {} } 1564 | 1565 | const decorator = connect( 1566 | state => { 1567 | actualState = state 1568 | return {} 1569 | }, 1570 | undefined, 1571 | undefined, 1572 | { context } 1573 | ) 1574 | const Decorated = decorator(Container) 1575 | 1576 | const store1 = createStore(() => expectedState) 1577 | const store2 = createStore(() => ignoredState) 1578 | 1579 | rtl.render( 1580 | 1581 | 1582 | 1583 | 1584 | 1585 | ) 1586 | 1587 | expect(actualState).toEqual(expectedState) 1588 | }) 1589 | 1590 | xit('should use a custom context provider and consumer if passed as a prop to the component', () => { 1591 | class Container extends Component { 1592 | render() { 1593 | return 1594 | } 1595 | } 1596 | 1597 | const context = React.createContext(null) 1598 | 1599 | let actualState 1600 | 1601 | const expectedState = { foos: {} } 1602 | const ignoredState = { bars: {} } 1603 | 1604 | const decorator = connect(state => { 1605 | actualState = state 1606 | return {} 1607 | }) 1608 | const Decorated = decorator(Container) 1609 | 1610 | const store1 = createStore(() => expectedState) 1611 | const store2 = createStore(() => ignoredState) 1612 | 1613 | rtl.render( 1614 | 1615 | 1616 | 1617 | 1618 | 1619 | ) 1620 | 1621 | expect(actualState).toEqual(expectedState) 1622 | }) 1623 | 1624 | xit('should ignore non-react-context values that are passed as a prop to the component', () => { 1625 | class Container extends Component { 1626 | render() { 1627 | return 1628 | } 1629 | } 1630 | 1631 | const nonContext = { someProperty: {} } 1632 | 1633 | let actualState 1634 | 1635 | const expectedState = { foos: {} } 1636 | 1637 | const decorator = connect(state => { 1638 | actualState = state 1639 | return {} 1640 | }) 1641 | const Decorated = decorator(Container) 1642 | 1643 | const store = createStore(() => expectedState) 1644 | 1645 | rtl.render( 1646 | 1647 | 1648 | 1649 | ) 1650 | 1651 | expect(actualState).toEqual(expectedState) 1652 | }) 1653 | 1654 | it('should throw an error if the store is not in the props or context', () => { 1655 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1656 | 1657 | class Container extends Component { 1658 | render() { 1659 | return 1660 | } 1661 | } 1662 | 1663 | const decorator = connect(() => {}) 1664 | const Decorated = decorator(Container) 1665 | 1666 | expect(() => rtl.render()).toThrow(/Could not find "store"/) 1667 | 1668 | spy.mockRestore() 1669 | }) 1670 | 1671 | it.skip('should throw when trying to access the wrapped instance if withRef is not specified', () => { 1672 | const store = createStore(() => ({})) 1673 | 1674 | class Container extends Component { 1675 | render() { 1676 | return 1677 | } 1678 | } 1679 | 1680 | const decorator = connect(state => state) 1681 | const Decorated = decorator(Container) 1682 | 1683 | class Wrapper extends Component { 1684 | render() { 1685 | return comp && comp.getWrappedInstance()} /> 1686 | } 1687 | } 1688 | 1689 | // TODO Remove this when React is fixed, per https://github.com/facebook/react/issues/11098 1690 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 1691 | expect(() => 1692 | rtl.render( 1693 | 1694 | 1695 | 1696 | ) 1697 | ).toThrow( 1698 | `To access the wrapped instance, you need to specify { withRef: true } in the options argument of the connect() call` 1699 | ) 1700 | spy.mockRestore() 1701 | }) 1702 | 1703 | it('should return the instance of the wrapped component for use in calling child methods', async done => { 1704 | const store = createStore(() => ({})) 1705 | 1706 | const someData = { 1707 | some: 'data' 1708 | } 1709 | 1710 | class Container extends Component { 1711 | someInstanceMethod() { 1712 | return someData 1713 | } 1714 | 1715 | render() { 1716 | return 1717 | } 1718 | } 1719 | 1720 | const decorator = connect( 1721 | state => state, 1722 | null, 1723 | null, 1724 | { forwardRef: true } 1725 | ) 1726 | const Decorated = decorator(Container) 1727 | 1728 | const ref = React.createRef() 1729 | 1730 | class Wrapper extends Component { 1731 | render() { 1732 | return 1733 | } 1734 | } 1735 | 1736 | const tester = rtl.render( 1737 | 1738 | 1739 | 1740 | ) 1741 | 1742 | await rtl.waitForElement(() => tester.getByTestId('loaded')) 1743 | 1744 | expect(ref.current.someInstanceMethod()).toBe(someData) 1745 | done() 1746 | }) 1747 | 1748 | xit('should return the instance of the wrapped component for use in calling child methods, impure component', async done => { 1749 | const store = createStore(() => ({})) 1750 | 1751 | const someData = { 1752 | some: 'data' 1753 | } 1754 | 1755 | class Container extends Component { 1756 | someInstanceMethod() { 1757 | return someData 1758 | } 1759 | 1760 | render() { 1761 | return 1762 | } 1763 | } 1764 | 1765 | const decorator = connect( 1766 | state => state, 1767 | undefined, 1768 | undefined, 1769 | { forwardRef: true, pure: false } 1770 | ) 1771 | const Decorated = decorator(Container) 1772 | 1773 | const ref = React.createRef() 1774 | 1775 | class Wrapper extends Component { 1776 | render() { 1777 | return 1778 | } 1779 | } 1780 | 1781 | const tester = rtl.render( 1782 | 1783 | 1784 | 1785 | ) 1786 | 1787 | await rtl.waitForElement(() => tester.getByTestId('loaded')) 1788 | 1789 | expect(ref.current.someInstanceMethod()).toBe(someData) 1790 | done() 1791 | }) 1792 | 1793 | xit('should wrap impure components without supressing updates', () => { 1794 | const store = createStore(() => ({})) 1795 | 1796 | class ImpureComponent extends Component { 1797 | render() { 1798 | return 1799 | } 1800 | } 1801 | 1802 | ImpureComponent.contextTypes = { 1803 | statefulValue: PropTypes.number 1804 | } 1805 | 1806 | const decorator = connect( 1807 | state => state, 1808 | null, 1809 | null, 1810 | { pure: false } 1811 | ) 1812 | const Decorated = decorator(ImpureComponent) 1813 | 1814 | let externalSetState 1815 | class StatefulWrapper extends Component { 1816 | constructor() { 1817 | super() 1818 | this.state = { value: 0 } 1819 | externalSetState = this.setState.bind(this) 1820 | } 1821 | 1822 | getChildContext() { 1823 | return { 1824 | statefulValue: this.state.value 1825 | } 1826 | } 1827 | 1828 | render() { 1829 | return 1830 | } 1831 | } 1832 | 1833 | StatefulWrapper.childContextTypes = { 1834 | statefulValue: PropTypes.number 1835 | } 1836 | 1837 | const tester = rtl.render( 1838 | 1839 | 1840 | 1841 | ) 1842 | 1843 | expect(tester.getByTestId('statefulValue')).toHaveTextContent('0') 1844 | externalSetState({ value: 1 }) 1845 | expect(tester.getByTestId('statefulValue')).toHaveTextContent('1') 1846 | }) 1847 | 1848 | xit('calls mapState and mapDispatch for impure components', () => { 1849 | const store = createStore(() => ({ 1850 | foo: 'foo', 1851 | bar: 'bar' 1852 | })) 1853 | 1854 | const mapStateSpy = jest.fn() 1855 | const mapDispatchSpy = jest.fn().mockReturnValue({}) 1856 | 1857 | class ImpureComponent extends Component { 1858 | render() { 1859 | return 1860 | } 1861 | } 1862 | 1863 | const decorator = connect( 1864 | (state, { storeGetter }) => { 1865 | mapStateSpy() 1866 | return { value: state[storeGetter.storeKey] } 1867 | }, 1868 | mapDispatchSpy, 1869 | null, 1870 | { pure: false } 1871 | ) 1872 | const Decorated = decorator(ImpureComponent) 1873 | 1874 | let externalSetState 1875 | let storeGetter 1876 | class StatefulWrapper extends Component { 1877 | constructor() { 1878 | super() 1879 | storeGetter = { storeKey: 'foo' } 1880 | this.state = { 1881 | storeGetter 1882 | } 1883 | externalSetState = this.setState.bind(this) 1884 | } 1885 | render() { 1886 | return 1887 | } 1888 | } 1889 | 1890 | const tester = rtl.render( 1891 | 1892 | 1893 | 1894 | ) 1895 | 1896 | expect(mapStateSpy).toHaveBeenCalledTimes(1) 1897 | expect(mapDispatchSpy).toHaveBeenCalledTimes(1) 1898 | expect(tester.getByTestId('statefulValue')).toHaveTextContent('foo') 1899 | 1900 | // Impure update 1901 | storeGetter.storeKey = 'bar' 1902 | externalSetState({ storeGetter }) 1903 | 1904 | expect(mapStateSpy).toHaveBeenCalledTimes(2) 1905 | expect(mapDispatchSpy).toHaveBeenCalledTimes(2) 1906 | expect(tester.getByTestId('statefulValue')).toHaveTextContent('bar') 1907 | }) 1908 | 1909 | it('should pass state consistently to mapState', () => { 1910 | const store = createStore(stringBuilder) 1911 | 1912 | store.dispatch({ type: 'APPEND', body: 'a' }) 1913 | let childMapStateInvokes = 0 1914 | 1915 | @connect(state => ({ state })) 1916 | class Container extends Component { 1917 | emitChange() { 1918 | rtl.act(() => { 1919 | store.dispatch({ type: 'APPEND', body: 'b' }) 1920 | }) 1921 | } 1922 | 1923 | render() { 1924 | return ( 1925 |
1926 | 1927 | 1928 |
1929 | ) 1930 | } 1931 | } 1932 | 1933 | const childCalls = [] 1934 | @connect((state, parentProps) => { 1935 | childMapStateInvokes++ 1936 | childCalls.push([state, parentProps.parentState]) 1937 | // The state from parent props should always be consistent with the current state 1938 | expect(state).toEqual(parentProps.parentState) 1939 | return {} 1940 | }) 1941 | class ChildContainer extends Component { 1942 | render() { 1943 | return 1944 | } 1945 | } 1946 | 1947 | const tester = rtl.render( 1948 | 1949 | 1950 | 1951 | ) 1952 | 1953 | expect(childMapStateInvokes).toBe(1) 1954 | expect(childCalls).toEqual([['a', 'a']]) 1955 | 1956 | // The store state stays consistent when setState calls are batched 1957 | ReactDOM.unstable_batchedUpdates(() => { 1958 | store.dispatch({ type: 'APPEND', body: 'c' }) 1959 | }) 1960 | expect(childMapStateInvokes).toBe(2) 1961 | expect(childCalls).toEqual([['a', 'a'], ['ac', 'ac']]) 1962 | 1963 | // setState calls DOM handlers are batched 1964 | const button = tester.getByText('change') 1965 | rtl.fireEvent.click(button) 1966 | expect(childMapStateInvokes).toBe(3) 1967 | 1968 | rtl.act(() => { 1969 | store.dispatch({ type: 'APPEND', body: 'd' }) 1970 | }) 1971 | expect(childMapStateInvokes).toBe(4) 1972 | expect(childCalls).toEqual([ 1973 | ['a', 'a'], 1974 | ['ac', 'ac'], 1975 | ['acb', 'acb'], 1976 | ['acbd', 'acbd'] 1977 | ]) 1978 | }) 1979 | 1980 | it('should not render the wrapped component when mapState does not produce change', () => { 1981 | const store = createStore(stringBuilder) 1982 | let renderCalls = 0 1983 | let mapStateCalls = 0 1984 | 1985 | @connect(() => { 1986 | mapStateCalls++ 1987 | return {} // no change! 1988 | }) 1989 | class Container extends Component { 1990 | render() { 1991 | renderCalls++ 1992 | return 1993 | } 1994 | } 1995 | 1996 | rtl.render( 1997 | 1998 | 1999 | 2000 | ) 2001 | 2002 | expect(renderCalls).toBe(1) 2003 | expect(mapStateCalls).toBe(1) 2004 | 2005 | rtl.act(() => { 2006 | store.dispatch({ type: 'APPEND', body: 'a' }) 2007 | }) 2008 | 2009 | // After store a change mapState has been called 2010 | expect(mapStateCalls).toBe(2) 2011 | // But render is not because it did not make any actual changes 2012 | expect(renderCalls).toBe(1) 2013 | }) 2014 | 2015 | it('should bail out early if mapState does not depend on props', () => { 2016 | const store = createStore(stringBuilder) 2017 | let renderCalls = 0 2018 | let mapStateCalls = 0 2019 | 2020 | @connect(state => { 2021 | mapStateCalls++ 2022 | return state === 'aaa' ? { change: 1 } : {} 2023 | }) 2024 | class Container extends Component { 2025 | render() { 2026 | renderCalls++ 2027 | return 2028 | } 2029 | } 2030 | 2031 | rtl.render( 2032 | 2033 | 2034 | 2035 | ) 2036 | 2037 | expect(renderCalls).toBe(1) 2038 | expect(mapStateCalls).toBe(1) 2039 | 2040 | rtl.act(() => { 2041 | store.dispatch({ type: 'APPEND', body: 'a' }) 2042 | }) 2043 | expect(mapStateCalls).toBe(2) 2044 | expect(renderCalls).toBe(1) 2045 | 2046 | rtl.act(() => { 2047 | store.dispatch({ type: 'APPEND', body: 'a' }) 2048 | }) 2049 | expect(mapStateCalls).toBe(3) 2050 | expect(renderCalls).toBe(1) 2051 | 2052 | rtl.act(() => { 2053 | store.dispatch({ type: 'APPEND', body: 'a' }) 2054 | }) 2055 | expect(mapStateCalls).toBe(4) 2056 | expect(renderCalls).toBe(2) 2057 | }) 2058 | 2059 | it('should not swallow errors when bailing out early', () => { 2060 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 2061 | const store = createStore(stringBuilder) 2062 | let renderCalls = 0 2063 | let mapStateCalls = 0 2064 | 2065 | @connect(state => { 2066 | mapStateCalls++ 2067 | if (state === 'a') { 2068 | throw new Error('Oops') 2069 | } else { 2070 | return {} 2071 | } 2072 | }) 2073 | class Container extends Component { 2074 | render() { 2075 | renderCalls++ 2076 | return 2077 | } 2078 | } 2079 | 2080 | rtl.render( 2081 | 2082 | 2083 | 2084 | ) 2085 | 2086 | expect(renderCalls).toBe(1) 2087 | expect(mapStateCalls).toBe(1) 2088 | expect(() => store.dispatch({ type: 'APPEND', body: 'a' })).toThrow() 2089 | 2090 | spy.mockRestore() 2091 | }) 2092 | 2093 | xit('should allow providing a factory function to mapStateToProps', () => { 2094 | let updatedCount = 0 2095 | let memoizedReturnCount = 0 2096 | const store = createStore(() => ({ value: 1 })) 2097 | 2098 | const mapStateFactory = () => { 2099 | let lastProp, lastVal, lastResult 2100 | return (state, props) => { 2101 | if (props.name === lastProp && lastVal === state.value) { 2102 | memoizedReturnCount++ 2103 | return lastResult 2104 | } 2105 | lastProp = props.name 2106 | lastVal = state.value 2107 | return (lastResult = { 2108 | someObject: { prop: props.name, stateVal: state.value } 2109 | }) 2110 | } 2111 | } 2112 | 2113 | @connect(mapStateFactory) 2114 | class Container extends Component { 2115 | componentDidUpdate() { 2116 | updatedCount++ 2117 | } 2118 | render() { 2119 | return 2120 | } 2121 | } 2122 | 2123 | rtl.render( 2124 | 2125 |
2126 | 2127 | 2128 |
2129 |
2130 | ) 2131 | 2132 | store.dispatch({ type: 'test' }) 2133 | expect(updatedCount).toBe(0) 2134 | expect(memoizedReturnCount).toBe(2) 2135 | }) 2136 | 2137 | xit('should allow a mapStateToProps factory consuming just state to return a function that gets ownProps', () => { 2138 | const store = createStore(() => ({ value: 1 })) 2139 | 2140 | let initialState 2141 | let initialOwnProps 2142 | let secondaryOwnProps 2143 | const mapStateFactory = function(factoryInitialState) { 2144 | initialState = factoryInitialState 2145 | initialOwnProps = arguments[1] 2146 | return (state, props) => { 2147 | secondaryOwnProps = props 2148 | return {} 2149 | } 2150 | } 2151 | 2152 | @connect(mapStateFactory) 2153 | class Container extends Component { 2154 | render() { 2155 | return 2156 | } 2157 | } 2158 | 2159 | rtl.render( 2160 | 2161 |
2162 | 2163 |
2164 |
2165 | ) 2166 | 2167 | store.dispatch({ type: 'test' }) 2168 | expect(initialOwnProps).toBe(undefined) 2169 | expect(initialState).not.toBe(undefined) 2170 | expect(secondaryOwnProps).not.toBe(undefined) 2171 | expect(secondaryOwnProps.name).toBe('a') 2172 | }) 2173 | 2174 | xit('should allow providing a factory function to mapDispatchToProps', () => { 2175 | let updatedCount = 0 2176 | let memoizedReturnCount = 0 2177 | const store = createStore(() => ({ value: 1 })) 2178 | 2179 | const mapDispatchFactory = () => { 2180 | let lastProp, lastResult 2181 | return (dispatch, props) => { 2182 | if (props.name === lastProp) { 2183 | memoizedReturnCount++ 2184 | return lastResult 2185 | } 2186 | lastProp = props.name 2187 | return (lastResult = { someObject: { dispatchFn: dispatch } }) 2188 | } 2189 | } 2190 | function mergeParentDispatch(stateProps, dispatchProps, parentProps) { 2191 | return { ...stateProps, ...dispatchProps, name: parentProps.name } 2192 | } 2193 | 2194 | @connect( 2195 | null, 2196 | mapDispatchFactory, 2197 | mergeParentDispatch 2198 | ) 2199 | class Passthrough extends Component { 2200 | componentDidUpdate() { 2201 | updatedCount++ 2202 | } 2203 | render() { 2204 | return
2205 | } 2206 | } 2207 | 2208 | class Container extends Component { 2209 | constructor(props) { 2210 | super(props) 2211 | this.state = { count: 0 } 2212 | } 2213 | componentDidMount() { 2214 | this.setState({ count: 1 }) 2215 | } 2216 | render() { 2217 | const { count } = this.state 2218 | return ( 2219 |
2220 | 2221 | 2222 |
2223 | ) 2224 | } 2225 | } 2226 | 2227 | rtl.render( 2228 | 2229 | 2230 | 2231 | ) 2232 | 2233 | store.dispatch({ type: 'test' }) 2234 | expect(updatedCount).toBe(0) 2235 | expect(memoizedReturnCount).toBe(2) 2236 | }) 2237 | 2238 | it('should not call update if mergeProps return value has not changed', () => { 2239 | let mapStateCalls = 0 2240 | let renderCalls = 0 2241 | const store = createStore(stringBuilder) 2242 | 2243 | @connect( 2244 | () => ({ a: ++mapStateCalls }), 2245 | null, 2246 | () => ({ changed: false }) 2247 | ) 2248 | class Container extends Component { 2249 | render() { 2250 | renderCalls++ 2251 | return 2252 | } 2253 | } 2254 | 2255 | rtl.render( 2256 | 2257 | 2258 | 2259 | ) 2260 | 2261 | expect(renderCalls).toBe(1) 2262 | expect(mapStateCalls).toBe(1) 2263 | 2264 | rtl.act(() => { 2265 | store.dispatch({ type: 'APPEND', body: 'a' }) 2266 | }) 2267 | 2268 | expect(mapStateCalls).toBe(2) 2269 | expect(renderCalls).toBe(1) 2270 | }) 2271 | 2272 | xit('should update impure components with custom mergeProps', () => { 2273 | let store = createStore(() => ({})) 2274 | let renderCount = 0 2275 | 2276 | @connect( 2277 | null, 2278 | null, 2279 | () => ({ a: 1 }), 2280 | { pure: false } 2281 | ) 2282 | class Container extends React.Component { 2283 | render() { 2284 | ++renderCount 2285 | return
2286 | } 2287 | } 2288 | 2289 | class Parent extends React.Component { 2290 | componentDidMount() { 2291 | this.forceUpdate() 2292 | } 2293 | render() { 2294 | return 2295 | } 2296 | } 2297 | 2298 | rtl.render( 2299 | 2300 | 2301 | 2302 | 2303 | 2304 | ) 2305 | 2306 | expect(renderCount).toBe(2) 2307 | }) 2308 | 2309 | it('should allow to clean up child state in parent componentWillUnmount', () => { 2310 | function reducer(state = { data: null }, action) { 2311 | switch (action.type) { 2312 | case 'fetch': 2313 | return { data: { profile: { name: 'April' } } } 2314 | case 'clean': 2315 | return { data: null } 2316 | default: 2317 | return state 2318 | } 2319 | } 2320 | 2321 | @connect(null) 2322 | class Parent extends React.Component { 2323 | componentWillUnmount() { 2324 | this.props.dispatch({ type: 'clean' }) 2325 | } 2326 | 2327 | render() { 2328 | return 2329 | } 2330 | } 2331 | 2332 | @connect(state => ({ 2333 | profile: state.data.profile 2334 | })) 2335 | class Child extends React.Component { 2336 | render() { 2337 | return null 2338 | } 2339 | } 2340 | 2341 | const store = createStore(reducer) 2342 | store.dispatch({ type: 'fetch' }) 2343 | const div = document.createElement('div') 2344 | ReactDOM.render( 2345 | 2346 | 2347 | , 2348 | div 2349 | ) 2350 | 2351 | ReactDOM.unmountComponentAtNode(div) 2352 | }) 2353 | 2354 | it('should allow custom displayName', () => { 2355 | @connect( 2356 | null, 2357 | null, 2358 | null, 2359 | { getDisplayName: name => `Custom(${name})` } 2360 | ) 2361 | class MyComponent extends React.Component { 2362 | render() { 2363 | return
2364 | } 2365 | } 2366 | 2367 | expect(MyComponent.displayName).toEqual('Custom(MyComponent)') 2368 | }) 2369 | 2370 | xit('should update impure components whenever the state of the store changes', () => { 2371 | const store = createStore(() => ({})) 2372 | let renderCount = 0 2373 | 2374 | @connect( 2375 | () => ({}), 2376 | null, 2377 | null, 2378 | { pure: false } 2379 | ) 2380 | class ImpureComponent extends React.Component { 2381 | render() { 2382 | ++renderCount 2383 | return
2384 | } 2385 | } 2386 | 2387 | rtl.render( 2388 | 2389 | 2390 | 2391 | ) 2392 | 2393 | const rendersBeforeStateChange = renderCount 2394 | store.dispatch({ type: 'ACTION' }) 2395 | expect(renderCount).toBe(rendersBeforeStateChange + 1) 2396 | }) 2397 | 2398 | function renderWithBadConnect(Component) { 2399 | const store = createStore(() => ({})) 2400 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 2401 | 2402 | try { 2403 | rtl.render( 2404 | 2405 | 2406 | 2407 | ) 2408 | return null 2409 | } catch (error) { 2410 | return error.message 2411 | } finally { 2412 | spy.mockRestore() 2413 | } 2414 | } 2415 | 2416 | xit('should throw a helpful error for invalid mapStateToProps arguments', () => { 2417 | @connect('invalid') 2418 | class InvalidMapState extends React.Component { 2419 | render() { 2420 | return
2421 | } 2422 | } 2423 | 2424 | const error = renderWithBadConnect(InvalidMapState) 2425 | expect(error).toContain('string') 2426 | expect(error).toContain('mapStateToProps') 2427 | expect(error).toContain('InvalidMapState') 2428 | }) 2429 | 2430 | xit('should throw a helpful error for invalid mapDispatchToProps arguments', () => { 2431 | @connect( 2432 | null, 2433 | 'invalid' 2434 | ) 2435 | class InvalidMapDispatch extends React.Component { 2436 | render() { 2437 | return
2438 | } 2439 | } 2440 | 2441 | const error = renderWithBadConnect(InvalidMapDispatch) 2442 | expect(error).toContain('string') 2443 | expect(error).toContain('mapDispatchToProps') 2444 | expect(error).toContain('InvalidMapDispatch') 2445 | }) 2446 | 2447 | xit('should throw a helpful error for invalid mergeProps arguments', () => { 2448 | @connect( 2449 | null, 2450 | null, 2451 | 'invalid' 2452 | ) 2453 | class InvalidMerge extends React.Component { 2454 | render() { 2455 | return
2456 | } 2457 | } 2458 | 2459 | const error = renderWithBadConnect(InvalidMerge) 2460 | expect(error).toContain('string') 2461 | expect(error).toContain('mergeProps') 2462 | expect(error).toContain('InvalidMerge') 2463 | }) 2464 | 2465 | it('should notify nested components through a blocking component', () => { 2466 | @connect(state => ({ count: state })) 2467 | class Parent extends Component { 2468 | render() { 2469 | return ( 2470 | 2471 | 2472 | 2473 | ) 2474 | } 2475 | } 2476 | 2477 | class BlockUpdates extends Component { 2478 | shouldComponentUpdate() { 2479 | return false 2480 | } 2481 | render() { 2482 | return this.props.children 2483 | } 2484 | } 2485 | 2486 | const mapStateToProps = jest.fn(state => ({ count: state })) 2487 | @connect(mapStateToProps) 2488 | class Child extends Component { 2489 | render() { 2490 | return
{this.props.count}
2491 | } 2492 | } 2493 | 2494 | const store = createStore((state = 0, action) => 2495 | action.type === 'INC' ? state + 1 : state 2496 | ) 2497 | rtl.render( 2498 | 2499 | 2500 | 2501 | ) 2502 | 2503 | expect(mapStateToProps).toHaveBeenCalledTimes(1) 2504 | rtl.act(() => { 2505 | store.dispatch({ type: 'INC' }) 2506 | }) 2507 | expect(mapStateToProps).toHaveBeenCalledTimes(2) 2508 | }) 2509 | 2510 | it('should subscribe properly when a middle connected component does not subscribe', () => { 2511 | @connect(state => ({ count: state })) 2512 | class A extends React.Component { 2513 | render() { 2514 | return 2515 | } 2516 | } 2517 | 2518 | @connect() // no mapStateToProps. therefore it should be transparent for subscriptions 2519 | class B extends React.Component { 2520 | render() { 2521 | return 2522 | } 2523 | } 2524 | 2525 | @connect((state, props) => { 2526 | expect(props.count).toBe(state) 2527 | return { count: state * 10 + props.count } 2528 | }) 2529 | class C extends React.Component { 2530 | render() { 2531 | return
{this.props.count}
2532 | } 2533 | } 2534 | 2535 | const store = createStore((state = 0, action) => 2536 | action.type === 'INC' ? (state += 1) : state 2537 | ) 2538 | rtl.render( 2539 | 2540 | 2541 | 2542 | ) 2543 | 2544 | rtl.act(() => { 2545 | store.dispatch({ type: 'INC' }) 2546 | }) 2547 | }) 2548 | 2549 | xit('should subscribe properly when a new store is provided via props', () => { 2550 | const store1 = createStore((state = 0, action) => 2551 | action.type === 'INC' ? state + 1 : state 2552 | ) 2553 | const store2 = createStore((state = 0, action) => 2554 | action.type === 'INC' ? state + 1 : state 2555 | ) 2556 | const customContext = React.createContext() 2557 | 2558 | @connect( 2559 | state => ({ count: state }), 2560 | undefined, 2561 | undefined, 2562 | { context: customContext } 2563 | ) 2564 | class A extends Component { 2565 | render() { 2566 | return 2567 | } 2568 | } 2569 | 2570 | const mapStateToPropsB = jest.fn(state => ({ count: state })) 2571 | @connect( 2572 | mapStateToPropsB, 2573 | undefined, 2574 | undefined, 2575 | { context: customContext } 2576 | ) 2577 | class B extends Component { 2578 | render() { 2579 | return 2580 | } 2581 | } 2582 | 2583 | const mapStateToPropsC = jest.fn(state => ({ count: state })) 2584 | @connect( 2585 | mapStateToPropsC, 2586 | undefined, 2587 | undefined, 2588 | { context: customContext } 2589 | ) 2590 | class C extends Component { 2591 | render() { 2592 | return 2593 | } 2594 | } 2595 | 2596 | const mapStateToPropsD = jest.fn(state => ({ count: state })) 2597 | @connect(mapStateToPropsD) 2598 | class D extends Component { 2599 | render() { 2600 | return
{this.props.count}
2601 | } 2602 | } 2603 | 2604 | rtl.render( 2605 | 2606 | 2607 |
2608 | 2609 | 2610 | ) 2611 | expect(mapStateToPropsB).toHaveBeenCalledTimes(1) 2612 | expect(mapStateToPropsC).toHaveBeenCalledTimes(1) 2613 | expect(mapStateToPropsD).toHaveBeenCalledTimes(1) 2614 | 2615 | rtl.act(() => { 2616 | store1.dispatch({ type: 'INC' }) 2617 | }) 2618 | expect(mapStateToPropsB).toHaveBeenCalledTimes(1) 2619 | expect(mapStateToPropsC).toHaveBeenCalledTimes(1) 2620 | expect(mapStateToPropsD).toHaveBeenCalledTimes(2) 2621 | 2622 | rtl.act(() => { 2623 | store2.dispatch({ type: 'INC' }) 2624 | }) 2625 | expect(mapStateToPropsB).toHaveBeenCalledTimes(2) 2626 | expect(mapStateToPropsC).toHaveBeenCalledTimes(2) 2627 | expect(mapStateToPropsD).toHaveBeenCalledTimes(2) 2628 | }) 2629 | 2630 | it('works in without warnings (React 16.3+)', () => { 2631 | if (!React.StrictMode) { 2632 | return 2633 | } 2634 | const spy = jest.spyOn(console, 'error').mockImplementation(() => {}) 2635 | const store = createStore(stringBuilder) 2636 | 2637 | @connect(state => ({ string: state })) 2638 | class Container extends Component { 2639 | render() { 2640 | return 2641 | } 2642 | } 2643 | 2644 | rtl.render( 2645 | 2646 | 2647 | 2648 | 2649 | 2650 | ) 2651 | 2652 | expect(spy).not.toHaveBeenCalled() 2653 | }) 2654 | 2655 | xit('should error on withRef=true', () => { 2656 | class Container extends Component { 2657 | render() { 2658 | return
hi
2659 | } 2660 | } 2661 | expect(() => 2662 | connect( 2663 | undefined, 2664 | undefined, 2665 | undefined, 2666 | { withRef: true } 2667 | )(Container) 2668 | ).toThrow(/withRef is removed/) 2669 | }) 2670 | 2671 | xit('should error on receiving a custom store key', () => { 2672 | const connectOptions = { storeKey: 'customStoreKey' } 2673 | 2674 | expect(() => { 2675 | @connect( 2676 | undefined, 2677 | undefined, 2678 | undefined, 2679 | connectOptions 2680 | ) 2681 | class Container extends Component { 2682 | render() { 2683 | return 2684 | } 2685 | } 2686 | new Container() 2687 | }).toThrow(/storeKey has been removed/) 2688 | }) 2689 | 2690 | xit('should error on custom store', () => { 2691 | function Comp() { 2692 | return
hi
2693 | } 2694 | const Container = connect()(Comp) 2695 | function Oops() { 2696 | return 2697 | } 2698 | expect(() => { 2699 | rtl.render() 2700 | }).toThrow(/Passing redux store/) 2701 | }) 2702 | 2703 | xit('should error on renderCount prop if specified in connect options', () => { 2704 | function Comp(props) { 2705 | return
{props.count}
2706 | } 2707 | expect(() => { 2708 | connect( 2709 | undefined, 2710 | undefined, 2711 | undefined, 2712 | { renderCountProp: 'count' } 2713 | )(Comp) 2714 | }).toThrow(/renderCountProp is removed/) 2715 | }) 2716 | 2717 | it('should not error on valid component with circular structure', () => { 2718 | const createComp = Tag => { 2719 | const Comp = React.forwardRef(function Comp(props) { 2720 | return {props.count} 2721 | }) 2722 | Comp.__real = Comp 2723 | return Comp 2724 | } 2725 | 2726 | expect(() => { 2727 | connect()(createComp('div')) 2728 | }).not.toThrow() 2729 | }) 2730 | }) 2731 | }) 2732 | -------------------------------------------------------------------------------- /test/install-test-deps.js: -------------------------------------------------------------------------------- 1 | /* eslint no-console: 0 */ 2 | 'use strict' 3 | 4 | const { readdirSync, existsSync, copyFile, mkdirSync } = require('fs') 5 | const rimraf = require('rimraf') 6 | const { join } = require('path') 7 | const spawn = require('cross-spawn') 8 | const reactVersion = process.env.REACT || '16.8' 9 | 10 | readdirSync(join(__dirname, 'react')).forEach(version => { 11 | if (reactVersion.toLowerCase() !== 'all' && version !== reactVersion) { 12 | console.log(`skipping ${version}, ${reactVersion} was specified`) 13 | return 14 | } 15 | const tests = [ 16 | join(__dirname, 'components'), 17 | join(__dirname, 'integration'), 18 | join(__dirname, 'utils') 19 | ] 20 | const srcs = [join(__dirname, '..', 'src')] 21 | const dest = [ 22 | join(__dirname, 'react', version, 'test', 'components'), 23 | join(__dirname, 'react', version, 'test', 'integration'), 24 | join(__dirname, 'react', version, 'test', 'utils') 25 | ] 26 | const srcDest = [join(__dirname, 'react', version, 'src')] 27 | 28 | if (!existsSync(join(__dirname, 'react', version, 'test'))) { 29 | mkdirSync(join(__dirname, 'react', version, 'test')) 30 | } 31 | 32 | if (!existsSync(join(__dirname, 'react', version, 'src'))) { 33 | mkdirSync(join(__dirname, 'react', version, 'src')) 34 | } 35 | 36 | console.log('Copying test files') 37 | tests.forEach((dir, i) => { 38 | if (existsSync(dest[i])) { 39 | console.log('clearing old tests in ' + dest[i]) 40 | rimraf.sync(join(dest[i], '*')) 41 | } else { 42 | mkdirSync(dest[i]) 43 | } 44 | readdirSync(dir).forEach(file => { 45 | copyFile(join(tests[i], file), join(dest[i], file), e => { 46 | if (e) console.log(e) 47 | }) 48 | }) 49 | }) 50 | console.log('Copying source files') 51 | srcs.forEach((dir, i) => { 52 | if (existsSync(srcDest[i])) { 53 | console.log('clearing old sources in ' + srcDest[i]) 54 | rimraf.sync(join(srcDest[i], '*')) 55 | } else { 56 | if (!existsSync(join(__dirname, 'react', version, 'src'))) { 57 | mkdirSync(join(__dirname, 'react', version, 'src')) 58 | } 59 | mkdirSync(srcDest[i]) 60 | } 61 | readdirSync(dir).forEach(file => { 62 | copyFile(join(srcs[i], file), join(srcDest[i], file), e => { 63 | if (e) console.log(e) 64 | }) 65 | }) 66 | copyFile( 67 | join(__dirname, '..', 'src', 'index.js'), 68 | join(__dirname, 'react', version, 'src', 'index.js'), 69 | e => { 70 | if (e) console.log(e) 71 | } 72 | ) 73 | }) 74 | const cwd = join(__dirname, 'react', version) 75 | if ( 76 | existsSync( 77 | join(__dirname, 'react', version, 'node_modules', 'react', 'package.json') 78 | ) 79 | ) { 80 | console.log(`Skipping React version ${version} ... (already installed)`) 81 | return 82 | } 83 | 84 | console.log(`Installing React version ${version}...`) 85 | 86 | const installTask = spawn.sync('npm', ['install'], { 87 | cwd, 88 | stdio: 'inherit' 89 | }) 90 | 91 | if (installTask.status > 0) { 92 | process.exit(installTask.status) 93 | } 94 | }) 95 | -------------------------------------------------------------------------------- /test/integration/server-rendering.spec.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable react/prop-types*/ 2 | 3 | import React from 'react' 4 | import { renderToString } from 'react-dom/server' 5 | import { createStore } from 'redux' 6 | import { ServerProvider as Provider, connect } from '../../src/index.js' 7 | 8 | describe('React', () => { 9 | describe('server rendering', () => { 10 | function greetingReducer(state = { greeting: 'Hello' }, action) { 11 | return action && action.payload ? action.payload : state 12 | } 13 | 14 | const Greeting = ({ greeting, greeted }) => greeting + ' ' + greeted 15 | const ConnectedGreeting = connect(state => state)(Greeting) 16 | 17 | const Greeter = props => ( 18 |
19 | 20 |
21 | ) 22 | 23 | it('should be able to render connected component with props and state from store', () => { 24 | const store = createStore(greetingReducer) 25 | 26 | const markup = renderToString( 27 | 28 | 29 | 30 | ) 31 | 32 | expect(markup).toContain('Hello world') 33 | }) 34 | 35 | it('should render with updated state if actions are dispatched before render', () => { 36 | const store = createStore(greetingReducer) 37 | 38 | store.dispatch({ type: 'Update', payload: { greeting: 'Hi' } }) 39 | 40 | const markup = renderToString( 41 | 42 | 43 | 44 | ) 45 | 46 | expect(markup).toContain('Hi world') 47 | expect(store.getState().greeting).toContain('Hi') 48 | }) 49 | 50 | it('should render children with original state even if actions are dispatched in ancestor', () => { 51 | /* 52 | Dispatching during construct, render or willMount is 53 | almost always a bug with SSR (or otherwise) 54 | 55 | This behaviour is undocumented and is likely to change between 56 | implementations, this test only verifies current behaviour 57 | */ 58 | const store = createStore(greetingReducer) 59 | 60 | class Dispatcher extends React.Component { 61 | constructor(props) { 62 | super(props) 63 | props.dispatch(props.action) 64 | } 65 | UNSAFE_componentWillMount() { 66 | this.props.dispatch(this.props.action) 67 | } 68 | render() { 69 | this.props.dispatch(this.props.action) 70 | 71 | return 72 | } 73 | } 74 | const ConnectedDispatcher = connect()(Dispatcher) 75 | 76 | const action = { type: 'Update', payload: { greeting: 'Hi' } } 77 | 78 | const markup = renderToString( 79 | 80 | 81 | 82 | ) 83 | 84 | expect(markup).toContain('Hello world') 85 | expect(store.getState().greeting).toContain('Hi') 86 | }) 87 | 88 | it('should render children with changed state if actions are dispatched in ancestor and new Provider wraps children', () => { 89 | /* 90 | Dispatching during construct, render or willMount is 91 | almost always a bug with SSR (or otherwise) 92 | 93 | This behaviour is undocumented and is likely to change between 94 | implementations, this test only verifies current behaviour 95 | */ 96 | const store = createStore(greetingReducer) 97 | 98 | class Dispatcher extends React.Component { 99 | constructor(props) { 100 | super(props) 101 | if (props.constructAction) { 102 | props.dispatch(props.constructAction) 103 | } 104 | } 105 | UNSAFE_componentWillMount() { 106 | if (this.props.willMountAction) { 107 | this.props.dispatch(this.props.willMountAction) 108 | } 109 | } 110 | render() { 111 | if (this.props.renderAction) { 112 | this.props.dispatch(this.props.renderAction) 113 | } 114 | 115 | return ( 116 | 117 | 118 | 119 | ) 120 | } 121 | } 122 | const ConnectedDispatcher = connect()(Dispatcher) 123 | 124 | const constructAction = { type: 'Update', payload: { greeting: 'Hi' } } 125 | const willMountAction = { type: 'Update', payload: { greeting: 'Hiya' } } 126 | const renderAction = { type: 'Update', payload: { greeting: 'Hey' } } 127 | 128 | const markup = renderToString( 129 | 130 | 134 | 138 | 139 | 140 | ) 141 | 142 | expect(markup).toContain('Hi world') 143 | expect(markup).toContain('Hiya world') 144 | expect(markup).toContain('Hey world') 145 | expect(store.getState().greeting).toContain('Hey') 146 | }) 147 | }) 148 | }) 149 | -------------------------------------------------------------------------------- /test/react/16.8/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "requires": true, 3 | "lockfileVersion": 1, 4 | "dependencies": { 5 | "@babel/runtime": { 6 | "version": "7.3.1", 7 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", 8 | "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", 9 | "requires": { 10 | "regenerator-runtime": "^0.12.0" 11 | } 12 | }, 13 | "@sheerun/mutationobserver-shim": { 14 | "version": "0.3.2", 15 | "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.2.tgz", 16 | "integrity": "sha512-vTCdPp/T/Q3oSqwHmZ5Kpa9oI7iLtGl3RQaA/NyLHikvcrPxACkkKVr/XzkSPJWXHRhKGzVvb0urJsbMlRxi1Q==" 17 | }, 18 | "ansi-regex": { 19 | "version": "4.0.0", 20 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz", 21 | "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w==" 22 | }, 23 | "ansi-styles": { 24 | "version": "3.2.1", 25 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 26 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 27 | "requires": { 28 | "color-convert": "^1.9.0" 29 | } 30 | }, 31 | "asap": { 32 | "version": "2.0.6", 33 | "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", 34 | "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", 35 | "dev": true 36 | }, 37 | "atob": { 38 | "version": "2.1.2", 39 | "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", 40 | "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" 41 | }, 42 | "chalk": { 43 | "version": "2.4.2", 44 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 45 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 46 | "requires": { 47 | "ansi-styles": "^3.2.1", 48 | "escape-string-regexp": "^1.0.5", 49 | "supports-color": "^5.3.0" 50 | } 51 | }, 52 | "color-convert": { 53 | "version": "1.9.3", 54 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 55 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 56 | "requires": { 57 | "color-name": "1.1.3" 58 | } 59 | }, 60 | "color-name": { 61 | "version": "1.1.3", 62 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 63 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" 64 | }, 65 | "core-js": { 66 | "version": "1.2.7", 67 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", 68 | "integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=", 69 | "dev": true 70 | }, 71 | "create-react-class": { 72 | "version": "15.6.3", 73 | "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", 74 | "integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", 75 | "dev": true, 76 | "requires": { 77 | "fbjs": "^0.8.9", 78 | "loose-envify": "^1.3.1", 79 | "object-assign": "^4.1.1" 80 | } 81 | }, 82 | "css": { 83 | "version": "2.2.4", 84 | "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", 85 | "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", 86 | "requires": { 87 | "inherits": "^2.0.3", 88 | "source-map": "^0.6.1", 89 | "source-map-resolve": "^0.5.2", 90 | "urix": "^0.1.0" 91 | } 92 | }, 93 | "css.escape": { 94 | "version": "1.5.1", 95 | "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", 96 | "integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=" 97 | }, 98 | "decode-uri-component": { 99 | "version": "0.2.0", 100 | "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", 101 | "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" 102 | }, 103 | "diff-sequences": { 104 | "version": "24.0.0", 105 | "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.0.0.tgz", 106 | "integrity": "sha512-46OkIuVGBBnrC0soO/4LHu5LHGHx0uhP65OVz8XOrAJpqiCB2aVIuESvjI1F9oqebuvY8lekS1pt6TN7vt7qsw==" 107 | }, 108 | "dom-testing-library": { 109 | "version": "3.16.8", 110 | "resolved": "https://registry.npmjs.org/dom-testing-library/-/dom-testing-library-3.16.8.tgz", 111 | "integrity": "sha512-VGn2piehGoN9lmZDYd+xoTZwwcS+FoXebvZMw631UhS5LshiLTFNJs9bxRa9W7fVb1cAn9AYKAKZXh67rCDaqw==", 112 | "requires": { 113 | "@babel/runtime": "^7.1.5", 114 | "@sheerun/mutationobserver-shim": "^0.3.2", 115 | "pretty-format": "^24.0.0", 116 | "wait-for-expect": "^1.1.0" 117 | } 118 | }, 119 | "encoding": { 120 | "version": "0.1.12", 121 | "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", 122 | "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", 123 | "dev": true, 124 | "requires": { 125 | "iconv-lite": "~0.4.13" 126 | } 127 | }, 128 | "escape-string-regexp": { 129 | "version": "1.0.5", 130 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 131 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" 132 | }, 133 | "fbjs": { 134 | "version": "0.8.17", 135 | "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", 136 | "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", 137 | "dev": true, 138 | "requires": { 139 | "core-js": "^1.0.0", 140 | "isomorphic-fetch": "^2.1.1", 141 | "loose-envify": "^1.0.0", 142 | "object-assign": "^4.1.0", 143 | "promise": "^7.1.1", 144 | "setimmediate": "^1.0.5", 145 | "ua-parser-js": "^0.7.18" 146 | } 147 | }, 148 | "has-flag": { 149 | "version": "3.0.0", 150 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 151 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" 152 | }, 153 | "iconv-lite": { 154 | "version": "0.4.24", 155 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 156 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 157 | "dev": true, 158 | "requires": { 159 | "safer-buffer": ">= 2.1.2 < 3" 160 | } 161 | }, 162 | "indent-string": { 163 | "version": "3.2.0", 164 | "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", 165 | "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" 166 | }, 167 | "inherits": { 168 | "version": "2.0.3", 169 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 170 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 171 | }, 172 | "is-stream": { 173 | "version": "1.1.0", 174 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 175 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", 176 | "dev": true 177 | }, 178 | "isomorphic-fetch": { 179 | "version": "2.2.1", 180 | "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", 181 | "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", 182 | "dev": true, 183 | "requires": { 184 | "node-fetch": "^1.0.1", 185 | "whatwg-fetch": ">=0.10.0" 186 | } 187 | }, 188 | "jest-diff": { 189 | "version": "24.0.0", 190 | "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.0.0.tgz", 191 | "integrity": "sha512-XY5wMpRaTsuMoU+1/B2zQSKQ9RdE9gsLkGydx3nvApeyPijLA8GtEvIcPwISRCer+VDf9W1mStTYYq6fPt8ryA==", 192 | "requires": { 193 | "chalk": "^2.0.1", 194 | "diff-sequences": "^24.0.0", 195 | "jest-get-type": "^24.0.0", 196 | "pretty-format": "^24.0.0" 197 | } 198 | }, 199 | "jest-dom": { 200 | "version": "3.1.2", 201 | "resolved": "https://registry.npmjs.org/jest-dom/-/jest-dom-3.1.2.tgz", 202 | "integrity": "sha512-QpyhZxgx8SkFefBaTD426RDT90dSmoB4nBXIHbQQ/MdrpFl9V2HRmhBYe7p82T22TkHQHbSAmis+il4c1R4cBg==", 203 | "requires": { 204 | "chalk": "^2.4.1", 205 | "css": "^2.2.3", 206 | "css.escape": "^1.5.1", 207 | "jest-diff": "^24.0.0", 208 | "jest-matcher-utils": "^24.0.0", 209 | "lodash": "^4.17.11", 210 | "pretty-format": "^24.0.0", 211 | "redent": "^2.0.0" 212 | } 213 | }, 214 | "jest-get-type": { 215 | "version": "24.0.0", 216 | "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.0.0.tgz", 217 | "integrity": "sha512-z6/Eyf6s9ZDGz7eOvl+fzpuJmN9i0KyTt1no37/dHu8galssxz5ZEgnc1KaV8R31q1khxyhB4ui/X5ZjjPk77w==" 218 | }, 219 | "jest-matcher-utils": { 220 | "version": "24.0.0", 221 | "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.0.0.tgz", 222 | "integrity": "sha512-LQTDmO+aWRz1Tf9HJg+HlPHhDh1E1c65kVwRFo5mwCVp5aQDzlkz4+vCvXhOKFjitV2f0kMdHxnODrXVoi+rlA==", 223 | "requires": { 224 | "chalk": "^2.0.1", 225 | "jest-diff": "^24.0.0", 226 | "jest-get-type": "^24.0.0", 227 | "pretty-format": "^24.0.0" 228 | } 229 | }, 230 | "js-tokens": { 231 | "version": "4.0.0", 232 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 233 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 234 | "dev": true 235 | }, 236 | "lodash": { 237 | "version": "4.17.11", 238 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 239 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 240 | }, 241 | "loose-envify": { 242 | "version": "1.4.0", 243 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 244 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 245 | "dev": true, 246 | "requires": { 247 | "js-tokens": "^3.0.0 || ^4.0.0" 248 | } 249 | }, 250 | "node-fetch": { 251 | "version": "1.7.3", 252 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", 253 | "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", 254 | "dev": true, 255 | "requires": { 256 | "encoding": "^0.1.11", 257 | "is-stream": "^1.0.1" 258 | } 259 | }, 260 | "object-assign": { 261 | "version": "4.1.1", 262 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 263 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 264 | "dev": true 265 | }, 266 | "pretty-format": { 267 | "version": "24.0.0", 268 | "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.0.0.tgz", 269 | "integrity": "sha512-LszZaKG665djUcqg5ZQq+XzezHLKrxsA86ZABTozp+oNhkdqa+tG2dX4qa6ERl5c/sRDrAa3lHmwnvKoP+OG/g==", 270 | "requires": { 271 | "ansi-regex": "^4.0.0", 272 | "ansi-styles": "^3.2.0" 273 | } 274 | }, 275 | "promise": { 276 | "version": "7.3.1", 277 | "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", 278 | "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", 279 | "dev": true, 280 | "requires": { 281 | "asap": "~2.0.3" 282 | } 283 | }, 284 | "prop-types": { 285 | "version": "15.7.2", 286 | "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", 287 | "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", 288 | "dev": true, 289 | "requires": { 290 | "loose-envify": "^1.4.0", 291 | "object-assign": "^4.1.1", 292 | "react-is": "^16.8.1" 293 | } 294 | }, 295 | "react": { 296 | "version": "16.8.2", 297 | "resolved": "https://registry.npmjs.org/react/-/react-16.8.2.tgz", 298 | "integrity": "sha512-aB2ctx9uQ9vo09HVknqv3DGRpI7OIGJhCx3Bt0QqoRluEjHSaObJl+nG12GDdYH6sTgE7YiPJ6ZUyMx9kICdXw==", 299 | "dev": true, 300 | "requires": { 301 | "loose-envify": "^1.1.0", 302 | "object-assign": "^4.1.1", 303 | "prop-types": "^15.6.2", 304 | "scheduler": "^0.13.2" 305 | } 306 | }, 307 | "react-dom": { 308 | "version": "16.8.2", 309 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.2.tgz", 310 | "integrity": "sha512-cPGfgFfwi+VCZjk73buu14pYkYBR1b/SRMSYqkLDdhSEHnSwcuYTPu6/Bh6ZphJFIk80XLvbSe2azfcRzNF+Xg==", 311 | "dev": true, 312 | "requires": { 313 | "loose-envify": "^1.1.0", 314 | "object-assign": "^4.1.1", 315 | "prop-types": "^15.6.2", 316 | "scheduler": "^0.13.2" 317 | } 318 | }, 319 | "react-is": { 320 | "version": "16.8.2", 321 | "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.2.tgz", 322 | "integrity": "sha512-D+NxhSR2HUCjYky1q1DwpNUD44cDpUXzSmmFyC3ug1bClcU/iDNy0YNn1iwme28fn+NFhpA13IndOd42CrFb+Q==", 323 | "dev": true 324 | }, 325 | "react-testing-library": { 326 | "version": "5.9.0", 327 | "resolved": "https://registry.npmjs.org/react-testing-library/-/react-testing-library-5.9.0.tgz", 328 | "integrity": "sha512-T303PJZvrLKeeiPpjmMD1wxVpzEg9yI0qteH/cUvpFqNHOzPe3yN+Pu+jo9JlxuTMvVGPAmCAcgZ3sEtEDpJUQ==", 329 | "requires": { 330 | "@babel/runtime": "^7.3.1", 331 | "dom-testing-library": "^3.13.1" 332 | } 333 | }, 334 | "redent": { 335 | "version": "2.0.0", 336 | "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", 337 | "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", 338 | "requires": { 339 | "indent-string": "^3.0.0", 340 | "strip-indent": "^2.0.0" 341 | } 342 | }, 343 | "regenerator-runtime": { 344 | "version": "0.12.1", 345 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz", 346 | "integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==" 347 | }, 348 | "resolve-url": { 349 | "version": "0.2.1", 350 | "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", 351 | "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" 352 | }, 353 | "safer-buffer": { 354 | "version": "2.1.2", 355 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 356 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 357 | "dev": true 358 | }, 359 | "scheduler": { 360 | "version": "0.13.2", 361 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.2.tgz", 362 | "integrity": "sha512-qK5P8tHS7vdEMCW5IPyt8v9MJOHqTrOUgPXib7tqm9vh834ibBX5BNhwkplX/0iOzHW5sXyluehYfS9yrkz9+w==", 363 | "dev": true, 364 | "requires": { 365 | "loose-envify": "^1.1.0", 366 | "object-assign": "^4.1.1" 367 | } 368 | }, 369 | "setimmediate": { 370 | "version": "1.0.5", 371 | "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", 372 | "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", 373 | "dev": true 374 | }, 375 | "source-map": { 376 | "version": "0.6.1", 377 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 378 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 379 | }, 380 | "source-map-resolve": { 381 | "version": "0.5.2", 382 | "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", 383 | "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", 384 | "requires": { 385 | "atob": "^2.1.1", 386 | "decode-uri-component": "^0.2.0", 387 | "resolve-url": "^0.2.1", 388 | "source-map-url": "^0.4.0", 389 | "urix": "^0.1.0" 390 | } 391 | }, 392 | "source-map-url": { 393 | "version": "0.4.0", 394 | "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", 395 | "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" 396 | }, 397 | "strip-indent": { 398 | "version": "2.0.0", 399 | "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", 400 | "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" 401 | }, 402 | "supports-color": { 403 | "version": "5.5.0", 404 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 405 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 406 | "requires": { 407 | "has-flag": "^3.0.0" 408 | } 409 | }, 410 | "ua-parser-js": { 411 | "version": "0.7.18", 412 | "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.18.tgz", 413 | "integrity": "sha512-LtzwHlVHwFGTptfNSgezHp7WUlwiqb0gA9AALRbKaERfxwJoiX0A73QbTToxteIAuIaFshhgIZfqK8s7clqgnA==", 414 | "dev": true 415 | }, 416 | "urix": { 417 | "version": "0.1.0", 418 | "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", 419 | "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" 420 | }, 421 | "wait-for-expect": { 422 | "version": "1.1.0", 423 | "resolved": "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-1.1.0.tgz", 424 | "integrity": "sha512-vQDokqxyMyknfX3luCDn16bSaRcOyH6gGuUXMIbxBLeTo6nWuEWYqMTT9a+44FmW8c2m6TRWBdNvBBjA1hwEKg==" 425 | }, 426 | "whatwg-fetch": { 427 | "version": "3.0.0", 428 | "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", 429 | "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", 430 | "dev": true 431 | } 432 | } 433 | } 434 | -------------------------------------------------------------------------------- /test/react/16.8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "create-react-class": "^15.6.3", 5 | "react": "16.8", 6 | "react-dom": "16.8" 7 | }, 8 | "dependencies": { 9 | "jest-dom": "^3.1.2", 10 | "react-testing-library": "^5.9.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/run-tests.js: -------------------------------------------------------------------------------- 1 | const npmRun = require('npm-run') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const LATEST_VERSION = '16.8' 5 | const version = process.env.REACT || LATEST_VERSION 6 | 7 | let jestConfig = { 8 | testURL: 'http://localhost', 9 | collectCoverage: true, 10 | coverageDirectory: `${__dirname}/coverage`, 11 | transform: { 12 | '.js$': `${__dirname}/babel-transformer.jest.js` 13 | } 14 | } 15 | 16 | require('./install-test-deps.js') 17 | 18 | if (version.toLowerCase() === 'all') { 19 | jestConfig = { 20 | ...jestConfig, 21 | rootDir: __dirname, 22 | // every directory has the same coverage, so we collect it only from one 23 | collectCoverageFrom: [`react/${LATEST_VERSION}/src/**.js`] 24 | } 25 | } else { 26 | jestConfig = { 27 | ...jestConfig, 28 | rootDir: `${__dirname}/react/${version}` 29 | } 30 | } 31 | 32 | const configFilePath = path.join(__dirname, 'jest-config.json') 33 | 34 | fs.writeFileSync(configFilePath, JSON.stringify(jestConfig)) 35 | 36 | const commandLine = `jest -c "${configFilePath}" ${process.argv 37 | .slice(2) 38 | .join(' ')}` 39 | 40 | npmRun.execSync(commandLine, { stdio: 'inherit' }) 41 | -------------------------------------------------------------------------------- /test/utils/shallowEqual.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowCompare as shallowEqual } from '../../src/utils' 2 | 3 | describe('Utils', () => { 4 | describe('shallowEqual', () => { 5 | it('should return true if arguments fields are equal', () => { 6 | expect( 7 | shallowEqual({ a: 1, b: 2, c: undefined }, { a: 1, b: 2, c: undefined }) 8 | ).toBe(true) 9 | 10 | expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toBe( 11 | true 12 | ) 13 | 14 | const o = {} 15 | expect(shallowEqual({ a: 1, b: 2, c: o }, { a: 1, b: 2, c: o })).toBe( 16 | true 17 | ) 18 | 19 | const d = function() { 20 | return 1 21 | } 22 | expect( 23 | shallowEqual({ a: 1, b: 2, c: o, d }, { a: 1, b: 2, c: o, d }) 24 | ).toBe(true) 25 | }) 26 | 27 | it('should return false if arguments fields are different function identities', () => { 28 | expect( 29 | shallowEqual( 30 | { 31 | a: 1, 32 | b: 2, 33 | d: function() { 34 | return 1 35 | } 36 | }, 37 | { 38 | a: 1, 39 | b: 2, 40 | d: function() { 41 | return 1 42 | } 43 | } 44 | ) 45 | ).toBe(false) 46 | }) 47 | 48 | it('should return false if first argument has too many keys', () => { 49 | expect(shallowEqual({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toBe(false) 50 | }) 51 | 52 | it('should return false if second argument has too many keys', () => { 53 | expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toBe(false) 54 | }) 55 | 56 | it('should return false if arguments have different keys', () => { 57 | expect( 58 | shallowEqual( 59 | { a: 1, b: 2, c: undefined }, 60 | { a: 1, bb: 2, c: undefined } 61 | ) 62 | ).toBe(false) 63 | }) 64 | 65 | it('should compare two NaN values', () => { 66 | expect(shallowEqual(NaN, NaN)).toBe(true) 67 | }) 68 | 69 | it('should compare empty objects, with false', () => { 70 | expect(shallowEqual({}, false)).toBe(false) 71 | expect(shallowEqual(false, {})).toBe(false) 72 | expect(shallowEqual([], false)).toBe(false) 73 | expect(shallowEqual(false, [])).toBe(false) 74 | }) 75 | 76 | it('should compare two zero values', () => { 77 | expect(shallowEqual(0, 0)).toBe(true) 78 | }) 79 | }) 80 | }) 81 | --------------------------------------------------------------------------------