testing-hooks-in-isolation
Flags a hook test that mocks more framework primitives than the hook under test uses.
Agent checkAdvises by default
Why this rule
Mock the framework's hook primitives with synchronous shims so the hook body executes without a render cycle; capture effect callbacks so tests can invoke them:
// Mocked Imports
// -------------------------
let mockEffectCallback: (() => undefined | (() => void)) | undefined;
jest.mock('preact/hooks', () => ({
useEffect: (cb: () => undefined | (() => void)) => {
mockEffectCallback = cb;
},
useCallback: <T>(cb: T) => cb,
useMemo: (factory: () => unknown) => factory(),
}));
// -------------------------
const setupEscapeKey = ({ isActive = true }: { isActive?: boolean } = {}) => {
mockEffectCallback = undefined;
const addEventListenerSpy = jest.spyOn(document, 'addEventListener');
const onEscape = jest.fn<() => void>();
useEscapeKey({ isActive, onEscape });
return { addEventListenerSpy, onEscape };
};
describe('useEscapeKey', () => {
test('adds a keydown event listener', () => {
const { addEventListenerSpy } = setupEscapeKey({ isActive: true });
mockEffectCallback!();
expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));
});
});
Only mock the hook primitives the hook under test actually uses.
Examples
The agent flags code like the incorrect example and accepts code like the correct one.
Incorrect
import { expect, describe, test, jest } from '@jest/globals';import { useEscapeKey } from './useEscapeKey';// Every hook primitive stubbed, including the three this hook never calls —// each one a shim that can drift from the framework for no coverage in return.jest.mock('react', () => ({useEffect: (callback: () => void) => callback(),useCallback: <T>(callback: T) => callback,useMemo: (factory: () => unknown) => factory(),useState: (initial: unknown) => [initial, () => undefined],useRef: (initial: unknown) => ({ current: initial }),useContext: () => ({}),}));describe('useEscapeKey', () => {test('adds a keydown event listener', () => {const addEventListenerSpy = jest.spyOn(document, 'addEventListener');useEscapeKey({ isActive: true, onEscape: jest.fn<() => void>() });expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));});});
Correct
import { expect, describe, test, jest } from '@jest/globals';import { useEscapeKey } from './useEscapeKey';// Mocked Imports// -------------------------let mockEffectCallback: (() => undefined | (() => void)) | undefined;jest.mock('react', () => ({useEffect: (callback: () => undefined | (() => void)) => {mockEffectCallback = callback;},}));// -------------------------const setupEscapeKey = ({ isActive = true }: { isActive?: boolean } = {}) => {mockEffectCallback = undefined;const addEventListenerSpy = jest.spyOn(document, 'addEventListener');const onEscape = jest.fn<() => void>();useEscapeKey({ isActive, onEscape });return { addEventListenerSpy, onEscape };};describe('useEscapeKey', () => {test('adds a keydown event listener', () => {const { addEventListenerSpy } = setupEscapeKey({ isActive: true });mockEffectCallback?.();expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function));});});
Configure
- Block
"blocking"Stops a run when a file the run changed breaks the rule. - AdviseDefault
"advisory"Reports it and hands it to the refactor agent. Never stops a run. - Off
"off"Not checked. Use it when your own linter already enforces the rule.
Add this to your lightsout.config.json, then change the value.
{"standards-checks": {"testing-hooks-in-isolation": "advisory"}}