Back to Repositories

Testing useId Hook Implementation in Preact Components

This test suite examines the useId hook functionality in Preact, focusing on unique ID generation and consistency across component renders. The tests verify ID uniqueness, persistence across updates, and proper behavior in various component hierarchies and rendering scenarios.

Test Coverage Overview

The test suite provides comprehensive coverage of the useId hook implementation:

  • ID consistency after component updates
  • Unique ID generation based on DOM depth
  • Sibling component ID uniqueness
  • Proper handling of dynamic element addition
  • Server-side rendering (SSR) compatibility
  • Fragment handling and hydration scenarios

Implementation Analysis

The testing approach employs Jest and Preact’s test utilities to validate useId behavior. Tests utilize component hierarchies with varying complexity, conditional rendering, and state updates to ensure robust ID generation. The implementation verifies both client-side rendering and server-side rendering consistency.

Technical Details

Testing tools and setup:

  • Jest as the testing framework
  • Preact test-utils for rerender functionality
  • preact-render-to-string for SSR testing
  • Custom scratch element setup for DOM manipulation
  • Hydration testing utilities

Best Practices Demonstrated

The test suite exemplifies several testing best practices:

  • Proper test isolation with beforeEach/afterEach hooks
  • Comprehensive edge case coverage
  • Clear test case organization
  • Consistent assertion patterns
  • thorough SSR and hydration validation

preactjs/preact

hooks/test/browser/useId.test.js

            
import { createElement, Fragment, hydrate, render } from 'preact';
import { useId, useReducer, useState } from 'preact/hooks';
import { setupRerender } from 'preact/test-utils';
import { render as rts } from 'preact-render-to-string';
import { setupScratch, teardown } from '../../../test/_util/helpers';

