Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/COMPT-33-test-suite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@ciscode/hooks-kit': patch
---

test(COMPT-33): full test suite for all 12 hooks

- Consolidate all hook tests under src/hooks/__tests__/
- Cover all 12 hooks: useDebounce, useLocalStorage, useSessionStorage,
useMediaQuery, useWindowSize, useClickOutside, useIntersectionObserver,
usePrevious, useToggle, useInterval, useTimeout, useIsFirstRender
- Use vitest fake timers for useDebounce, useInterval, useTimeout, useWindowSize
Comment on lines +7 to +11
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This release note says the test suite covers “all 12 hooks”, but the package currently exports another hook (useNoop) publicly. Please either include useNoop in this list/coverage statement, or adjust exports so the “12 hooks” claim is accurate.

Copilot uses AI. Check for mistakes.
- Verify all acceptance criteria per COMPT-33 definition of done
- Coverage ≥ 85% lines across all hooks
17 changes: 15 additions & 2 deletions .github/instructions/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@

- **State & Storage** (COMPT-30 ✅) — `useDebounce`, `useLocalStorage`, `useSessionStorage`
- **DOM & Events** (COMPT-31 ✅) — `useMediaQuery`, `useWindowSize`, `useClickOutside`, `useIntersectionObserver`
<<<<<<< HEAD
- **Async & Lifecycle** (COMPT-32 ✅) — `usePrevious`, `useToggle`, `useInterval`, `useTimeout`, `useIsFirstRender`
- **Test Suite** (COMPT-33 ✅) — Full coverage for all 12 hooks, all tests in `src/hooks/__tests__/`

Comment on lines 20 to 24
Copy link

Copilot AI Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs state the package provides/tests “all 12 hooks”, but the public exports include an additional hook (useNoop) via src/hooks/index.ts (and re-exported from src/index.ts). Either update the docs/count to include useNoop (and its test location), or stop exporting it if it’s not meant to be part of the public hook surface.

Copilot uses AI. Check for mistakes.
### Module Responsibilities:

Expand Down Expand Up @@ -54,7 +54,20 @@ src/
│ ├── useInterval.ts # COMPT-32 ✅
│ ├── useTimeout.ts # COMPT-32 ✅
│ ├── useIsFirstRender.ts # COMPT-32 ✅
│ └── index.ts # Hook barrel
│ ├── index.ts # Hook barrel
│ └── __tests__/ # All hook tests (COMPT-33 ✅)
│ ├── useDebounce.test.ts
│ ├── useLocalStorage.test.ts
│ ├── useSessionStorage.test.ts
│ ├── useMediaQuery.test.ts
│ ├── useWindowSize.test.ts
│ ├── useClickOutside.test.ts
│ ├── useIntersectionObserver.test.ts
│ ├── usePrevious.test.ts
│ ├── useToggle.test.ts
│ ├── useInterval.test.ts
│ ├── useTimeout.test.ts
│ └── useIsFirstRender.test.ts
├── utils/ # Framework-agnostic utils
│ ├── noop.ts
│ └── index.ts
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { act, renderHook } from '@testing-library/react';
import { useRef } from 'react';
import { describe, expect, it, vi } from 'vitest';
import { useClickOutside } from './useClickOutside';
import { useClickOutside } from '../useClickOutside';

function mountClickOutside(element: HTMLDivElement, handler: ReturnType<typeof vi.fn>) {
return renderHook(() => {
Expand All @@ -26,7 +26,7 @@

expect(handler).toHaveBeenCalledTimes(1);
unmount();
document.body.removeChild(outer);

Check warning on line 29 in src/hooks/__tests__/useClickOutside.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFIla6zafUljzkJ&open=AZ0_CdFIla6zafUljzkJ&pullRequest=10
});

it('calls handler on touchstart outside the ref element', () => {
Expand All @@ -45,8 +45,8 @@

expect(handler).toHaveBeenCalledTimes(1);
unmount();
document.body.removeChild(outer);

Check warning on line 48 in src/hooks/__tests__/useClickOutside.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFIla6zafUljzkK&open=AZ0_CdFIla6zafUljzkK&pullRequest=10
document.body.removeChild(outsideNode);

Check warning on line 49 in src/hooks/__tests__/useClickOutside.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFIla6zafUljzkL&open=AZ0_CdFIla6zafUljzkL&pullRequest=10
});

it('does NOT call handler on mousedown inside the ref element', () => {
Expand All @@ -64,7 +64,7 @@

expect(handler).not.toHaveBeenCalled();
unmount();
document.body.removeChild(outer);

Check warning on line 67 in src/hooks/__tests__/useClickOutside.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFIla6zafUljzkM&open=AZ0_CdFIla6zafUljzkM&pullRequest=10
});

it('removes event listeners on unmount', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useDebounce } from './useDebounce';
import { useDebounce } from '../useDebounce';

describe('useDebounce', () => {
afterEach(() => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { act, renderHook } from '@testing-library/react';
import { useRef } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useIntersectionObserver } from './useIntersectionObserver';
import { useIntersectionObserver } from '../useIntersectionObserver';

type IntersectionCallback = (entries: IntersectionObserverEntry[]) => void;

class MockIntersectionObserver {
static instances: MockIntersectionObserver[] = [];

Check warning on line 9 in src/hooks/__tests__/useIntersectionObserver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make this public static property readonly.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFPla6zafUljzkN&open=AZ0_CdFPla6zafUljzkN&pullRequest=10
callback: IntersectionCallback;
disconnect = vi.fn();
observe = vi.fn();
Expand Down Expand Up @@ -61,7 +61,7 @@
});

expect(result.current).toBe(fakeEntry);
document.body.removeChild(el);

Check warning on line 64 in src/hooks/__tests__/useIntersectionObserver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFPla6zafUljzkO&open=AZ0_CdFPla6zafUljzkO&pullRequest=10
});

