Back to Repositories

Validating React 18 Hooks Implementation in PreactJS

This test suite validates React 18 hooks implementation in Preact’s compatibility layer, focusing on newer hooks like useDeferredValue, useInsertionEffect, and useTransition. The tests ensure proper hook behavior and context management within the Preact ecosystem.

Test Coverage Overview

The test suite provides comprehensive coverage of React 18 hooks implementation in Preact, focusing on three main areas:

  • useDeferredValue hook functionality and value preservation
  • useInsertionEffect execution and timing
  • useTransition state management and pending status
  • Complex context consumer scenarios and state management

Implementation Analysis

The testing approach employs Jest’s describe/it pattern with Preact’s test utilities for component rendering and state updates. Tests utilize act() for managing side effects and setupRerender for handling component updates. The implementation validates both simple hook usage and complex scenarios involving context and state management.

Technical Details

  • Testing Framework: Jest
  • Utilities: Preact test-utils (act, setupRerender)
  • Helper Functions: setupScratch, teardown
  • Assertion Library: Sinon for spies
  • DOM Manipulation: Virtual scratch element for rendering

Best Practices Demonstrated

The test suite exemplifies several testing best practices including proper test isolation, cleanup between tests, and comprehensive edge case coverage. Each test focuses on a specific aspect of hook functionality, using clear assertions and proper setup/teardown patterns. The use of spy functions and state management demonstrates thorough validation of both synchronous and asynchronous behavior.

preactjs/preact

compat/test/browser/hooks.test.js

            
import { createContext } from 'preact';
import React, {
	createElement,
	useDeferredValue,
	useInsertionEffect,
	useTransition,
	render,
	useState,
	useContext,
	useEffect
} from 'preact/compat';
import { setupRerender, act } from 'preact/test-utils';
import { setupScratch, teardown } from '../../../test/_util/helpers';

describe('React-18-hooks', () => {
	/** @type {HTMLDivElement} */
	let scratch;

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

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

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

	describe('useDeferredValue', () => {
		it('returns the value', () => {
			const App = props => {
				const val = useDeferredValue(props.text);
				return <p>{val}</p>;
			};

			render(<App text="hello world" />, scratch);

			expect(scratch.innerHTML).to.equal('<p>hello world</p>');
		});
	});

	describe('useInsertionEffect', () => {
		it('runs the effect', () => {
			const spy = sinon.spy();
			const App = () => {
				useInsertionEffect(spy, []);
				return <p>hello world</p>;
			};

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

			expect(scratch.innerHTML).to.equal('<p>hello world</p>');
			expect(spy).to.be.calledOnce;
		});
	});

	describe('useTransition', () => {
		it('runs transitions', () => {
			const spy = sinon.spy();

			/** @type {(v) => void} */
			let go;
			const App = () => {
				const [isPending, start] = useTransition();
				go = start;
				return <p>Pending: {isPending ? 'yes' : 'no'}</p>;
			};

			render(<App />, scratch);
			expect(scratch.innerHTML).to.equal('<p>Pending: no</p>');

			go(spy);
			rerender();
			expect(spy).to.be.calledOnce;
			expect(scratch.innerHTML).to.equal('<p>Pending: no</p>');
		});
	});

	it('should release ._force on context-consumers', () => {
		let sequence, setSubmitting;
		const Ctx = createContext({
			isSubmitting: false,
			setIsSubmitting: () => {}
		});
		const FormWrapper = props => {
			const [isSubmitting, setIsSubmitting] = useState(false);
			setSubmitting = setIsSubmitting;
			return (
				<Ctx.Provider value={{ isSubmitting, setIsSubmitting }}>
					{props.children}
				</Ctx.Provider>
			);
		};

		const Form = () => {
			const { isSubmitting, setIsSubmitting } = useContext(Ctx);
			const [shouldSubmit, setShouldSubmit] = useState(false);

			sequence = () => {
				setShouldSubmit(true);
			};

			const submit = () => {
				setIsSubmitting(true);
				setShouldSubmit(false);
			};

			useEffect(() => {
				if (shouldSubmit) {
					submit();
				}
			}, [shouldSubmit]);

			return (
				<p>
					isSubmitting: {'' + isSubmitting} | shouldSubmit: {'' + shouldSubmit}
				</p>
			);
		};

		const App = () => {
			return (
				<FormWrapper>
					<Form />
				</FormWrapper>
			);
		};

		render(<App />, scratch);

		act(() => {
			sequence();
		});
		expect(scratch.innerHTML).to.equal(
			'<p>isSubmitting: true | shouldSubmit: false</p>'
		);

		act(() => {
			setSubmitting(false);
		});
		expect(scratch.innerHTML).to.equal(
			'<p>isSubmitting: false | shouldSubmit: false</p>'
		);

		act(() => {
			sequence();
		});
		expect(scratch.innerHTML).to.equal(
			'<p>isSubmitting: true | shouldSubmit: false</p>'
		);
	});
});