Back to Repositories

Testing Debug Options Integration in Preact Components

This test suite validates debug options functionality in Preact, focusing on lifecycle hooks, component mounting, and error handling. It ensures proper integration between Preact’s debug features and component lifecycle events.

Test Coverage Overview

The test suite provides comprehensive coverage of Preact’s debug options across different component lifecycle stages.

Key areas tested include:
  • Component mounting and unmounting events
  • State updates and re-rendering
  • Hook component integration
  • Error boundary functionality
  • Debug option callback execution

Implementation Analysis

The testing approach utilizes Jest’s describe/it pattern with systematic verification of debug option spies. It implements both class-based and hook-based components to ensure complete coverage of Preact’s component patterns.

Notable patterns include:
  • Spy function verification for lifecycle events
  • State management testing
  • Error boundary implementation
  • Clock manipulation for async operations

Technical Details

Testing infrastructure includes:
  • Jest test framework
  • Sinon for spy functions and time manipulation
  • Preact test-utils for rerender functionality
  • Custom scratch element setup and teardown
  • Debug option spy utilities

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices through organized and systematic verification of component behavior.

Key practices include:
  • Proper test setup and teardown
  • Isolated component testing
  • Comprehensive spy verification
  • Error handling validation
  • Clean state management between tests

preactjs/preact

debug/test/browser/debug.options.test.js

            
import {
	vnodeSpy,
	rootSpy,
	beforeDiffSpy,
	hookSpy,
	afterDiffSpy,
	catchErrorSpy
} from '../../../test/_util/optionSpies';

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

/** @jsx createElement */

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

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

	/** @type {(count: number) => void} */
	let setCount;

	/** @type {import('sinon').SinonFakeTimers | undefined} */
	let clock;

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

		vnodeSpy.resetHistory();
		rootSpy.resetHistory();
		beforeDiffSpy.resetHistory();
		hookSpy.resetHistory();
		afterDiffSpy.resetHistory();
		catchErrorSpy.resetHistory();
	});

	afterEach(() => {
		teardown(scratch);
		if (clock) clock.restore();
	});

	class ClassApp extends Component {
		constructor() {
			super();
			this.state = { count: 0 };
			setCount = count => this.setState({ count });
		}

		render() {
			return <div>{this.state.count}</div>;
		}
	}

	it('should call old options on mount', () => {
		render(<ClassApp />, scratch);

		expect(vnodeSpy).to.have.been.called;
		expect(rootSpy).to.have.been.called;
		expect(beforeDiffSpy).to.have.been.called;
		expect(afterDiffSpy).to.have.been.called;
	});

	it('should call old options on update', () => {
		render(<ClassApp />, scratch);

		setCount(1);
		rerender();

		expect(vnodeSpy).to.have.been.called;
		expect(rootSpy).to.have.been.called;
		expect(beforeDiffSpy).to.have.been.called;
		expect(afterDiffSpy).to.have.been.called;
	});

	it('should call old options on unmount', () => {
		render(<ClassApp />, scratch);
		render(null, scratch);

		expect(vnodeSpy).to.have.been.called;
		expect(rootSpy).to.have.been.called;
		expect(beforeDiffSpy).to.have.been.called;
		expect(afterDiffSpy).to.have.been.called;
	});

	it('should call old hook options for hook components', () => {
		function HookApp() {
			const [count, realSetCount] = useState(0);
			setCount = realSetCount;
			return <div>{count}</div>;
		}

		render(<HookApp />, scratch);

		expect(hookSpy).to.have.been.called;
	});

	it('should call old options on error', () => {
		const e = new Error('test');
		class ErrorApp extends Component {
			constructor() {
				super();
				this.state = { error: true };
			}
			componentDidCatch() {
				this.setState({ error: false });
			}
			render() {
				return <Throw error={this.state.error} />;
			}
		}

		function Throw({ error }) {
			if (error) {
				throw e;
			} else {
				return <div>no error</div>;
			}
		}

		clock = sinon.useFakeTimers();

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

		expect(catchErrorSpy).to.have.been.called;

		// we expect to throw after setTimeout to trigger a window.onerror
		// this is to ensure react compat (i.e. with next.js' dev overlay)
		expect(() => clock.tick(0)).to.throw(e);
	});
});