Back to Repositories

Testing useImperativeHandle Hook Implementation in Preact

This test suite comprehensively validates the useImperativeHandle hook implementation in Preact, focusing on ref manipulation and lifecycle behavior. It verifies the hook’s ability to customize the instance value exposed to parent components through refs, while ensuring proper cleanup and dependency tracking.

Test Coverage Overview

The test suite provides extensive coverage of useImperativeHandle functionality:
  • Ref mutation and value exposure
  • Unmounting behavior and cleanup
  • Dependency array handling and re-render triggers
  • Reference object updates and state changes
  • Edge cases with null refs

Implementation Analysis

The testing approach employs Jest and Sinon for comprehensive hook validation. Tests utilize setup/teardown patterns with scratch DOM elements and implement spy functions to track callback execution. The suite demonstrates proper component lifecycle handling with mounting, updating, and unmounting scenarios.

Technical Details

Testing infrastructure includes:
  • Jest test framework
  • Sinon for spy/stub functionality
  • Preact test-utils for rerender capabilities
  • Custom setup/teardown helpers
  • DOM scratch element management

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices:
  • Isolated test cases with clear assertions
  • Proper cleanup between tests
  • Comprehensive edge case coverage
  • Effective use of spy functions for behavior verification
  • Consistent test organization and naming

preactjs/preact

hooks/test/browser/useImperativeHandle.test.js

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

/** @jsx createElement */

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

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

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

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

	it('Mutates given ref', () => {
		let ref;

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, () => ({ test: () => 'test' }), []);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test');
	});

	it('Calls ref unmounting function', () => {
		let ref;
		const unmount = sinon.spy();

		function Comp() {
			useImperativeHandle(
				r => {
					ref = r;
					return unmount;
				},
				() => ({ test: () => 'test' }),
				[]
			);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(ref).to.have.property('test');
		expect(ref.test()).to.equal('test');
		render(null, scratch);
		expect(unmount).to.be.calledOnce;
		expect(ref).to.equal(null);
	});

	it('calls createHandle after every render by default', () => {
		let ref,
			createHandleSpy = sinon.spy();

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledTwice;

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledThrice;
	});

	it('calls createHandle only on mount if an empty array is passed', () => {
		let ref,
			createHandleSpy = sinon.spy();

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy, []);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
	});

	it('Updates given ref when args change', () => {
		let ref,
			createHandleSpy = sinon.spy();

		function Comp({ a }) {
			ref = useRef({});
			useImperativeHandle(
				ref,
				() => {
					createHandleSpy();
					return { test: () => 'test' + a };
				},
				[a]
			);
			return <p>Test</p>;
		}

		render(<Comp a={0} />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test0');

		render(<Comp a={1} />, scratch);
		expect(createHandleSpy).to.have.been.calledTwice;
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test1');

		render(<Comp a={0} />, scratch);
		expect(createHandleSpy).to.have.been.calledThrice;
		expect(ref.current).to.have.property('test');
		expect(ref.current.test()).to.equal('test0');
	});

	it('Updates given ref when passed-in ref changes', () => {
		let ref1, ref2;

		/** @type {(arg: any) => void} */
		let setRef;

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

		const createHandleSpy = sinon.spy(() => ({
			test: () => 'test'
		}));

		function Comp() {
			ref1 = useRef({});
			ref2 = useRef({});

			const [ref, setRefInternal] = useState(ref1);
			setRef = setRefInternal;

			let [value, setState] = useState(0);
			updateState = () => setState((value + 1) % 2);

			useImperativeHandle(ref, createHandleSpy, []);
			return <p>Test</p>;
		}

		render(<Comp a={0} />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;

		updateState();
		rerender();
		expect(createHandleSpy).to.have.been.calledOnce;

		setRef(ref2);
		rerender();
		expect(createHandleSpy).to.have.been.calledTwice;

		updateState();
		rerender();
		expect(createHandleSpy).to.have.been.calledTwice;

		setRef(ref1);
		rerender();
		expect(createHandleSpy).to.have.been.calledThrice;
	});

	it('should not update ref when args have not changed', () => {
		let ref,
			createHandleSpy = sinon.spy(() => ({ test: () => 'test' }));

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy, [1]);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current.test()).to.equal('test');

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current.test()).to.equal('test');
	});

	it('should not throw with nullish ref', () => {
		function Comp() {
			useImperativeHandle(null, () => ({ test: () => 'test' }), [1]);
			return <p>Test</p>;
		}

		expect(() => render(<Comp />, scratch)).to.not.throw();
	});

	it('should reset ref object to null when the component get unmounted', () => {
		let ref,
			createHandleSpy = sinon.spy(() => ({ test: () => 'test' }));

		function Comp() {
			ref = useRef({});
			useImperativeHandle(ref, createHandleSpy, [1]);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current).to.not.equal(null);

		render(<div />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref.current).to.equal(null);
	});

	it('should reset ref callback to null when the component get unmounted', () => {
		const ref = sinon.spy();
		const handle = { test: () => 'test' };
		const createHandleSpy = sinon.spy(() => handle);

		function Comp() {
			useImperativeHandle(ref, createHandleSpy, [1]);
			return <p>Test</p>;
		}

		render(<Comp />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref).to.have.been.calledWith(handle);

		ref.resetHistory();

		render(<div />, scratch);
		expect(createHandleSpy).to.have.been.calledOnce;
		expect(ref).to.have.been.calledWith(null);
	});
});