Back to Repositories

Testing componentWillReceiveProps Lifecycle Implementation in Preact

This test suite examines the componentWillReceiveProps lifecycle method in Preact components. It verifies the behavior, timing, and state management of this lifecycle hook through comprehensive test cases.

Test Coverage Overview

The test suite covers multiple aspects of componentWillReceiveProps functionality:

  • State updates within componentWillReceiveProps
  • Initial render behavior
  • Interaction with setState and forceUpdate
  • Props updates from parent components
  • Execution order with other lifecycle methods

Implementation Analysis

The testing approach uses nested component hierarchies to validate parent-child interactions and lifecycle method sequencing. Tests utilize Jest’s assertion patterns and Sinon spies to track method calls and verify execution order.

Component state management is tested through various scenarios including direct setState calls, forceUpdate triggers, and prop changes.

Technical Details

  • Testing Framework: Jest
  • Assertion Library: Sinon for spies and mocks
  • Setup Utilities: setupScratch and setupRerender from Preact test utils
  • Component Structure: Nested class components with lifecycle methods

Best Practices Demonstrated

The test suite exemplifies strong testing practices including:

  • Isolated component testing
  • Comprehensive lifecycle method verification
  • Clean setup and teardown between tests
  • Edge case coverage
  • Clear test case organization and naming

preactjs/preact

test/browser/lifecycles/componentWillReceiveProps.test.js

            
import { setupRerender } from 'preact/test-utils';
import { createElement, render, Component } from 'preact';
import { setupScratch, teardown } from '../../_util/helpers';

/** @jsx createElement */