/** @jsx createElement */

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

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

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

	it('keeps the id consistent after an update', () => {
		function Comp() {
			const id = useId();
			return <div id={id} />;
		}

		render(<Comp />, scratch);
		const id = scratch.firstChild.getAttribute('id');
		expect(scratch.firstChild.getAttribute('id')).to.equal(id);

		render(<Comp />, scratch);
		expect(scratch.firstChild.getAttribute('id')).to.equal(id);
	});

	it('ids are unique according to dom-depth', () => {
		function Child() {
			const id = useId();
			const spanId = useId();
			return (
				<div id={id}>
					<span id={spanId}>h</span>
				</div>
			);
		}

		function Comp() {
			const id = useId();
			return (
				<div id={id}>
					<Child />
				</div>
			);
		}

		render(<Comp />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<div id="P0-0"><div id="P0-1"><span id="P0-2">h</span></div></div>'
		);

		render(<Comp />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<div id="P0-0"><div id="P0-1"><span id="P0-2">h</span></div></div>'
		);
	});

	it('ids are unique across siblings', () => {
		function Child() {
			const id = useId();
			return <span id={id}>h</span>;
		}

		function Comp() {
			const id = useId();
			return (
				<div id={id}>
					<Child />
					<Child />
					<Child />
				</div>
			);
		}

		render(<Comp />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<div id="P0-0"><span id="P0-1">h</span><span id="P0-2">h</span><span id="P0-3">h</span></div>'
		);

		render(<Comp />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<div id="P0-0"><span id="P0-1">h</span><span id="P0-2">h</span><span id="P0-3">h</span></div>'
		);
	});

	it('correctly handles new elements', () => {
		let set;
		function Child() {
			const id = useId();
			return <span id={id}>h</span>;
		}

		function Stateful() {
			const [state, setState] = useState(false);
			set = setState;
			return (
				<div>
					<Child />
					{state && <Child />}
				</div>
			);
		}

		function Comp() {
			const id = useId();
			return (
				<div id={id}>
					<Stateful />
				</div>
			);
		}

		render(<Comp />, scratch);
		expect(scratch.innerHTML).to.equal(
			'<div id="P0-0"><div><span id="P0-1">h</span></div></div>'
		);

		set(true);
		rerender();
		expect(scratch.innerHTML).to.equal(
			'<div id="P0-0"><div><span id="P0-1">h</span><span id="P0-2">h</span></div></div>'
		);
	});

	it('matches with rts', () => {
		const ChildFragmentReturn = ({ children }) => {
			return <Fragment>{children}</Fragment>;
		};

		const ChildReturn = ({ children }) => {
			return children;
		};

		const SomeMessage = ({ msg }) => {
			const id = useId();
			return (
				<p>
					{msg} {id}
				</p>
			);
		};

		const Stateful = () => {
			const [count, add] = useReducer(c => c + 1, 0);
			const id = useId();
			return (
				<div>
					id: {id}, count: {count}
					<button onClick={add}>+1</button>
				</div>
			);
		};

		const Component = ({ showStateful = false }) => {
			const rootId = useId();
			const paragraphId = useId();

			return (
				<main>
					ID: {rootId}
					<p>Hello world id: {paragraphId}</p>
					{showStateful ? (
						<Stateful />
					) : (
						<ChildReturn>
							<SomeMessage msg="child-return" />
							<ChildReturn>
								<SomeMessage msg="child-return" />
								<ChildReturn>
									<SomeMessage msg="child-return" />
								</ChildReturn>
							</ChildReturn>
						</ChildReturn>
					)}
					<ChildFragmentReturn>
						<SomeMessage msg="child-fragment-return" />
						<SomeMessage msg="child-fragment-return-2" />
						<SomeMessage msg="child-fragment-return-3" />
						<SomeMessage msg="child-fragment-return-4" />
						<ChildReturn>
							<SomeMessage msg="child-return" />
							<ChildFragmentReturn>
								<SomeMessage msg="child-fragment-return" />
							</ChildFragmentReturn>
						</ChildReturn>
					</ChildFragmentReturn>
				</main>
			);
		};

		const rtsOutput = rts(<Component />);
		render(<Component />, scratch);
		expect(rtsOutput === scratch.innerHTML).to.equal(true);
	});

	it('matches with rts after hydration', () => {
		const ChildFragmentReturn = ({ children }) => {
			return <Fragment>{children}</Fragment>;
		};

		const ChildReturn = ({ children }) => {
			return children;
		};

		const SomeMessage = ({ msg }) => {
			const id = useId();
			return (
				<p>
					{msg} {id}
				</p>
			);
		};

		const Stateful = () => {
			const [count, add] = useReducer(c => c + 1, 0);
			const id = useId();
			return (
				<div>
					id: {id}, count: {count}
					<button onClick={add}>+1</button>
				</div>
			);
		};

		const Component = ({ showStateful = false }) => {
			const rootId = useId();
			const paragraphId = useId();

			return (
				<main>
					ID: {rootId}
					<p>Hello world id: {paragraphId}</p>
					{showStateful ? (
						<Stateful />
					) : (
						<ChildReturn>
							<SomeMessage msg="child-return" />
							<ChildReturn>
								<SomeMessage msg="child-return" />
								<ChildReturn>
									<SomeMessage msg="child-return" />
								</ChildReturn>
							</ChildReturn>
						</ChildReturn>
					)}
					<ChildFragmentReturn>
						<SomeMessage msg="child-fragment-return" />
						<SomeMessage msg="child-fragment-return-2" />
						<SomeMessage msg="child-fragment-return-3" />
						<SomeMessage msg="child-fragment-return-4" />
						<ChildReturn>
							<SomeMessage msg="child-return" />
							<ChildFragmentReturn>
								<SomeMessage msg="child-fragment-return" />
							</ChildFragmentReturn>
						</ChildReturn>
					</ChildFragmentReturn>
				</main>
			);
		};

		const rtsOutput = rts(<Component />);

		scratch.innerHTML = rtsOutput;
		hydrate(<Component />, scratch);
		expect(rtsOutput).to.equal(scratch.innerHTML);
	});

	it('should be unique across Fragments', () => {
		const ids = [];
		function Foo() {
			const id = useId();
			ids.push(id);
			return <p>{id}</p>;
		}

		function App() {
			return (
				<div>
					<Foo />
					<Fragment>
						<Foo />
					</Fragment>
				</div>
			);
		}

		render(<App />, scratch);

		expect(ids[0]).not.to.equal(ids[1]);
	});

	it('should match implicite Fragments with RTS', () => {
		function Foo() {
			const id = useId();
			return <p>{id}</p>;
		}

		function Bar(props) {
			return props.children;
		}

		function App() {
			return (
				<Bar>
					<Foo />
					<Fragment>
						<Foo />
					</Fragment>
				</Bar>
			);
		}

		const rtsOutput = rts(<App />);

		scratch.innerHTML = rtsOutput;
		hydrate(<App />, scratch);
		expect(rtsOutput).to.equal(scratch.innerHTML);
	});

	it('should skip component top level Fragment child', () => {
		const Wrapper = ({ children }) => {
			return <Fragment>{children}</Fragment>;
		};

		const ids = [];
		function Foo() {
			const id = useId();
			ids.push(id);
			return <p>{id}</p>;
		}

		function App() {
			const id = useId();
			ids.push(id);
			return (
				<div>
					<p>{id}</p>
					<Wrapper>
						<Foo />
					</Wrapper>
				</div>
			);
		}

		render(<App />, scratch);
		expect(ids[0]).not.to.equal(ids[1]);
	});

	it('should skip over HTML', () => {
		const ids = [];

		function Foo() {
			const id = useId();
			ids.push(id);
			return <p>{id}</p>;
		}

		function App() {
			return (
				<div>
					<span>
						<Foo />
					</span>
					<span>
						<Foo />
					</span>
				</div>
			);
		}

		render(<App />, scratch);
		expect(ids[0]).not.to.equal(ids[1]);
	});

	it('should reset for each renderToString roots', () => {
		const ids = [];

		function Foo() {
			const id = useId();
			ids.push(id);
			return <p>{id}</p>;
		}

		function App() {
			return (
				<div>
					<span>
						<Foo />
					</span>
					<span>
						<Foo />
					</span>
				</div>
			);
		}

		const res1 = rts(<App />);
		const res2 = rts(<App />);
		expect(res1).to.equal(res2);
	});

	it('should work with conditional components', () => {
		function Foo() {
			const id = useId();
			return <p>{id}</p>;
		}
		function Bar() {
			const id = useId();
			return <p>{id}</p>;
		}

		let update;
		function App() {
			const [v, setV] = useState(false);
			update = setV;
			return <div>{!v ? <Foo /> : <Bar />}</div>;
		}

		render(<App />, scratch);
		const first = scratch.innerHTML;

		update(v => !v);
		rerender();
		expect(first).not.to.equal(scratch.innerHTML);
	});

	it('should return a unique id across invocations of render', () => {
		const Id = () => {
			const id = useId();
			return <div>My id is {id}</div>;
		};

		const App = props => {
			return (
				<div>
					<Id />
					{props.secondId ? <Id /> : null}
				</div>
			);
		};

		render(createElement(App, { secondId: false }), scratch);
		expect(scratch.innerHTML).to.equal('<div><div>My id is P0-0</div></div>');
		render(createElement(App, { secondId: true }), scratch);
		expect(scratch.innerHTML).to.equal(
			'<div><div>My id is P0-0</div><div>My id is P0-1</div></div>'
		);
	});

	it('should not crash for rendering null after a non-null render', () => {
		const Id = () => {
			const id = useId();
			return <div>My id is {id}</div>;
		};

		const App = props => {
			return (
				<div>
					<Id />
					{props.secondId ? <Id /> : null}
				</div>
			);
		};

		render(createElement(App, { secondId: false }), scratch);
		expect(scratch.innerHTML).to.equal('<div><div>My id is P0-0</div></div>');
		render(null, scratch);
		expect(scratch.innerHTML).to.equal('');
	});
});