Back to Repositories

Testing ComponentDidCatch Error Boundaries with Hooks in Preact

This test suite focuses on error handling and lifecycle management in Preact components, specifically testing the componentDidCatch functionality with hooks. It verifies error propagation and errorInfo object behavior during component unmounting scenarios.

Test Coverage Overview

The test suite examines error handling during component unmounting when using hooks. It covers:
  • Hook cleanup error propagation
  • ComponentDidCatch error interception
  • ErrorInfo object validation
  • Component state management during errors

Implementation Analysis

The testing approach utilizes Preact’s component lifecycle and hooks architecture to validate error boundaries. It implements a pattern where useEffect cleanup throws an error, testing how errors propagate through the component tree and are caught by error boundaries.

The implementation leverages Preact’s test-utils act() function for managing component updates and state changes.

Technical Details

Testing tools and setup include:
  • Preact test-utils for component rendering
  • Custom scratch element setup/teardown helpers
  • Jest testing framework
  • Component class implementation with error boundary
  • Functional component with useEffect hook

Best Practices Demonstrated

The test demonstrates several testing best practices:
  • Proper setup and teardown of test environment
  • Isolation of test cases
  • Controlled component state management
  • Explicit error boundary testing
  • Async operation handling with act()

preactjs/preact

hooks/test/browser/componentDidCatch.test.js

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

/** @jsx createElement */

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

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

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

	it('should pass errorInfo on hook unmount error', () => {
		let info;
		let update;
		class Receiver extends Component {
			constructor(props) {
				super(props);
				this.state = { error: null, i: 0 };
				update = this.setState.bind(this);
			}
			componentDidCatch(error, errorInfo) {
				info = errorInfo;
				this.setState({ error });
			}
			render() {
				if (this.state.error) return <div />;
				if (this.state.i === 0) return <ThrowErr />;
				return null;
			}
		}

		function ThrowErr() {
			useEffect(() => {
				return () => {
					throw new Error('fail');
				};
			}, []);

			return <h1 />;
		}

		act(() => {
			render(<Receiver />, scratch);
		});

		act(() => {
			update({ i: 1 });
		});

		expect(info).to.deep.equal({});
	});
});