Back to Repositories

Testing React Component Lifecycle Implementation in PreactJS

This test suite evaluates the compatibility layer and component lifecycle methods in Preact’s React compatibility module. It focuses on verifying React component behavior, lifecycle methods, and UNSAFE_* method implementations within the Preact ecosystem.

Test Coverage Overview

The test suite provides comprehensive coverage of React component functionality in Preact, including:

  • Basic component rendering and props handling
  • Component lifecycle method implementations
  • UNSAFE_* lifecycle methods compatibility
  • Suspense integration with lifecycle methods

Implementation Analysis

The testing approach uses Jest’s describe/it pattern with setup and teardown hooks for consistent test environments. Tests validate component behavior through direct instantiation, rendering, and lifecycle method verification using sinon spies for method call tracking.

  • Component property verification
  • Props and children handling
  • Lifecycle method aliasing and execution order

Technical Details

  • Testing Framework: Jest
  • Assertion Library: Expect
  • Mocking: Sinon
  • Test Utilities: preact/test-utils
  • Setup Helpers: Custom scratch element management

Best Practices Demonstrated

The test suite exemplifies strong testing practices through isolated component testing, proper setup/teardown patterns, and comprehensive lifecycle verification. Notable practices include:

  • Consistent test environment setup/teardown
  • Isolation of component lifecycle behaviors
  • Thorough edge case coverage
  • Clear test case organization

preactjs/preact

compat/test/browser/component.test.js

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

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

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

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

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

	it('should have "isReactComponent" property', () => {
		let Comp = new React.Component();
		expect(Comp.isReactComponent).to.deep.equal({});
	});

	it('should be sane', () => {
		let props;

		class Demo extends React.Component {
			render() {
				props = this.props;
				return <div id="demo">{this.props.children}</div>;
			}
		}

		React.render(
			<Demo a="b" c="d">
				inner
			</Demo>,
			scratch
		);

		expect(props).to.exist.and.deep.equal({
			a: 'b',
			c: 'd',
			children: 'inner'
		});

		expect(scratch.innerHTML).to.equal('<div id="demo">inner</div>');
	});

	it('should single out children before componentWillReceiveProps', () => {
		let props;

		class Child extends React.Component {
			componentWillReceiveProps(newProps) {
				props = newProps;
			}
			render() {
				return this.props.children;
			}
		}

		class Parent extends React.Component {
			render() {
				return <Child>second</Child>;
			}
		}

		let a = React.render(<Parent />, scratch);
		a.forceUpdate();
		rerender();

		expect(props).to.exist.and.deep.equal({
			children: 'second'
		});
	});

	describe('UNSAFE_* lifecycle methods', () => {
		it('should support UNSAFE_componentWillMount', () => {
			let spy = sinon.spy();

			class Foo extends React.Component {
				// eslint-disable-next-line camelcase
				UNSAFE_componentWillMount() {
					spy();
				}

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

			React.render(<Foo />, scratch);

			expect(spy).to.be.calledOnce;
		});

		it('should support UNSAFE_componentWillMount #2', () => {
			let spy = sinon.spy();

			class Foo extends React.Component {
				render() {
					return <h1>foo</h1>;
				}
			}

			Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillMount', {
				value: spy
			});

			React.render(<Foo />, scratch);
			expect(spy).to.be.calledOnce;
		});

		it('should support UNSAFE_componentWillReceiveProps', () => {
			let spy = sinon.spy();

			class Foo extends React.Component {
				// eslint-disable-next-line camelcase
				UNSAFE_componentWillReceiveProps() {
					spy();
				}

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

			React.render(<Foo />, scratch);
			// Trigger an update
			React.render(<Foo />, scratch);
			expect(spy).to.be.calledOnce;
		});

		it('should support UNSAFE_componentWillReceiveProps #2', () => {
			let spy = sinon.spy();

			class Foo extends React.Component {
				render() {
					return <h1>foo</h1>;
				}
			}

			Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillReceiveProps', {
				value: spy
			});

			React.render(<Foo />, scratch);
			// Trigger an update
			React.render(<Foo />, scratch);
			expect(spy).to.be.calledOnce;
		});

		it('should support UNSAFE_componentWillUpdate', () => {
			let spy = sinon.spy();

			class Foo extends React.Component {
				// eslint-disable-next-line camelcase
				UNSAFE_componentWillUpdate() {
					spy();
				}

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

			React.render(<Foo />, scratch);
			// Trigger an update
			React.render(<Foo />, scratch);
			expect(spy).to.be.calledOnce;
		});

		it('should support UNSAFE_componentWillUpdate #2', () => {
			let spy = sinon.spy();

			class Foo extends React.Component {
				render() {
					return <h1>foo</h1>;
				}
			}

			Object.defineProperty(Foo.prototype, 'UNSAFE_componentWillUpdate', {
				value: spy
			});

			React.render(<Foo />, scratch);
			// Trigger an update
			React.render(<Foo />, scratch);
			expect(spy).to.be.calledOnce;
		});

		it('should alias UNSAFE_* method to non-prefixed variant', () => {
			let inst;
			class Foo extends React.Component {
				// eslint-disable-next-line camelcase
				UNSAFE_componentWillMount() {}
				// eslint-disable-next-line camelcase
				UNSAFE_componentWillReceiveProps() {}
				// eslint-disable-next-line camelcase
				UNSAFE_componentWillUpdate() {}
				render() {
					inst = this;
					return <div>foo</div>;
				}
			}

			React.render(<Foo />, scratch);

			expect(inst.UNSAFE_componentWillMount).to.equal(inst.componentWillMount);
			expect(inst.UNSAFE_componentWillReceiveProps).to.equal(
				inst.UNSAFE_componentWillReceiveProps
			);
			expect(inst.UNSAFE_componentWillUpdate).to.equal(
				inst.UNSAFE_componentWillUpdate
			);
		});

		it('should call UNSAFE_* methods through Suspense with wrapper component #2525', () => {
			class Page extends React.Component {
				UNSAFE_componentWillMount() {}
				render() {
					return <h1>Example</h1>;
				}
			}

			const Wrapper = () => <Page />;

			sinon.spy(Page.prototype, 'UNSAFE_componentWillMount');

			React.render(
				<React.Suspense fallback={<div>fallback</div>}>
					<Wrapper />
				</React.Suspense>,
				scratch
			);

			expect(scratch.innerHTML).to.equal('<h1>Example</h1>');
			expect(Page.prototype.UNSAFE_componentWillMount).to.have.been.called;
		});
	});
});