it('calls observe on the ref element', () => {
Expand All @@ -75,7 +75,7 @@
});

expect(MockIntersectionObserver.instances[0].observe).toHaveBeenCalledWith(el);
document.body.removeChild(el);

Check warning on line 78 in src/hooks/__tests__/useIntersectionObserver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFPla6zafUljzkP&open=AZ0_CdFPla6zafUljzkP&pullRequest=10
});

it('calls disconnect on unmount', () => {
Expand All @@ -91,14 +91,14 @@
unmount();

expect(MockIntersectionObserver.instances[0].disconnect).toHaveBeenCalled();
document.body.removeChild(el);

Check warning on line 94 in src/hooks/__tests__/useIntersectionObserver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `childNode.remove()` over `parentNode.removeChild(childNode)`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFPla6zafUljzkQ&open=AZ0_CdFPla6zafUljzkQ&pullRequest=10
});

it('returns null in SSR context (typeof window === undefined)', () => {
vi.stubGlobal('window', undefined);

const getDefault = () => {

Check failure on line 100 in src/hooks/__tests__/useIntersectionObserver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to not always return the same value.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFPla6zafUljzkR&open=AZ0_CdFPla6zafUljzkR&pullRequest=10
if (typeof window === 'undefined') return null;

Check warning on line 101 in src/hooks/__tests__/useIntersectionObserver.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFPla6zafUljzkS&open=AZ0_CdFPla6zafUljzkS&pullRequest=10
return null;
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
import { readStorageValue } from './storage';
import { useLocalStorage } from './useLocalStorage';
import { readStorageValue } from '../storage';
import { useLocalStorage } from '../useLocalStorage';

describe('useLocalStorage', () => {
afterEach(() => {
if (typeof window !== 'undefined') {

Check warning on line 8 in src/hooks/__tests__/useLocalStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdE6la6zafUljzkC&open=AZ0_CdE6la6zafUljzkC&pullRequest=10
window.localStorage.clear();

Check warning on line 9 in src/hooks/__tests__/useLocalStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdE6la6zafUljzkD&open=AZ0_CdE6la6zafUljzkD&pullRequest=10
}
vi.unstubAllGlobals();
});

it('reads existing JSON value from localStorage', () => {
window.localStorage.setItem('user', JSON.stringify({ name: 'Ana' }));

Check warning on line 15 in src/hooks/__tests__/useLocalStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdE6la6zafUljzkE&open=AZ0_CdE6la6zafUljzkE&pullRequest=10

const { result } = renderHook(() => useLocalStorage('user', { name: 'Default' }));

Expand All @@ -27,12 +27,12 @@
result.current[1](5);
});

expect(window.localStorage.getItem('count')).toBe('5');

Check warning on line 30 in src/hooks/__tests__/useLocalStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdE6la6zafUljzkF&open=AZ0_CdE6la6zafUljzkF&pullRequest=10
expect(result.current[0]).toBe(5);
});

it('returns initial value on JSON parse error', () => {
window.localStorage.setItem('bad-json', '{ invalid json');

Check warning on line 35 in src/hooks/__tests__/useLocalStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdE6la6zafUljzkG&open=AZ0_CdE6la6zafUljzkG&pullRequest=10

const { result } = renderHook(() => useLocalStorage('bad-json', 42));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { useMediaQuery } from './useMediaQuery';
import { useMediaQuery } from '../useMediaQuery';

type ChangeHandler = () => void;

Expand Down Expand Up @@ -74,8 +74,8 @@
vi.stubGlobal('window', undefined);

const getDefault = () => {
if (typeof window === 'undefined') return false;

Check warning on line 77 in src/hooks/__tests__/useMediaQuery.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFCla6zafUljzkH&open=AZ0_CdFCla6zafUljzkH&pullRequest=10
return window.matchMedia('(min-width: 768px)').matches;

Check warning on line 78 in src/hooks/__tests__/useMediaQuery.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdFCla6zafUljzkI&open=AZ0_CdFCla6zafUljzkI&pullRequest=10
};

expect(getDefault()).toBe(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
import { readStorageValue } from './storage';
import { useSessionStorage } from './useSessionStorage';
import { readStorageValue } from '../storage';
import { useSessionStorage } from '../useSessionStorage';

describe('useSessionStorage', () => {
afterEach(() => {
if (typeof window !== 'undefined') {

Check warning on line 8 in src/hooks/__tests__/useSessionStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis.window` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdEzla6zafUljzj9&open=AZ0_CdEzla6zafUljzj9&pullRequest=10
window.sessionStorage.clear();

Check warning on line 9 in src/hooks/__tests__/useSessionStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdEzla6zafUljzj-&open=AZ0_CdEzla6zafUljzj-&pullRequest=10
}
vi.unstubAllGlobals();
});

it('reads existing JSON value from sessionStorage', () => {
window.sessionStorage.setItem('prefs', JSON.stringify({ theme: 'dark' }));

Check warning on line 15 in src/hooks/__tests__/useSessionStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdEzla6zafUljzj_&open=AZ0_CdEzla6zafUljzj_&pullRequest=10

const { result } = renderHook(() => useSessionStorage('prefs', { theme: 'light' }));

Expand All @@ -27,12 +27,12 @@
result.current[1](true);
});

expect(window.sessionStorage.getItem('enabled')).toBe('true');

Check warning on line 30 in src/hooks/__tests__/useSessionStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdEzla6zafUljzkA&open=AZ0_CdEzla6zafUljzkA&pullRequest=10
expect(result.current[0]).toBe(true);
});

it('returns initial value on JSON parse error', () => {
window.sessionStorage.setItem('bad-json', '{ invalid json');

Check warning on line 35 in src/hooks/__tests__/useSessionStorage.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdEzla6zafUljzkB&open=AZ0_CdEzla6zafUljzkB&pullRequest=10

const { result } = renderHook(() => useSessionStorage('bad-json', { retry: 3 }));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { act, renderHook } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { getWindowSize, useWindowSize } from './useWindowSize';
import { getWindowSize, useWindowSize } from '../useWindowSize';

function setViewport(width: number, height: number): void {
Object.defineProperty(window, 'innerWidth', {

Check warning on line 6 in src/hooks/__tests__/useWindowSize.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdCQla6zafUljzj3&open=AZ0_CdCQla6zafUljzj3&pullRequest=10
writable: true,
configurable: true,
value: width,
});

Object.defineProperty(window, 'innerHeight', {

Check warning on line 12 in src/hooks/__tests__/useWindowSize.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdCQla6zafUljzj4&open=AZ0_CdCQla6zafUljzj4&pullRequest=10
writable: true,
configurable: true,
value: height,
Expand Down Expand Up @@ -40,7 +40,7 @@
setViewport(1280, 800);

act(() => {
window.dispatchEvent(new Event('resize'));

Check warning on line 43 in src/hooks/__tests__/useWindowSize.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdCQla6zafUljzj5&open=AZ0_CdCQla6zafUljzj5&pullRequest=10
vi.advanceTimersByTime(50);
});

Expand All @@ -61,10 +61,10 @@
const { result } = renderHook(() => useWindowSize());

act(() => {
window.dispatchEvent(new Event('resize'));

Check warning on line 64 in src/hooks/__tests__/useWindowSize.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdCQla6zafUljzj6&open=AZ0_CdCQla6zafUljzj6&pullRequest=10
vi.advanceTimersByTime(30);
setViewport(1920, 1080);
window.dispatchEvent(new Event('resize'));

Check warning on line 67 in src/hooks/__tests__/useWindowSize.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdCQla6zafUljzj7&open=AZ0_CdCQla6zafUljzj7&pullRequest=10
vi.advanceTimersByTime(30);
});

Expand All @@ -78,7 +78,7 @@
});

it('removes resize listener on unmount', () => {
const removeSpy = vi.spyOn(window, 'removeEventListener');

Check warning on line 81 in src/hooks/__tests__/useWindowSize.test.ts

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `globalThis` over `window`.

See more on https://sonarcloud.io/project/issues?id=CISCODE-MA_HooksKit&issues=AZ0_CdCQla6zafUljzj8&open=AZ0_CdCQla6zafUljzj8&pullRequest=10
const { unmount } = renderHook(() => useWindowSize());

unmount();
Expand Down
Loading