Back to Repositories

Validating Hook Argument Handling in Preact

This test suite validates the handling of NaN arguments in Preact’s hook implementations, ensuring proper validation and warning mechanisms. It systematically tests multiple hook types including useEffect, useLayoutEffect, useCallback, useMemo, and useImperativeHandle for consistent argument validation behavior.

Test Coverage Overview

The test suite provides comprehensive coverage of hook argument validation in Preact, focusing specifically on NaN value handling.

Key areas tested include:
  • Initial mount with NaN arguments
  • Runtime updates introducing NaN values
  • Multiple hook implementations (useEffect, useLayoutEffect, useCallback, useMemo, useImperativeHandle)
  • Warning message consistency across different hooks

Implementation Analysis

The testing approach utilizes a reusable validation pattern through the validateHook helper function, which creates a test component with controllable state. Each hook type is tested using the same validation logic, ensuring consistent behavior across Preact’s hook implementations.

Technical implementation features:
  • Dynamic test generation for each hook type
  • Controlled state updates via button clicks
  • Console warning interception and verification
  • Async test execution for state updates

Technical Details

Testing infrastructure includes:
  • Jest as the test runner
  • Sinon for console warning stubbing
  • Custom setupScratch and teardown utilities
  • Preact test-utils for rerender functionality
  • TypeScript type annotations for improved code safety

Best Practices Demonstrated

The test suite exemplifies several testing best practices in React/Preact ecosystem.

Notable practices include:
  • Isolation of test cases using beforeEach/afterEach hooks
  • Proper cleanup of DOM elements and stubs
  • Reusable test patterns through helper functions
  • Comprehensive error message validation
  • Type-safe testing with TypeScript annotations

preactjs/preact

debug/test/browser/validateHookArgs.test.js

            
import { createElement, render, createRef } from 'preact';
import {
	useState,
	useEffect,
	useLayoutEffect,
	useCallback,
	useMemo,
	useImperativeHandle
} from 'preact/hooks';
import { setupRerender } from 'preact/test-utils';
import { setupScratch, teardown } from '../../../test/_util/helpers';
import 'preact/debug';

/** @jsx createElement */

describe('Hook argument validation', () => {
	/**
	 * @param {string} name
	 * @param {(arg: number) => void} hook
	 */
	function validateHook(name, hook) {
		const TestComponent = ({ initialValue }) => {
			const [value, setValue] = useState(initialValue);
			hook(value);

			return (
				<button type="button" onClick={() => setValue(NaN)}>
					Set to NaN
				</button>
			);
		};

		it(`should error if ${name} is mounted with NaN as an argument`, async () => {
			render(<TestComponent initialValue={NaN} />, scratch);
			expect(console.warn).to.be.calledOnce;
			expect(console.warn.args[0]).to.match(
				/Hooks should not be called with NaN in the dependency array/
			);
		});

		it(`should error if ${name} is updated with NaN as an argument`, async () => {
			render(<TestComponent initialValue={0} />, scratch);

			scratch.querySelector('button').click();
			rerender();

			expect(console.warn).to.be.calledOnce;
			expect(console.warn.args[0]).to.match(
				/Hooks should not be called with NaN in the dependency array/
			);
		});
	}

	/** @type {HTMLElement} */
	let scratch;
	/** @type {() => void} */
	let rerender;
	let warnings = [];

	beforeEach(() => {
		scratch = setupScratch();
		rerender = setupRerender();
		warnings = [];
		sinon.stub(console, 'warn').callsFake(w => warnings.push(w));
	});

	afterEach(() => {
		teardown(scratch);
		console.warn.restore();
	});

	validateHook('useEffect', arg => useEffect(() => {}, [arg]));
	validateHook('useLayoutEffect', arg => useLayoutEffect(() => {}, [arg]));
	validateHook('useCallback', arg => useCallback(() => {}, [arg]));
	validateHook('useMemo', arg => useMemo(() => {}, [arg]));

	const ref = createRef();
	validateHook('useImperativeHandle', arg => {
		useImperativeHandle(ref, () => undefined, [arg]);
	});
});