describe('Lifecycle methods', () => {
	/** @type {HTMLDivElement} */
	let scratch;

	/** @type {() => void} */
	let rerender;

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
	});

	afterEach(() => {
		teardown(scratch);
	});

	describe('#componentWillReceiveProps', () => {
		it('should update state when called setState in componentWillReceiveProps', () => {
			let componentState;

			class Foo extends Component {
				constructor(props) {
					super(props);
					this.state = {
						dummy: 0
					};
				}
				componentDidMount() {
					// eslint-disable-next-line react/no-did-mount-set-state
					this.setState({ dummy: 1 });
				}
				render() {
					return <Bar dummy={this.state.dummy} />;
				}
			}
			class Bar extends Component {
				constructor(props) {
					super(props);
					this.state = {
						value: 0
					};
				}
				componentWillReceiveProps() {
					this.setState({ value: 1 });
				}
				render() {
					componentState = this.state;
					return <div />;
				}
			}

			render(<Foo />, scratch);
			rerender();

			expect(componentState).to.deep.equal({ value: 1 });

			const cWRP = Foo.prototype.componentWillReceiveProps;
			delete Foo.prototype.componentWillReceiveProps;

			Foo.prototype.shouldComponentUpdate = cWRP;

			render(null, scratch);
			render(<Foo />, scratch);
			rerender();

			expect(componentState, 'via shouldComponentUpdate').to.deep.equal({
				value: 1
			});

			delete Foo.prototype.shouldComponentUpdate;
			Foo.prototype.componentWillUpdate = cWRP;

			render(null, scratch);
			render(<Foo />, scratch);
			rerender();

			expect(componentState, 'via componentWillUpdate').to.deep.equal({
				value: 1
			});
		});

		it('should NOT be called on initial render', () => {
			class ReceivePropsComponent extends Component {
				componentWillReceiveProps() {}
				render() {
					return <div />;
				}
			}
			sinon.spy(ReceivePropsComponent.prototype, 'componentWillReceiveProps');
			render(<ReceivePropsComponent />, scratch);
			expect(ReceivePropsComponent.prototype.componentWillReceiveProps).not.to
				.have.been.called;
		});

		// See last paragraph of cWRP section https://reactjs.org/docs/react-component.html#unsafe_componentwillreceiveprops
		it('should not be called on setState or forceUpdate', () => {
			let spy = sinon.spy();
			let spyInner = sinon.spy();
			let c;

			class Inner extends Component {
				componentWillReceiveProps() {
					spyInner();
				}

				render() {
					return <div>foo</div>;
				}
			}

			class Outer extends Component {
				constructor() {
					super();
					c = this;
				}

				componentWillReceiveProps() {
					spy();
				}

				render() {
					return <Inner />;
				}
			}

			render(<Outer />, scratch);
			expect(spy).to.not.be.called;

			c.setState({});
			rerender();
			expect(spy).to.not.be.called;
			expect(spyInner).to.be.calledOnce;
			spy.resetHistory();
			spyInner.resetHistory();

			c.forceUpdate();
			rerender();
			expect(spy).to.not.be.called;
			expect(spyInner).to.be.calledOnce;
		});

		it('should be called when rerender with new props from parent', () => {
			let doRender;
			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return <Inner i={i} {...props} />;
				}
			}
			class Inner extends Component {
				componentWillMount() {
					expect(this.props.i).to.be.equal(0);
				}
				componentWillReceiveProps(nextProps) {
					expect(nextProps.i).to.be.equal(1);
				}
				render() {
					return <div />;
				}
			}
			sinon.spy(Inner.prototype, 'componentWillReceiveProps');
			sinon.spy(Outer.prototype, 'componentDidMount');

			// Initial render
			render(<Outer />, scratch);
			expect(Inner.prototype.componentWillReceiveProps).not.to.have.been.called;

			// Rerender inner with new props
			doRender();
			rerender();
			expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
		});

		it('should be called when rerender with new props from parent even with setState/forceUpdate in child', () => {
			let setStateAndUpdateProps;
			let forceUpdateAndUpdateProps;
			let cWRPSpy = sinon.spy();

			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
					this.update = this.update.bind(this);
				}
				update() {
					this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return <Inner i={i} update={this.update} />;
				}
			}
			class Inner extends Component {
				componentDidMount() {
					expect(this.props.i).to.be.equal(0);

					setStateAndUpdateProps = () => {
						this.setState({});
						this.props.update();
					};
					forceUpdateAndUpdateProps = () => {
						this.forceUpdate();
						this.props.update();
					};
				}
				componentWillReceiveProps(nextProps) {
					cWRPSpy(nextProps.i);
				}
				render() {
					return <div />;
				}
			}
			// Initial render
			render(<Outer />, scratch);
			expect(cWRPSpy).not.to.have.been.called;

			// setState in inner component and update with new props
			setStateAndUpdateProps();
			rerender();
			expect(cWRPSpy).to.have.been.calledWith(1);

			// forceUpdate in inner component and update with new props
			forceUpdateAndUpdateProps();
			rerender();
			expect(cWRPSpy).to.have.been.calledWith(2);
		});

		it('should be called in right execution order', () => {
			let doRender;
			class Outer extends Component {
				constructor(p, c) {
					super(p, c);
					this.state = { i: 0 };
				}
				componentDidMount() {
					doRender = () => this.setState({ i: this.state.i + 1 });
				}
				render(props, { i }) {
					return <Inner i={i} {...props} />;
				}
			}
			class Inner extends Component {
				componentDidUpdate() {
					expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
					expect(Inner.prototype.componentWillUpdate).to.have.been.called;
				}
				componentWillReceiveProps() {
					expect(Inner.prototype.componentWillUpdate).not.to.have.been.called;
					expect(Inner.prototype.componentDidUpdate).not.to.have.been.called;
				}
				componentWillUpdate() {
					expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
					expect(Inner.prototype.componentDidUpdate).not.to.have.been.called;
				}
				shouldComponentUpdate() {
					expect(Inner.prototype.componentWillReceiveProps).to.have.been.called;
					expect(Inner.prototype.componentWillUpdate).not.to.have.been.called;
					return true;
				}
				render() {
					return <div />;
				}
			}
			sinon.spy(Inner.prototype, 'componentWillReceiveProps');
			sinon.spy(Inner.prototype, 'componentDidUpdate');
			sinon.spy(Inner.prototype, 'componentWillUpdate');
			sinon.spy(Inner.prototype, 'shouldComponentUpdate');
			sinon.spy(Outer.prototype, 'componentDidMount');

			render(<Outer />, scratch);
			doRender();
			rerender();

			expect(
				Inner.prototype.componentWillReceiveProps
			).to.have.been.calledBefore(Inner.prototype.componentWillUpdate);
			expect(
				Inner.prototype.componentWillReceiveProps
			).to.have.been.calledBefore(Inner.prototype.shouldComponentUpdate);
			expect(Inner.prototype.componentWillUpdate).to.have.been.calledBefore(
				Inner.prototype.componentDidUpdate
			);
		});
	});
});