Back to Repositories

Validating PureComponent Optimization and Lifecycle in Preact

This test suite validates the PureComponent functionality in Preact’s React compatibility layer, focusing on component lifecycle, props handling, and rendering optimization. It ensures proper implementation of React’s PureComponent behavior within Preact’s ecosystem.

Test Coverage Overview

The test suite provides comprehensive coverage of PureComponent functionality:

  • Constructor behavior with props and context passing
  • Rendering optimization and component updates
  • State and prop change detection
  • Property validation and inheritance verification
Key edge cases include handling removed props and undefined state scenarios.

Implementation Analysis

The testing approach utilizes Jest’s describe/it blocks with sinon spies for method tracking. The implementation validates core PureComponent features through isolated test cases that verify constructor initialization, selective rendering, and proper props comparison.

Tests leverage Preact’s test-utils for rerender capabilities and custom scratch element setup.

Technical Details

  • Testing Framework: Jest
  • Assertion Library: Sinon for spies and mocks
  • Setup Utilities: setupScratch, teardown helpers
  • Component Lifecycle: beforeEach/afterEach hooks
  • DOM Manipulation: Virtual scratch element

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices with isolated component testing, proper cleanup between tests, and comprehensive edge case coverage.

  • Consistent before/after test cleanup
  • Isolated component rendering
  • Spy-based method verification
  • Explicit state and props validation

preactjs/preact

compat/test/browser/PureComponent.test.js

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

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

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

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

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

	it('should be a class', () => {
		expect(React).to.have.property('PureComponent').that.is.a('function');
	});

	it('should pass props in constructor', () => {
		let spy = sinon.spy();
		class Foo extends React.PureComponent {
			constructor(props) {
				super(props);
				spy(this.props, props);
			}
		}

		React.render(<Foo foo="bar" />, scratch);

		let expected = { foo: 'bar' };
		expect(spy).to.be.calledWithMatch(expected, expected);
	});

	it('should pass context in constructor', () => {
		let instance;
		// Not initializing state matches React behavior: https://codesandbox.io/s/rml19v8o2q
		class Foo extends React.PureComponent {
			constructor(props, context) {
				super(props, context);
				expect(this.props).to.equal(props);
				expect(this.state).to.deep.equal(undefined);
				expect(this.context).to.equal(context);

				instance = this;
			}
			render(props) {
				return <div {...props}>Hello</div>;
			}
		}

		sinon.spy(Foo.prototype, 'render');

		const PROPS = { foo: 'bar' };
		React.render(<Foo {...PROPS} />, scratch);

		expect(Foo.prototype.render)
			.to.have.been.calledOnce.and.to.have.been.calledWithMatch(PROPS, {}, {})
			.and.to.have.returned(sinon.match({ type: 'div', props: PROPS }));
		expect(instance.props).to.deep.equal(PROPS);
		expect(instance.state).to.deep.equal({});
		expect(instance.context).to.deep.equal({});

		expect(scratch.innerHTML).to.equal('<div foo="bar">Hello</div>');
	});

	it('should ignore the __source variable', () => {
		const pureSpy = sinon.spy();
		const appSpy = sinon.spy();
		/** @type {(v) => void} */
		let set;
		class Pure extends React.PureComponent {
			render() {
				pureSpy();
				return <div>Static</div>;
			}
		}

		const App = () => {
			const [, setState] = React.useState(0);
			appSpy();
			set = setState;
			return <Pure __source={{}} />;
		};

		React.render(<App />, scratch);
		expect(appSpy).to.be.calledOnce;
		expect(pureSpy).to.be.calledOnce;

		set(1);
		rerender();
		expect(appSpy).to.be.calledTwice;
		expect(pureSpy).to.be.calledOnce;
	});

	it('should only re-render when props or state change', () => {
		class C extends React.PureComponent {
			render() {
				return <div />;
			}
		}
		let spy = sinon.spy(C.prototype, 'render');

		let inst = React.render(<C />, scratch);
		expect(spy).to.have.been.calledOnce;
		spy.resetHistory();

		inst = React.render(<C />, scratch);
		expect(spy).not.to.have.been.called;

		let b = { foo: 'bar' };
		inst = React.render(<C a="a" b={b} />, scratch);
		expect(spy).to.have.been.calledOnce;
		spy.resetHistory();

		inst = React.render(<C a="a" b={b} />, scratch);
		expect(spy).not.to.have.been.called;

		inst.setState({});
		rerender();
		expect(spy).not.to.have.been.called;

		inst.setState({ a: 'a', b });
		rerender();
		expect(spy).to.have.been.calledOnce;
		spy.resetHistory();

		inst.setState({ a: 'a', b });
		rerender();
		expect(spy).not.to.have.been.called;
	});

	it('should update when props are removed', () => {
		let spy = sinon.spy();
		class App extends React.PureComponent {
			render() {
				spy();
				return <div>foo</div>;
			}
		}

		React.render(<App a="foo" />, scratch);
		React.render(<App />, scratch);
		expect(spy).to.be.calledTwice;
	});

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