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
Implementation Analysis
Technical Details
Best Practices Demonstrated
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]);
});
});