From 7ddbf179f4c3ab17ea2954816066c3c0b9fe67ea Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 19 Aug 2017 01:03:01 -0500 Subject: [PATCH 01/21] Add project license Generate MIT license via GitHub --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f4410f0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Drew Keller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From e4353aee1974248980249f74c4d191908d62e20f Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 19 Aug 2017 20:15:30 -0500 Subject: [PATCH 02/21] Add README file Document purpose, installation, and usage of package --- README.md | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0085918 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# react-component-update + +[![Build Status](https://travis-ci.org/wimpyprogrammer/react-component-update.svg?branch=master)](https://travis-ci.org/wimpyprogrammer/react-component-update) +[![codecov](https://codecov.io/gh/wimpyprogrammer/react-component-update/branch/master/graph/badge.svg)](https://codecov.io/gh/wimpyprogrammer/react-component-update) + +Extends the React `Component` and `PureComponent` classes with convenience lifecycle events. + + - `componentWillMountOrReceiveProps(nextProps)` - Combines the [`componentWillMount()`](https://facebook.github.io/react/docs/react-component.html#componentwillmount) and [`componentWillReceiveProps(nextProps)`](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) events. This allows you to consolidate all pre-`render()` logic. + + - `componentDidMountOrUpdate(prevProps, prevState)` - Combines the [`componentDidMount()`](https://facebook.github.io/react/docs/react-component.html#componentdidmount) and [`componentDidUpdate(prevProps, prevState)`](https://facebook.github.io/react/docs/react-component.html#componentdidupdate) events. This allows you to consolidate all post-`render()` logic. + +## Installation + +Published on `npm` as [`react-component-update`](https://www.npmjs.com/package/react-component-update). + +npm users: +``` +npm install --save react-component-update +``` + +yarn users: +``` +yarn add react-component-update +``` + +## Usage + +To extend React's `Component` class: + +```js +import React from 'react'; +import { Component } from 'react-component-update'; + +class MyReactComponent extends Component { + componentWillMountOrReceiveProps(nextProps) { + // Code that runs before every render(). For example, check that the data + // used by this component has already loaded, otherwise trigger an AJAX + // request for it. nextProps contains the props that render() will receive. + } + + componentDidMountOrUpdate(prevProps, prevState) { + // Code that runs after every render(). For example, inspect the latest DOM + // to get the height of the rendered elements. prevProps and prevState + // contain the props and state that render() will receive. + } + + render() { + return
; + } +} +``` + +Or to extend React's `PureComponent` class (available in React v15.3.0+): +```js +import { PureComponent } from 'react-component-update'; +``` + +## Mixing with your own lifecycle events + +`react-component-update` implements four lifecycle events of the React base classes: + - `componentWillMount()` + - `componentDidMount()` + - `componentWillReceiveProps()` + - `componentDidUpdate()` + +If you also implement these events in your component, you will need to call the corresponding `super()` method like so: + +```js +componentWillMount() { + super.componentWillMount(); +} + +componentDidMount() { + super.componentDidMount(); +} + +componentWillReceiveProps(nextProps) { + super.componentWillReceiveProps(nextProps); +} + +componentDidUpdate(prevProps, prevState) { + super.componentDidUpdate(prevProps, prevState); +} +``` + +The `super()` method can be called anywhere in your function to suit your needs. + +## License + +[MIT](/LICENSE.md) From f4998abd34aa6e4729b3e79b798c42e0a3c414bc Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 19 Aug 2017 08:31:20 -0500 Subject: [PATCH 03/21] Specify MIT license in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ef42199..ffb4837 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "url": "git+https://github.com/wimpyprogrammer/react-component-update.git" }, "author": "Drew Keller ", - "license": "ISC", + "license": "MIT", "bugs": { "url": "https://github.com/wimpyprogrammer/react-component-update/issues" }, From 21886414e9171fff2a42880e4c2ab0af6683495f Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 19 Aug 2017 20:46:18 -0500 Subject: [PATCH 04/21] Fix export method of package entry point The ES2015 "export default" syntax returns the exported properties in a "default" property when imported with require(). The module.exports method will behave identically for import and require(). --- src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index 27ded4d..9936847 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,7 @@ import Component from './component'; import PureComponent from './pureComponent'; -export default { +module.exports = { Component, PureComponent, }; From 2c79abc0784e4e215fbda6c6bf87c6aedc0153de Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sun, 20 Aug 2017 10:02:53 -0500 Subject: [PATCH 05/21] Test "this" value of lifecycle events --- src/component.spec.js | 28 ++++++++++++++++++++++++++++ src/pureComponent.spec.js | 28 ++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/component.spec.js b/src/component.spec.js index 67d7865..875c010 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -51,6 +51,11 @@ describe('Component extension', () => { expect(callbackWill.firstCall.args[0]).to.equal(component.props()); }); + it('runs on mount with "this" context of component', () => { + const component = mount(); + expect(callbackWill.firstCall.thisValue).to.equal(component.getNode()); + }); + it('runs on mount before render()', () => { mount(); expect(callbackWill.calledBefore(callbackRender)).to.be.true(); @@ -74,6 +79,12 @@ describe('Component extension', () => { expect(callbackWill.secondCall.args[0]).to.equal(component.props()); }); + it('runs on props update with "this" context of component', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackWill.secondCall.thisValue).to.equal(component.getNode()); + }); + it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); @@ -103,6 +114,11 @@ describe('Component extension', () => { expect(callbackDid.firstCall.args[1]).to.equal(component.state()); }); + it('runs on mount with "this" context of component', () => { + const component = mount(); + expect(callbackDid.firstCall.thisValue).to.equal(component.getNode()); + }); + it('runs after render()', () => { mount(); expect(callbackDid.calledAfter(callbackRender)).to.be.true(); @@ -134,6 +150,12 @@ describe('Component extension', () => { expect(callbackDid.secondCall.args[1]).to.equal(initialState); }); + it('runs on props update with "this" context of component', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + }); + it('runs on state update', () => { const component = mount(); component.setState(getUniqueState()); @@ -160,6 +182,12 @@ describe('Component extension', () => { expect(callbackDid.secondCall.args[1]).to.equal(initialState); }); + it('runs on state update with "this" context of component', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + }); + it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index a28e41b..b75adfd 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -54,6 +54,11 @@ descriptor('PureComponent extension', () => { expect(callbackWill.firstCall.args[0]).to.equal(component.props()); }); + it('runs on mount with "this" context of component', () => { + const component = mount(); + expect(callbackWill.firstCall.thisValue).to.equal(component.getNode()); + }); + it('runs on mount before render()', () => { mount(); expect(callbackWill.calledBefore(callbackRender)).to.be.true(); @@ -77,6 +82,12 @@ descriptor('PureComponent extension', () => { expect(callbackWill.secondCall.args[0]).to.equal(component.props()); }); + it('runs on props update with "this" context of component', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackWill.secondCall.thisValue).to.equal(component.getNode()); + }); + it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); @@ -106,6 +117,11 @@ descriptor('PureComponent extension', () => { expect(callbackDid.firstCall.args[1]).to.equal(component.state()); }); + it('runs on mount with "this" context of component', () => { + const component = mount(); + expect(callbackDid.firstCall.thisValue).to.equal(component.getNode()); + }); + it('runs after render()', () => { mount(); expect(callbackDid.calledAfter(callbackRender)).to.be.true(); @@ -131,6 +147,12 @@ descriptor('PureComponent extension', () => { expect(callbackDid.secondCall.args[1]).to.equal(initialState); }); + it('runs on props update with "this" context of component', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + }); + it('runs on state update', () => { const component = mount(); component.setState(getUniqueState()); @@ -151,6 +173,12 @@ descriptor('PureComponent extension', () => { expect(callbackDid.secondCall.args[1]).to.equal(initialState); }); + it('runs on state update with "this" context of component', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + }); + it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); From 0d23e5b1db8f5f41125d01b8bd84392f6bcf6e8b Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sun, 20 Aug 2017 10:03:29 -0500 Subject: [PATCH 06/21] Ignore test files outside of src/ directory This excludes the transpiled tests in the lib/ directory. --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ffb4837..84ace9c 100644 --- a/package.json +++ b/package.json @@ -51,6 +51,7 @@ }, "jest": { "coverageDirectory": "./coverage/", - "collectCoverage": true + "collectCoverage": true, + "testMatch": ["**/src/*.spec.js?(x)"] } } From a2c44ae5242e6fb4265831f3f8441855b9b34e18 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sun, 20 Aug 2017 23:49:19 -0500 Subject: [PATCH 07/21] Convert tests to sinon-chai format The new format improves assertion readability. --- package.json | 3 ++- src/component.spec.js | 54 ++++++++++++++++++++------------------- src/pureComponent.spec.js | 50 +++++++++++++++++++----------------- 3 files changed, 56 insertions(+), 51 deletions(-) diff --git a/package.json b/package.json index 84ace9c..73c3253 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ "react-dom": "*", "react-test-renderer": "*", "rimraf": "^2.6.1", - "sinon": "^2.3.4" + "sinon": "^2.3.4", + "sinon-chai": "^2.13.0" }, "peerDependencies": { "react": "*" diff --git a/src/component.spec.js b/src/component.spec.js index 875c010..3271e71 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -5,10 +5,12 @@ import { mount } from 'enzyme'; import uniqueId from 'lodash.uniqueid'; import React from 'react'; import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; import Component from './component'; chai.use(dirtyChai); +chai.use(sinonChai); function getUniqueState() { return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; @@ -43,155 +45,155 @@ describe('Component extension', () => { describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { mount(); - expect(callbackWill.calledOnce).to.be.true(); + expect(callbackWill).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { const component = mount(); - expect(callbackWill.firstCall.args[0]).to.equal(component.props()); + expect(callbackWill.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with "this" context of component', () => { const component = mount(); - expect(callbackWill.firstCall.thisValue).to.equal(component.getNode()); + expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs on mount before render()', () => { mount(); - expect(callbackWill.calledBefore(callbackRender)).to.be.true(); + expect(callbackWill).to.have.been.calledBefore(callbackRender); }); it('runs on props update', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.calledTwice).to.be.true(); + expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update when no props change', () => { const component = mount(); component.setProps(component.props()); - expect(callbackWill.calledTwice).to.be.true(); + expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update with first parameter of component props', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.secondCall.args[0]).to.equal(component.props()); + expect(callbackWill.secondCall).to.have.been.calledWith(component.props()); }); it('runs on props update with "this" context of component', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.secondCall.thisValue).to.equal(component.getNode()); + expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.secondCall.calledBefore(callbackRender.secondCall)).to.be.true(); + expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); }); it('does not run on state update', () => { const component = mount(); component.setState(getUniqueState()); - expect(callbackWill.calledOnce).to.be.true(); + expect(callbackWill).to.have.been.calledOnce(); }); }); describe('componentDidMountOrUpdate()', () => { it('runs once when mounted', () => { mount(); - expect(callbackDid.calledOnce).to.be.true(); + expect(callbackDid).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { const component = mount(); - expect(callbackDid.firstCall.args[0]).to.equal(component.props()); + expect(callbackDid.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with second parameter of component state', () => { const component = mount(); - expect(callbackDid.firstCall.args[1]).to.equal(component.state()); + expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state()); }); it('runs on mount with "this" context of component', () => { const component = mount(); - expect(callbackDid.firstCall.thisValue).to.equal(component.getNode()); + expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs after render()', () => { mount(); - expect(callbackDid.calledAfter(callbackRender)).to.be.true(); + expect(callbackDid).to.have.been.calledAfter(callbackRender); }); it('runs on props update', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackDid.calledTwice).to.be.true(); + expect(callbackDid).to.have.been.calledTwice(); }); it('runs on props update when no props change', () => { const component = mount(); component.setProps(component.props()); - expect(callbackDid.calledTwice).to.be.true(); + expect(callbackDid).to.have.been.calledTwice(); }); it('runs on props update with first parameter of previous component props', () => { const component = mount(); const initialProps = component.props(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.args[0]).to.equal(initialProps); + expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on props update with second parameter of previous component state', () => { const component = mount(); const initialState = component.state(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.args[1]).to.equal(initialState); + expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on props update with "this" context of component', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on state update', () => { const component = mount(); component.setState(getUniqueState()); - expect(callbackDid.calledTwice).to.be.true(); + expect(callbackDid).to.have.been.calledTwice(); }); it('runs on state update when no state changes', () => { const component = mount(); component.setState(component.state()); - expect(callbackDid.calledTwice).to.be.true(); + expect(callbackDid).to.have.been.calledTwice(); }); it('runs on state update with first parameter of previous component props', () => { const component = mount(); const initialProps = component.props(); component.setState(getUniqueState()); - expect(callbackDid.secondCall.args[0]).to.equal(initialProps); + expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on state update with second parameter of previous component state', () => { const component = mount(); const initialState = component.state(); component.setState(getUniqueState()); - expect(callbackDid.secondCall.args[1]).to.equal(initialState); + expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on state update with "this" context of component', () => { const component = mount(); component.setState(getUniqueState()); - expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.calledAfter(callbackRender.secondCall)).to.be.true(); + expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); }); }); }); diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index b75adfd..3734481 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -5,8 +5,10 @@ import { mount } from 'enzyme'; import uniqueId from 'lodash.uniqueid'; import React from 'react'; import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; chai.use(dirtyChai); +chai.use(sinonChai); function getUniqueState() { return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; @@ -46,143 +48,143 @@ descriptor('PureComponent extension', () => { describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { mount(); - expect(callbackWill.calledOnce).to.be.true(); + expect(callbackWill).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { const component = mount(); - expect(callbackWill.firstCall.args[0]).to.equal(component.props()); + expect(callbackWill.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with "this" context of component', () => { const component = mount(); - expect(callbackWill.firstCall.thisValue).to.equal(component.getNode()); + expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs on mount before render()', () => { mount(); - expect(callbackWill.calledBefore(callbackRender)).to.be.true(); + expect(callbackWill).to.have.been.calledBefore(callbackRender); }); it('runs on props update', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.calledTwice).to.be.true(); + expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update when no props change', () => { const component = mount(); component.setProps(component.props()); - expect(callbackWill.calledTwice).to.be.true(); + expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update with first parameter of component props', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.secondCall.args[0]).to.equal(component.props()); + expect(callbackWill.secondCall).to.have.been.calledWith(component.props()); }); it('runs on props update with "this" context of component', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.secondCall.thisValue).to.equal(component.getNode()); + expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackWill.secondCall.calledBefore(callbackRender.secondCall)).to.be.true(); + expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); }); it('does not run on state update', () => { const component = mount(); component.setState(getUniqueState()); - expect(callbackWill.calledOnce).to.be.true(); + expect(callbackWill).to.have.been.calledOnce(); }); }); describe('componentDidMountOrUpdate()', () => { it('runs once when mounted', () => { mount(); - expect(callbackDid.calledOnce).to.be.true(); + expect(callbackDid).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { const component = mount(); - expect(callbackDid.firstCall.args[0]).to.equal(component.props()); + expect(callbackDid.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with second parameter of component state', () => { const component = mount(); - expect(callbackDid.firstCall.args[1]).to.equal(component.state()); + expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state()); }); it('runs on mount with "this" context of component', () => { const component = mount(); - expect(callbackDid.firstCall.thisValue).to.equal(component.getNode()); + expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs after render()', () => { mount(); - expect(callbackDid.calledAfter(callbackRender)).to.be.true(); + expect(callbackDid).to.have.been.calledAfter(callbackRender); }); it('runs on props update', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackDid.calledTwice).to.be.true(); + expect(callbackDid).to.have.been.calledTwice(); }); it('runs on props update with first parameter of previous component props', () => { const component = mount(); const initialProps = component.props(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.args[0]).to.equal(initialProps); + expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on props update with second parameter of previous component state', () => { const component = mount(); const initialState = component.state(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.args[1]).to.equal(initialState); + expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on props update with "this" context of component', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on state update', () => { const component = mount(); component.setState(getUniqueState()); - expect(callbackDid.calledTwice).to.be.true(); + expect(callbackDid).to.have.been.calledTwice(); }); it('runs on state update with first parameter of previous component props', () => { const component = mount(); const initialProps = component.props(); component.setState(getUniqueState()); - expect(callbackDid.secondCall.args[0]).to.equal(initialProps); + expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on state update with second parameter of previous component state', () => { const component = mount(); const initialState = component.state(); component.setState(getUniqueState()); - expect(callbackDid.secondCall.args[1]).to.equal(initialState); + expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on state update with "this" context of component', () => { const component = mount(); component.setState(getUniqueState()); - expect(callbackDid.secondCall.thisValue).to.equal(component.getNode()); + expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { const component = mount(); component.setProps(getUniqueProps()); - expect(callbackDid.secondCall.calledAfter(callbackRender.secondCall)).to.be.true(); + expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); }); }); }); From d5444ed32e85f6711509f36e69fab49551ca1434 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sun, 20 Aug 2017 23:50:29 -0500 Subject: [PATCH 08/21] Fix package.json formatting The new formatting is automatically applied by npm whenever it modifies the package.json. --- package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 73c3253..539b248 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "jest": { "coverageDirectory": "./coverage/", "collectCoverage": true, - "testMatch": ["**/src/*.spec.js?(x)"] + "testMatch": [ + "**/src/*.spec.js?(x)" + ] } } From 4e5f37d285c6c75ea50644e459047c50840b2439 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 26 Aug 2017 09:05:21 -0500 Subject: [PATCH 09/21] Upgrade Sinon to latest. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 539b248..ce8c57a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "react-dom": "*", "react-test-renderer": "*", "rimraf": "^2.6.1", - "sinon": "^2.3.4", + "sinon": "^3.2.1", "sinon-chai": "^2.13.0" }, "peerDependencies": { From 778a877c04597ae576c1ebe01fa097d619a67f09 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 26 Aug 2017 09:08:22 -0500 Subject: [PATCH 10/21] Test component with lifecycle overrides Assert that calls to super() will trigger the new lifecycle hooks, and that all logic runs in the expected order. --- src/component.spec.js | 132 +++++++++++++++++++++++++++++++++++++ src/pureComponent.spec.js | 134 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 266 insertions(+) diff --git a/src/component.spec.js b/src/component.spec.js index 3271e71..229c35a 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -1,4 +1,5 @@ /* eslint-env jest */ +/* eslint-disable react/no-multi-comp */ import chai, { expect } from 'chai'; import dirtyChai from 'dirty-chai'; import { mount } from 'enzyme'; @@ -197,3 +198,134 @@ describe('Component extension', () => { }); }); }); + +describe('Component extension with overrides calling super()', () => { + const userComponentWillMountBefore = sinon.spy(); + const userComponentWillMountAfter = sinon.spy(); + const userComponentDidMountBefore = sinon.spy(); + const userComponentDidMountAfter = sinon.spy(); + const userComponentWillReceivePropsBefore = sinon.spy(); + const userComponentWillReceivePropsAfter = sinon.spy(); + const userComponentDidUpdateBefore = sinon.spy(); + const userComponentDidUpdateAfter = sinon.spy(); + + class TestComponentWithSuper extends Component { + componentWillMount() { + userComponentWillMountBefore(); + super.componentWillMount(); + userComponentWillMountAfter(); + } + + componentDidMount() { + userComponentDidMountBefore(); + super.componentDidMount(); + userComponentDidMountAfter(); + } + + componentWillReceiveProps() { + userComponentWillReceivePropsBefore(); + super.componentWillReceiveProps(); + userComponentWillReceivePropsAfter(); + } + + componentDidUpdate() { + userComponentDidUpdateBefore(); + super.componentDidUpdate(); + userComponentDidUpdateAfter(); + } + + render() { + return null; + } + } + + const callbackWill = sinon.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sinon.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); + + afterEach(() => { + const allSpies = [ + userComponentWillMountBefore, userComponentWillMountAfter, + userComponentDidMountBefore, userComponentDidMountAfter, + userComponentWillReceivePropsBefore, userComponentWillReceivePropsAfter, + userComponentDidUpdateBefore, userComponentDidUpdateAfter, + callbackWill, callbackDid, + ]; + allSpies.forEach(spy => spy.reset()); + }); + + describe('componentWillMountOrReceiveProps()', () => { + it('runs once on mount', () => { + mount(); + expect(callbackWill).to.have.been.calledOnce(); + }); + + it('runs user code in override on mount', () => { + mount(); + sinon.assert.callOrder( + userComponentWillMountBefore, callbackWill, userComponentWillMountAfter, + ); + }); + + it('runs on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackWill).to.have.been.calledTwice(); + }); + + it('runs user code in override on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + sinon.assert.callOrder( + userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter, + ); + }); + + it('does not run on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackWill).to.have.been.calledOnce(); + }); + }); + + describe('componentDidMountOrUpdate()', () => { + it('runs once when mounted', () => { + mount(); + expect(callbackDid).to.have.been.calledOnce(); + }); + + it('runs user code in override on mount', () => { + mount(); + sinon.assert.callOrder( + userComponentDidMountBefore, callbackDid, userComponentDidMountAfter, + ); + }); + + it('runs on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs user code in override on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + sinon.assert.callOrder( + userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, + ); + }); + + it('runs on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs user code in override on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + sinon.assert.callOrder( + userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, + ); + }); + }); +}); diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index 3734481..971b1bc 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -1,4 +1,5 @@ /* eslint-env jest */ +/* eslint-disable react/no-multi-comp */ import chai, { expect } from 'chai'; import dirtyChai from 'dirty-chai'; import { mount } from 'enzyme'; @@ -188,3 +189,136 @@ descriptor('PureComponent extension', () => { }); }); }); + +descriptor('PureComponent extension with overrides calling super()', () => { + const { default: PureComponent } = require('./pureComponent'); // eslint-disable-line global-require + + const userComponentWillMountBefore = sinon.spy(); + const userComponentWillMountAfter = sinon.spy(); + const userComponentDidMountBefore = sinon.spy(); + const userComponentDidMountAfter = sinon.spy(); + const userComponentWillReceivePropsBefore = sinon.spy(); + const userComponentWillReceivePropsAfter = sinon.spy(); + const userComponentDidUpdateBefore = sinon.spy(); + const userComponentDidUpdateAfter = sinon.spy(); + + class TestComponentWithSuper extends PureComponent { + componentWillMount() { + userComponentWillMountBefore(); + super.componentWillMount(); + userComponentWillMountAfter(); + } + + componentDidMount() { + userComponentDidMountBefore(); + super.componentDidMount(); + userComponentDidMountAfter(); + } + + componentWillReceiveProps() { + userComponentWillReceivePropsBefore(); + super.componentWillReceiveProps(); + userComponentWillReceivePropsAfter(); + } + + componentDidUpdate() { + userComponentDidUpdateBefore(); + super.componentDidUpdate(); + userComponentDidUpdateAfter(); + } + + render() { + return null; + } + } + + const callbackWill = sinon.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sinon.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); + + afterEach(() => { + const allSpies = [ + userComponentWillMountBefore, userComponentWillMountAfter, + userComponentDidMountBefore, userComponentDidMountAfter, + userComponentWillReceivePropsBefore, userComponentWillReceivePropsAfter, + userComponentDidUpdateBefore, userComponentDidUpdateAfter, + callbackWill, callbackDid, + ]; + allSpies.forEach(spy => spy.reset()); + }); + + describe('componentWillMountOrReceiveProps()', () => { + it('runs once on mount', () => { + mount(); + expect(callbackWill).to.have.been.calledOnce(); + }); + + it('runs user code in override on mount', () => { + mount(); + sinon.assert.callOrder( + userComponentWillMountBefore, callbackWill, userComponentWillMountAfter, + ); + }); + + it('runs on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackWill).to.have.been.calledTwice(); + }); + + it('runs user code in override on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + sinon.assert.callOrder( + userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter, + ); + }); + + it('does not run on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackWill).to.have.been.calledOnce(); + }); + }); + + describe('componentDidMountOrUpdate()', () => { + it('runs once when mounted', () => { + mount(); + expect(callbackDid).to.have.been.calledOnce(); + }); + + it('runs user code in override on mount', () => { + mount(); + sinon.assert.callOrder( + userComponentDidMountBefore, callbackDid, userComponentDidMountAfter, + ); + }); + + it('runs on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs user code in override on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + sinon.assert.callOrder( + userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, + ); + }); + + it('runs on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs user code in override on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + sinon.assert.callOrder( + userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, + ); + }); + }); +}); From b4f963c994888df7b01fe49ca1251c50bc571a71 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 26 Aug 2017 09:12:13 -0500 Subject: [PATCH 11/21] Test component with lifecycle overrides Assert that implementing a lifecycle hook without calling the same hook on the super class will cause the new lifecycle hooks to not run. --- src/component.spec.js | 59 +++++++++++++++++++++++++++++++++++++ src/pureComponent.spec.js | 61 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/component.spec.js b/src/component.spec.js index 229c35a..c5f153e 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -329,3 +329,62 @@ describe('Component extension with overrides calling super()', () => { }); }); }); + +describe('Component extension with overrides not calling super()', () => { + class TestComponentWithoutSuper extends Component { + componentWillMount() {} + componentDidMount() {} + componentWillReceiveProps() {} + componentDidUpdate() {} + + render() { + return null; + } + } + + const callbackWill = sinon.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sinon.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); + + afterEach(() => { + callbackWill.reset(); + callbackDid.reset(); + }); + + describe('componentWillMountOrReceiveProps()', () => { + it('does not run on mount', () => { + mount(); + expect(callbackWill).not.to.have.been.called(); + }); + + it('does not run on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackWill).not.to.have.been.called(); + }); + + it('does not run on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackWill).not.to.have.been.called(); + }); + }); + + describe('componentDidMountOrUpdate()', () => { + it('does not run when mounted', () => { + mount(); + expect(callbackDid).not.to.have.been.called(); + }); + + it('does not run on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackDid).not.to.have.been.called(); + }); + + it('does not run on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackDid).not.to.have.been.called(); + }); + }); +}); diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index 971b1bc..5a9ce62 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -322,3 +322,64 @@ descriptor('PureComponent extension with overrides calling super()', () => { }); }); }); + +descriptor('Component extension with overrides not calling super()', () => { + const { default: PureComponent } = require('./pureComponent'); // eslint-disable-line global-require + + class TestComponentWithoutSuper extends PureComponent { + componentWillMount() {} + componentDidMount() {} + componentWillReceiveProps() {} + componentDidUpdate() {} + + render() { + return null; + } + } + + const callbackWill = sinon.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sinon.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); + + afterEach(() => { + callbackWill.reset(); + callbackDid.reset(); + }); + + describe('componentWillMountOrReceiveProps()', () => { + it('does not run on mount', () => { + mount(); + expect(callbackWill).not.to.have.been.called(); + }); + + it('does not run on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackWill).not.to.have.been.called(); + }); + + it('does not run on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackWill).not.to.have.been.called(); + }); + }); + + describe('componentDidMountOrUpdate()', () => { + it('does not run when mounted', () => { + mount(); + expect(callbackDid).not.to.have.been.called(); + }); + + it('does not run on props update', () => { + const component = mount(); + component.setProps(getUniqueProps()); + expect(callbackDid).not.to.have.been.called(); + }); + + it('does not run on state update', () => { + const component = mount(); + component.setState(getUniqueState()); + expect(callbackDid).not.to.have.been.called(); + }); + }); +}); From 8a03d97842c6c3a50ba3c34314dc5f3652870514 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 26 Aug 2017 09:14:57 -0500 Subject: [PATCH 12/21] Use package entry point for require() Since 2188641, the package entry point is require() friendly to avoid the awkward "default" property. --- src/pureComponent.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index 5a9ce62..7cadb66 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -23,7 +23,7 @@ function getUniqueProps() { const descriptor = React.PureComponent ? describe : describe.skip; descriptor('PureComponent extension', () => { - const { default: PureComponent } = require('./pureComponent'); // eslint-disable-line global-require + const { PureComponent } = require('./'); // eslint-disable-line global-require class TestComponent extends PureComponent { constructor(props) { @@ -191,7 +191,7 @@ descriptor('PureComponent extension', () => { }); descriptor('PureComponent extension with overrides calling super()', () => { - const { default: PureComponent } = require('./pureComponent'); // eslint-disable-line global-require + const { PureComponent } = require('./'); // eslint-disable-line global-require const userComponentWillMountBefore = sinon.spy(); const userComponentWillMountAfter = sinon.spy(); @@ -324,7 +324,7 @@ descriptor('PureComponent extension with overrides calling super()', () => { }); descriptor('Component extension with overrides not calling super()', () => { - const { default: PureComponent } = require('./pureComponent'); // eslint-disable-line global-require + const { PureComponent } = require('./'); // eslint-disable-line global-require class TestComponentWithoutSuper extends PureComponent { componentWillMount() {} From 02bd1f39fe34746196338ade27068ee323f8dd06 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 26 Aug 2017 09:51:29 -0500 Subject: [PATCH 13/21] Remove component variable where unnecessary This helps simplify tests that don't read from the mounted component. --- src/component.spec.js | 68 +++++++++++++++++++-------------------- src/pureComponent.spec.js | 68 +++++++++++++++++++-------------------- 2 files changed, 68 insertions(+), 68 deletions(-) diff --git a/src/component.spec.js b/src/component.spec.js index c5f153e..78e1d95 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -65,8 +65,8 @@ describe('Component extension', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); @@ -89,14 +89,14 @@ describe('Component extension', () => { }); it('runs on props update before render()', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); @@ -128,8 +128,8 @@ describe('Component extension', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); @@ -160,8 +160,8 @@ describe('Component extension', () => { }); it('runs on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); @@ -192,8 +192,8 @@ describe('Component extension', () => { }); it('runs on props update before render()', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); }); }); @@ -267,22 +267,22 @@ describe('Component extension with overrides calling super()', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); sinon.assert.callOrder( userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter, ); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); @@ -301,28 +301,28 @@ describe('Component extension with overrides calling super()', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); }); it('runs on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); @@ -357,14 +357,14 @@ describe('Component extension with overrides not calling super()', () => { }); it('does not run on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill).not.to.have.been.called(); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackWill).not.to.have.been.called(); }); }); @@ -376,14 +376,14 @@ describe('Component extension with overrides not calling super()', () => { }); it('does not run on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid).not.to.have.been.called(); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackDid).not.to.have.been.called(); }); }); diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index 7cadb66..87c606e 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -68,8 +68,8 @@ descriptor('PureComponent extension', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); @@ -92,14 +92,14 @@ descriptor('PureComponent extension', () => { }); it('runs on props update before render()', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); @@ -131,8 +131,8 @@ descriptor('PureComponent extension', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); @@ -157,8 +157,8 @@ descriptor('PureComponent extension', () => { }); it('runs on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); @@ -183,8 +183,8 @@ descriptor('PureComponent extension', () => { }); it('runs on props update before render()', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); }); }); @@ -260,22 +260,22 @@ descriptor('PureComponent extension with overrides calling super()', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); sinon.assert.callOrder( userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter, ); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); @@ -294,28 +294,28 @@ descriptor('PureComponent extension with overrides calling super()', () => { }); it('runs on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); }); it('runs on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); @@ -352,14 +352,14 @@ descriptor('Component extension with overrides not calling super()', () => { }); it('does not run on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackWill).not.to.have.been.called(); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackWill).not.to.have.been.called(); }); }); @@ -371,14 +371,14 @@ descriptor('Component extension with overrides not calling super()', () => { }); it('does not run on props update', () => { - const component = mount(); - component.setProps(getUniqueProps()); + mount() + .setProps(getUniqueProps()); expect(callbackDid).not.to.have.been.called(); }); it('does not run on state update', () => { - const component = mount(); - component.setState(getUniqueState()); + mount() + .setState(getUniqueState()); expect(callbackDid).not.to.have.been.called(); }); }); From fa63d96b29424baafb56f14883449872385e7f18 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sat, 26 Aug 2017 12:41:54 -0500 Subject: [PATCH 14/21] Create test fakes in sinon sandbox The sandbox is easily reset between test runs. --- src/component.spec.js | 56 +++++++++++++++------------------------ src/pureComponent.spec.js | 56 +++++++++++++++------------------------ 2 files changed, 42 insertions(+), 70 deletions(-) diff --git a/src/component.spec.js b/src/component.spec.js index 78e1d95..2ce090a 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -13,6 +13,8 @@ import Component from './component'; chai.use(dirtyChai); chai.use(sinonChai); +const sandbox = sinon.createSandbox(); + function getUniqueState() { return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; } @@ -33,15 +35,11 @@ describe('Component extension', () => { } } - const callbackWill = sinon.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps'); - const callbackDid = sinon.spy(TestComponent.prototype, 'componentDidMountOrUpdate'); - const callbackRender = sinon.spy(TestComponent.prototype, 'render'); + const callbackWill = sandbox.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sandbox.spy(TestComponent.prototype, 'componentDidMountOrUpdate'); + const callbackRender = sandbox.spy(TestComponent.prototype, 'render'); - afterEach(() => { - callbackWill.reset(); - callbackDid.reset(); - callbackRender.reset(); - }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { @@ -200,14 +198,14 @@ describe('Component extension', () => { }); describe('Component extension with overrides calling super()', () => { - const userComponentWillMountBefore = sinon.spy(); - const userComponentWillMountAfter = sinon.spy(); - const userComponentDidMountBefore = sinon.spy(); - const userComponentDidMountAfter = sinon.spy(); - const userComponentWillReceivePropsBefore = sinon.spy(); - const userComponentWillReceivePropsAfter = sinon.spy(); - const userComponentDidUpdateBefore = sinon.spy(); - const userComponentDidUpdateAfter = sinon.spy(); + const userComponentWillMountBefore = sandbox.spy(); + const userComponentWillMountAfter = sandbox.spy(); + const userComponentDidMountBefore = sandbox.spy(); + const userComponentDidMountAfter = sandbox.spy(); + const userComponentWillReceivePropsBefore = sandbox.spy(); + const userComponentWillReceivePropsAfter = sandbox.spy(); + const userComponentDidUpdateBefore = sandbox.spy(); + const userComponentDidUpdateAfter = sandbox.spy(); class TestComponentWithSuper extends Component { componentWillMount() { @@ -239,19 +237,10 @@ describe('Component extension with overrides calling super()', () => { } } - const callbackWill = sinon.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); - const callbackDid = sinon.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); - - afterEach(() => { - const allSpies = [ - userComponentWillMountBefore, userComponentWillMountAfter, - userComponentDidMountBefore, userComponentDidMountAfter, - userComponentWillReceivePropsBefore, userComponentWillReceivePropsAfter, - userComponentDidUpdateBefore, userComponentDidUpdateAfter, - callbackWill, callbackDid, - ]; - allSpies.forEach(spy => spy.reset()); - }); + const callbackWill = sandbox.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sandbox.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); + + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { @@ -342,13 +331,10 @@ describe('Component extension with overrides not calling super()', () => { } } - const callbackWill = sinon.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); - const callbackDid = sinon.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); + const callbackWill = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); - afterEach(() => { - callbackWill.reset(); - callbackDid.reset(); - }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('does not run on mount', () => { diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index 87c606e..36759dc 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -11,6 +11,8 @@ import sinonChai from 'sinon-chai'; chai.use(dirtyChai); chai.use(sinonChai); +const sandbox = sinon.createSandbox(); + function getUniqueState() { return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; } @@ -36,15 +38,11 @@ descriptor('PureComponent extension', () => { } } - const callbackWill = sinon.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps'); - const callbackDid = sinon.spy(TestComponent.prototype, 'componentDidMountOrUpdate'); - const callbackRender = sinon.spy(TestComponent.prototype, 'render'); + const callbackWill = sandbox.spy(TestComponent.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sandbox.spy(TestComponent.prototype, 'componentDidMountOrUpdate'); + const callbackRender = sandbox.spy(TestComponent.prototype, 'render'); - afterEach(() => { - callbackWill.reset(); - callbackDid.reset(); - callbackRender.reset(); - }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { @@ -193,14 +191,14 @@ descriptor('PureComponent extension', () => { descriptor('PureComponent extension with overrides calling super()', () => { const { PureComponent } = require('./'); // eslint-disable-line global-require - const userComponentWillMountBefore = sinon.spy(); - const userComponentWillMountAfter = sinon.spy(); - const userComponentDidMountBefore = sinon.spy(); - const userComponentDidMountAfter = sinon.spy(); - const userComponentWillReceivePropsBefore = sinon.spy(); - const userComponentWillReceivePropsAfter = sinon.spy(); - const userComponentDidUpdateBefore = sinon.spy(); - const userComponentDidUpdateAfter = sinon.spy(); + const userComponentWillMountBefore = sandbox.spy(); + const userComponentWillMountAfter = sandbox.spy(); + const userComponentDidMountBefore = sandbox.spy(); + const userComponentDidMountAfter = sandbox.spy(); + const userComponentWillReceivePropsBefore = sandbox.spy(); + const userComponentWillReceivePropsAfter = sandbox.spy(); + const userComponentDidUpdateBefore = sandbox.spy(); + const userComponentDidUpdateAfter = sandbox.spy(); class TestComponentWithSuper extends PureComponent { componentWillMount() { @@ -232,19 +230,10 @@ descriptor('PureComponent extension with overrides calling super()', () => { } } - const callbackWill = sinon.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); - const callbackDid = sinon.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); - - afterEach(() => { - const allSpies = [ - userComponentWillMountBefore, userComponentWillMountAfter, - userComponentDidMountBefore, userComponentDidMountAfter, - userComponentWillReceivePropsBefore, userComponentWillReceivePropsAfter, - userComponentDidUpdateBefore, userComponentDidUpdateAfter, - callbackWill, callbackDid, - ]; - allSpies.forEach(spy => spy.reset()); - }); + const callbackWill = sandbox.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sandbox.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); + + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { @@ -337,13 +326,10 @@ descriptor('Component extension with overrides not calling super()', () => { } } - const callbackWill = sinon.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); - const callbackDid = sinon.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); + const callbackWill = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); + const callbackDid = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); - afterEach(() => { - callbackWill.reset(); - callbackDid.reset(); - }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('does not run on mount', () => { From 39fa32100b14c2c136ba1e8fa51147fbfc82e3ec Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Sun, 27 Aug 2017 11:10:40 -0500 Subject: [PATCH 15/21] Set up test component in beforeEach() This centralizes code for mounting the component under test. --- src/component.spec.js | 90 +++++++++++++-------------------------- src/pureComponent.spec.js | 88 +++++++++++++------------------------- 2 files changed, 60 insertions(+), 118 deletions(-) diff --git a/src/component.spec.js b/src/component.spec.js index 2ce090a..68e6b57 100644 --- a/src/component.spec.js +++ b/src/component.spec.js @@ -14,6 +14,7 @@ chai.use(dirtyChai); chai.use(sinonChai); const sandbox = sinon.createSandbox(); +let component; function getUniqueState() { return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; @@ -39,159 +40,137 @@ describe('Component extension', () => { const callbackDid = sandbox.spy(TestComponent.prototype, 'componentDidMountOrUpdate'); const callbackRender = sandbox.spy(TestComponent.prototype, 'render'); + beforeEach(() => { + component = mount(); + }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { - mount(); expect(callbackWill).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { - const component = mount(); expect(callbackWill.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with "this" context of component', () => { - const component = mount(); expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs on mount before render()', () => { - mount(); expect(callbackWill).to.have.been.calledBefore(callbackRender); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update when no props change', () => { - const component = mount(); component.setProps(component.props()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update with first parameter of component props', () => { - const component = mount(); component.setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledWith(component.props()); }); it('runs on props update with "this" context of component', () => { - const component = mount(); component.setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); describe('componentDidMountOrUpdate()', () => { it('runs once when mounted', () => { - mount(); expect(callbackDid).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { - const component = mount(); expect(callbackDid.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with second parameter of component state', () => { - const component = mount(); expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state()); }); it('runs on mount with "this" context of component', () => { - const component = mount(); expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs after render()', () => { - mount(); expect(callbackDid).to.have.been.calledAfter(callbackRender); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs on props update when no props change', () => { - const component = mount(); component.setProps(component.props()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs on props update with first parameter of previous component props', () => { - const component = mount(); const initialProps = component.props(); component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on props update with second parameter of previous component state', () => { - const component = mount(); const initialState = component.state(); component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on props update with "this" context of component', () => { - const component = mount(); component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs on state update when no state changes', () => { - const component = mount(); component.setState(component.state()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs on state update with first parameter of previous component props', () => { - const component = mount(); const initialProps = component.props(); component.setState(getUniqueState()); expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on state update with second parameter of previous component state', () => { - const component = mount(); const initialState = component.state(); component.setState(getUniqueState()); expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on state update with "this" context of component', () => { - const component = mount(); component.setState(getUniqueState()); expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); }); }); @@ -240,78 +219,71 @@ describe('Component extension with overrides calling super()', () => { const callbackWill = sandbox.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); const callbackDid = sandbox.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); + beforeEach(() => { + component = mount(); + }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { - mount(); expect(callbackWill).to.have.been.calledOnce(); }); it('runs user code in override on mount', () => { - mount(); sinon.assert.callOrder( userComponentWillMountBefore, callbackWill, userComponentWillMountAfter, ); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); sinon.assert.callOrder( userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter, ); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); describe('componentDidMountOrUpdate()', () => { it('runs once when mounted', () => { - mount(); expect(callbackDid).to.have.been.calledOnce(); }); it('runs user code in override on mount', () => { - mount(); sinon.assert.callOrder( userComponentDidMountBefore, callbackDid, userComponentDidMountAfter, ); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); }); it('runs on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); @@ -334,42 +306,40 @@ describe('Component extension with overrides not calling super()', () => { const callbackWill = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); const callbackDid = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); + beforeEach(() => { + component = mount(); + }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('does not run on mount', () => { - mount(); expect(callbackWill).not.to.have.been.called(); }); it('does not run on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill).not.to.have.been.called(); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackWill).not.to.have.been.called(); }); }); describe('componentDidMountOrUpdate()', () => { it('does not run when mounted', () => { - mount(); expect(callbackDid).not.to.have.been.called(); }); it('does not run on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid).not.to.have.been.called(); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackDid).not.to.have.been.called(); }); }); diff --git a/src/pureComponent.spec.js b/src/pureComponent.spec.js index 36759dc..820bb82 100644 --- a/src/pureComponent.spec.js +++ b/src/pureComponent.spec.js @@ -12,6 +12,7 @@ chai.use(dirtyChai); chai.use(sinonChai); const sandbox = sinon.createSandbox(); +let component; function getUniqueState() { return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; @@ -42,147 +43,127 @@ descriptor('PureComponent extension', () => { const callbackDid = sandbox.spy(TestComponent.prototype, 'componentDidMountOrUpdate'); const callbackRender = sandbox.spy(TestComponent.prototype, 'render'); + beforeEach(() => { + component = mount(); + }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { - mount(); expect(callbackWill).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { - const component = mount(); expect(callbackWill.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with "this" context of component', () => { - const component = mount(); expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs on mount before render()', () => { - mount(); expect(callbackWill).to.have.been.calledBefore(callbackRender); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update when no props change', () => { - const component = mount(); component.setProps(component.props()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs on props update with first parameter of component props', () => { - const component = mount(); component.setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledWith(component.props()); }); it('runs on props update with "this" context of component', () => { - const component = mount(); component.setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); describe('componentDidMountOrUpdate()', () => { it('runs once when mounted', () => { - mount(); expect(callbackDid).to.have.been.calledOnce(); }); it('runs on mount with first parameter of component props', () => { - const component = mount(); expect(callbackDid.firstCall).to.have.been.calledWith(component.props()); }); it('runs on mount with second parameter of component state', () => { - const component = mount(); expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state()); }); it('runs on mount with "this" context of component', () => { - const component = mount(); expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode()); }); it('runs after render()', () => { - mount(); expect(callbackDid).to.have.been.calledAfter(callbackRender); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs on props update with first parameter of previous component props', () => { - const component = mount(); const initialProps = component.props(); component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on props update with second parameter of previous component state', () => { - const component = mount(); const initialState = component.state(); component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on props update with "this" context of component', () => { - const component = mount(); component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs on state update with first parameter of previous component props', () => { - const component = mount(); const initialProps = component.props(); component.setState(getUniqueState()); expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); }); it('runs on state update with second parameter of previous component state', () => { - const component = mount(); const initialState = component.state(); component.setState(getUniqueState()); expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); }); it('runs on state update with "this" context of component', () => { - const component = mount(); component.setState(getUniqueState()); expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); }); it('runs on props update before render()', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); }); }); @@ -233,78 +214,71 @@ descriptor('PureComponent extension with overrides calling super()', () => { const callbackWill = sandbox.spy(TestComponentWithSuper.prototype, 'componentWillMountOrReceiveProps'); const callbackDid = sandbox.spy(TestComponentWithSuper.prototype, 'componentDidMountOrUpdate'); + beforeEach(() => { + component = mount(); + }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('runs once on mount', () => { - mount(); expect(callbackWill).to.have.been.calledOnce(); }); it('runs user code in override on mount', () => { - mount(); sinon.assert.callOrder( userComponentWillMountBefore, callbackWill, userComponentWillMountAfter, ); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); sinon.assert.callOrder( userComponentWillReceivePropsBefore, callbackWill, userComponentWillReceivePropsAfter, ); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackWill).to.have.been.calledOnce(); }); }); describe('componentDidMountOrUpdate()', () => { it('runs once when mounted', () => { - mount(); expect(callbackDid).to.have.been.calledOnce(); }); it('runs user code in override on mount', () => { - mount(); sinon.assert.callOrder( userComponentDidMountBefore, callbackDid, userComponentDidMountAfter, ); }); it('runs on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); }); it('runs on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackDid).to.have.been.calledTwice(); }); it('runs user code in override on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); sinon.assert.callOrder( userComponentDidUpdateBefore, callbackDid, userComponentDidUpdateAfter, ); @@ -329,42 +303,40 @@ descriptor('Component extension with overrides not calling super()', () => { const callbackWill = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentWillMountOrReceiveProps'); const callbackDid = sandbox.spy(TestComponentWithoutSuper.prototype, 'componentDidMountOrUpdate'); + beforeEach(() => { + component = mount(); + }); + afterEach(() => sandbox.reset()); describe('componentWillMountOrReceiveProps()', () => { it('does not run on mount', () => { - mount(); expect(callbackWill).not.to.have.been.called(); }); it('does not run on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackWill).not.to.have.been.called(); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackWill).not.to.have.been.called(); }); }); describe('componentDidMountOrUpdate()', () => { it('does not run when mounted', () => { - mount(); expect(callbackDid).not.to.have.been.called(); }); it('does not run on props update', () => { - mount() - .setProps(getUniqueProps()); + component.setProps(getUniqueProps()); expect(callbackDid).not.to.have.been.called(); }); it('does not run on state update', () => { - mount() - .setState(getUniqueState()); + component.setState(getUniqueState()); expect(callbackDid).not.to.have.been.called(); }); }); From 9ed78d61c9364216e585be797cfeedfef75dc5d1 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Thu, 7 Sep 2017 20:47:38 -0500 Subject: [PATCH 16/21] Enable ES2016 rest/spread syntax --- .babelrc | 1 + .editorconfig | 2 +- package.json | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.babelrc b/.babelrc index 86c445f..2e23624 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,4 @@ { + "plugins": ["transform-object-rest-spread"], "presets": ["es2015", "react"] } diff --git a/.editorconfig b/.editorconfig index 4eb6462..f05c22d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,6 +9,6 @@ end_of_line = lf insert_final_newline = true indent_style = tab -[package.json] +[{.babelrc,package.json}] indent_style = space indent_size = 2 diff --git a/package.json b/package.json index ce8c57a..8bfba16 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "devDependencies": { "babel-cli": "^6.26.0", "babel-jest": "^20.0.3", + "babel-plugin-transform-object-rest-spread": "^6.26.0", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "chai": "^4.0.2", From a1f1d0254a8a2605bf908250d491d7453241fc2d Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Thu, 7 Sep 2017 20:48:19 -0500 Subject: [PATCH 17/21] Exclude built code from ESLint --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index c64162c..0d9033d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ /coverage/* +/lib/* From 92f41cd9269941954b757bf58d0b46499319df0c Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Thu, 7 Sep 2017 21:00:07 -0500 Subject: [PATCH 18/21] Add higher-order component This adds compatibility for react-create-class. --- package.json | 4 + src/index.js | 2 + src/withEvents.js | 39 +++++++ src/withEvents.spec.js | 245 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 290 insertions(+) create mode 100644 src/withEvents.js create mode 100644 src/withEvents.spec.js diff --git a/package.json b/package.json index 8bfba16..c04fafe 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "babel-preset-react": "^6.24.1", "chai": "^4.0.2", "codecov": "^2.3.0", + "create-react-class": "^15.6.0", "dirty-chai": "^2.0.0", "enzyme": "^2.8.2", "eslint": "^3.19.0", @@ -57,5 +58,8 @@ "testMatch": [ "**/src/*.spec.js?(x)" ] + }, + "dependencies": { + "lodash.wrap": "^4.1.1" } } diff --git a/src/index.js b/src/index.js index 9936847..06680e5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,9 @@ import Component from './component'; import PureComponent from './pureComponent'; +import withEvents from './withEvents'; module.exports = { Component, PureComponent, + withEvents, }; diff --git a/src/withEvents.js b/src/withEvents.js new file mode 100644 index 0000000..bcd7787 --- /dev/null +++ b/src/withEvents.js @@ -0,0 +1,39 @@ +import wrap from 'lodash.wrap'; + +function noop() {} + +export default function withEvents(config) { + function willMountCustom(nativeFunc = noop, ...args) { + const result = nativeFunc(...args); + this.componentWillMountOrReceiveProps(this.props); + return result; + } + + function didMountCustom(nativeFunc = noop, ...args) { + const result = nativeFunc(...args); + this.componentDidMountOrUpdate(this.props, this.state); + return result; + } + + function willReceivePropsCustom(nativeFunc = noop, ...args) { + const result = nativeFunc(...args); + this.componentWillMountOrReceiveProps(...args); + return result; + } + + function didUpdateCustom(nativeFunc = noop, ...args) { + const result = nativeFunc(...args); + this.componentDidMountOrUpdate(...args); + return result; + } + + return { + componentWillMountOrReceiveProps: noop, + componentDidMountOrUpdate: noop, + ...config, + componentWillMount: wrap(config.componentWillMount, willMountCustom), + componentDidMount: wrap(config.componentDidMount, didMountCustom), + componentWillReceiveProps: wrap(config.componentWillReceiveProps, willReceivePropsCustom), + componentDidUpdate: wrap(config.componentDidUpdate, didUpdateCustom), + }; +} diff --git a/src/withEvents.spec.js b/src/withEvents.spec.js new file mode 100644 index 0000000..b1aa483 --- /dev/null +++ b/src/withEvents.spec.js @@ -0,0 +1,245 @@ +/* eslint-env jest */ +import chai, { expect } from 'chai'; +import createReactClass from 'create-react-class'; +import dirtyChai from 'dirty-chai'; +import { mount } from 'enzyme'; +import uniqueId from 'lodash.uniqueid'; +import React from 'react'; +import sinon from 'sinon'; +import sinonChai from 'sinon-chai'; +import withEvents from './withEvents'; + +chai.use(dirtyChai); +chai.use(sinonChai); + +const sandbox = sinon.createSandbox(); +let component; + +function getUniqueState() { + return { [uniqueId('stateVarName')]: uniqueId('stateVarValue') }; +} + +function getUniqueProps() { + return { [uniqueId('propName')]: uniqueId('propValue') }; +} + +describe('withEvents extension', () => { + const callbackWill = sandbox.spy(); + const callbackDid = sandbox.spy(); + + const TestComponent = createReactClass(withEvents({ + getInitialState: getUniqueState, + componentWillMountOrReceiveProps: callbackWill, + componentDidMountOrUpdate: callbackDid, + render: () => null, + })); + + const callbackRender = sandbox.spy(TestComponent.prototype, 'render'); + + beforeEach(() => { + component = mount(); + }); + + afterEach(() => sandbox.reset()); + + describe('componentWillMountOrReceiveProps()', () => { + it('runs once on mount', () => { + expect(callbackWill).to.have.been.calledOnce(); + }); + + it('runs on mount with first parameter of component props', () => { + expect(callbackWill.firstCall).to.have.been.calledWith(component.props()); + }); + + it('runs on mount with "this" context of component', () => { + expect(callbackWill.firstCall).to.have.been.calledOn(component.getNode()); + }); + + it('runs on mount before render()', () => { + expect(callbackWill).to.have.been.calledBefore(callbackRender); + }); + + it('runs on props update', () => { + component.setProps(getUniqueProps()); + expect(callbackWill).to.have.been.calledTwice(); + }); + + it('runs on props update when no props change', () => { + component.setProps(component.props()); + expect(callbackWill).to.have.been.calledTwice(); + }); + + it('runs on props update with first parameter of component props', () => { + component.setProps(getUniqueProps()); + expect(callbackWill.secondCall).to.have.been.calledWith(component.props()); + }); + + it('runs on props update with "this" context of component', () => { + component.setProps(getUniqueProps()); + expect(callbackWill.secondCall).to.have.been.calledOn(component.getNode()); + }); + + it('runs on props update before render()', () => { + component.setProps(getUniqueProps()); + expect(callbackWill.secondCall).to.have.been.calledBefore(callbackRender.secondCall); + }); + + it('does not run on state update', () => { + component.setState(getUniqueState()); + expect(callbackWill).to.have.been.calledOnce(); + }); + }); + + describe('componentDidMountOrUpdate()', () => { + it('runs once when mounted', () => { + expect(callbackDid).to.have.been.calledOnce(); + }); + + it('runs on mount with first parameter of component props', () => { + expect(callbackDid.firstCall).to.have.been.calledWith(component.props()); + }); + + it('runs on mount with second parameter of component state', () => { + expect(callbackDid.firstCall).to.have.been.calledWith(sinon.match.any, component.state()); + }); + + it('runs on mount with "this" context of component', () => { + expect(callbackDid.firstCall).to.have.been.calledOn(component.getNode()); + }); + + it('runs after render()', () => { + expect(callbackDid).to.have.been.calledAfter(callbackRender); + }); + + it('runs on props update', () => { + component.setProps(getUniqueProps()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs on props update with first parameter of previous component props', () => { + const initialProps = component.props(); + component.setProps(getUniqueProps()); + expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); + }); + + it('runs on props update with second parameter of previous component state', () => { + const initialState = component.state(); + component.setProps(getUniqueProps()); + expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); + }); + + it('runs on props update with "this" context of component', () => { + component.setProps(getUniqueProps()); + expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); + }); + + it('runs on state update', () => { + component.setState(getUniqueState()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs on state update with first parameter of previous component props', () => { + const initialProps = component.props(); + component.setState(getUniqueState()); + expect(callbackDid.secondCall).to.have.been.calledWith(initialProps); + }); + + it('runs on state update with second parameter of previous component state', () => { + const initialState = component.state(); + component.setState(getUniqueState()); + expect(callbackDid.secondCall).to.have.been.calledWith(sinon.match.any, initialState); + }); + + it('runs on state update with "this" context of component', () => { + component.setState(getUniqueState()); + expect(callbackDid.secondCall).to.have.been.calledOn(component.getNode()); + }); + + it('runs on props update before render()', () => { + component.setProps(getUniqueProps()); + expect(callbackDid.secondCall).to.have.been.calledAfter(callbackRender.secondCall); + }); + }); +}); + +describe('withEvents extension with overrides', () => { + const userComponentWillMount = sandbox.spy(); + const userComponentDidMount = sandbox.spy(); + const userComponentWillReceiveProps = sandbox.spy(); + const userComponentDidUpdate = sandbox.spy(); + + const callbackWill = sandbox.spy(); + const callbackDid = sandbox.spy(); + + const TestComponentWithOverrides = createReactClass(withEvents({ + getInitialState: getUniqueState, + componentWillMount: userComponentWillMount, + componentDidMount: userComponentDidMount, + componentWillReceiveProps: userComponentWillReceiveProps, + componentDidUpdate: userComponentDidUpdate, + componentWillMountOrReceiveProps: callbackWill, + componentDidMountOrUpdate: callbackDid, + render: () => null, + })); + + beforeEach(() => { + component = mount(); + }); + + afterEach(() => sandbox.reset()); + + describe('componentWillMountOrReceiveProps()', () => { + it('runs once on mount', () => { + expect(callbackWill).to.have.been.calledOnce(); + }); + + it('runs user code in override on mount', () => { + expect(userComponentWillMount).to.have.been.calledBefore(callbackWill); + }); + + it('runs on props update', () => { + component.setProps(getUniqueProps()); + expect(callbackWill).to.have.been.calledTwice(); + }); + + it('runs user code in override on props update', () => { + component.setProps(getUniqueProps()); + expect(userComponentWillReceiveProps).to.have.been.calledBefore(callbackWill.secondCall); + }); + + it('does not run on state update', () => { + component.setState(getUniqueState()); + expect(callbackWill).to.have.been.calledOnce(); + }); + }); + + describe('componentDidMountOrUpdate()', () => { + it('runs once when mounted', () => { + expect(callbackDid).to.have.been.calledOnce(); + }); + + it('runs user code in override on mount', () => { + expect(userComponentDidMount).to.have.been.calledBefore(callbackDid); + }); + + it('runs on props update', () => { + component.setProps(getUniqueProps()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs user code in override on props update', () => { + component.setProps(getUniqueProps()); + expect(userComponentDidUpdate).to.have.been.calledBefore(callbackDid.secondCall); + }); + + it('runs on state update', () => { + component.setState(getUniqueState()); + expect(callbackDid).to.have.been.calledTwice(); + }); + + it('runs user code in override on state update', () => { + component.setState(getUniqueState()); + expect(userComponentDidUpdate).to.have.been.calledBefore(callbackDid.secondCall); + }); + }); +}); From 98b476de92c437a7b395014f4f098ed58d18359a Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Thu, 7 Sep 2017 22:16:17 -0500 Subject: [PATCH 19/21] Document React as a peer dependency. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0085918..05e23e6 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,8 @@ yarn users: yarn add react-component-update ``` +`react-component-update` does not include its own version of React. It will use whatever version is already installed in your project. + ## Usage To extend React's `Component` class: From 27c8d97e61dcad2c9e4b69a4be47ec484ce872a6 Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Thu, 7 Sep 2017 22:17:55 -0500 Subject: [PATCH 20/21] Document withEvents() higher-order component. --- README.md | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 05e23e6..6c7530b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Build Status](https://travis-ci.org/wimpyprogrammer/react-component-update.svg?branch=master)](https://travis-ci.org/wimpyprogrammer/react-component-update) [![codecov](https://codecov.io/gh/wimpyprogrammer/react-component-update/branch/master/graph/badge.svg)](https://codecov.io/gh/wimpyprogrammer/react-component-update) -Extends the React `Component` and `PureComponent` classes with convenience lifecycle events. +Adds convenience lifecycle events to your React components. - `componentWillMountOrReceiveProps(nextProps)` - Combines the [`componentWillMount()`](https://facebook.github.io/react/docs/react-component.html#componentwillmount) and [`componentWillReceiveProps(nextProps)`](https://facebook.github.io/react/docs/react-component.html#componentwillreceiveprops) events. This allows you to consolidate all pre-`render()` logic. @@ -57,6 +57,27 @@ Or to extend React's `PureComponent` class (available in React v15.3.0+): import { PureComponent } from 'react-component-update'; ``` +For compatibility with [`create-react-class`](https://www.npmjs.com/package/create-react-class), use the `withEvents()` higher-order component. + +```js +import createReactClass from 'create-react-class'; +import { withEvents } from 'react-component-update'; + +const MyReactComponent = createReactClass(withEvents({ + componentWillMountOrReceiveProps: function(nextProps) { + // Code that runs before every render(). + }, + + componentDidMountOrUpdate: function(prevProps, prevState) { + // Code that runs after every render(). + }, + + render: function() { + return
; + } +})); +``` + ## Mixing with your own lifecycle events `react-component-update` implements four lifecycle events of the React base classes: @@ -65,7 +86,7 @@ import { PureComponent } from 'react-component-update'; - `componentWillReceiveProps()` - `componentDidUpdate()` -If you also implement these events in your component, you will need to call the corresponding `super()` method like so: +If you extend `Component` or `PureComponent` from `react-component-update` and you also implement these events in your component, you will need to call the corresponding `super()` method like so: ```js componentWillMount() { @@ -87,6 +108,8 @@ componentDidUpdate(prevProps, prevState) { The `super()` method can be called anywhere in your function to suit your needs. +If you use the `withEvents()` higher-order component, you do not need to add any extra code to your events. The new event (ex. `componentDidMountOrUpdate()`) will always run after the related built-in event (ex. `componentDidUpdate()`). + ## License [MIT](/LICENSE.md) From f3b7747cc94fbb7b9dcda6f6aa6fa8e21683195b Mon Sep 17 00:00:00 2001 From: Drew Keller Date: Thu, 7 Sep 2017 22:21:08 -0500 Subject: [PATCH 21/21] Bump to version 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c04fafe..ed273e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-component-update", - "version": "1.0.0-alpha", + "version": "1.0.0", "description": "Extends the native React Component to streamline updates", "main": "lib/index.js", "scripts": {