Skip to content

test: add 50 tests for 6 previously untested hooks#1625

Open
ChuxiJ wants to merge 2 commits intomainfrom
feat/hooks-test-coverage-sprint
Open

test: add 50 tests for 6 previously untested hooks#1625
ChuxiJ wants to merge 2 commits intomainfrom
feat/hooks-test-coverage-sprint

Conversation

@ChuxiJ
Copy link
Copy Markdown

@ChuxiJ ChuxiJ commented Apr 8, 2026

Summary

  • Adds test coverage for 6 hooks that had zero tests: useToast (16), useMetaKeyDown (5), useAnimatedPresence (7), usePromptAutocomplete (13), useWaveform (4), useReducedMotion (4)
  • All 50 new tests pass, zero TypeScript errors

Test plan

  • npm test — all 7070 tests pass
  • npx tsc --noEmit — 0 errors
  • No existing test regressions

Closes #1622

https://claude.ai/code/session_01AAbmUU34sN8t5XRPsvuyzh

Adds comprehensive test coverage for hooks that had zero tests:

- useToast: 16 tests (show/dismiss, auto-dismiss, pause/resume, helpers)
- useMetaKeyDown: 5 tests (keydown/keyup, blur reset)
- useAnimatedPresence: 7 tests (mount/unmount, exit delay, reduced motion)
- usePromptAutocomplete: 13 tests (token extraction, suggestions, accept)
- useWaveform: 4 tests (null key, async loading, peak computation)
- useReducedMotion: 4 tests (media query sync, manual override)

All 7070 tests pass. Zero TypeScript errors.

https://claude.ai/code/session_01AAbmUU34sN8t5XRPsvuyzh
Copilot AI review requested due to automatic review settings April 8, 2026 20:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Vitest coverage for six previously untested React hooks, addressing #1622 by introducing a new test suite for toast notifications, keyboard/meta tracking, animated presence, prompt autocomplete, waveform loading, and reduced motion syncing.

Changes:

  • Added 50 new Vitest tests across 6 new hook test files.
  • Added mocks/stubs for audio engine, waveform peak computation, and media-query behavior to support hook testing.
  • Validated hook behaviors including timers, keyboard events, and store/media-query synchronization.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/hooks/tests/useToast.test.ts Adds store/timer behavior tests for toast creation, dismissal, pause/resume, and helpers.
src/hooks/tests/useMetaKeyDown.test.ts Adds keyboard event tests for Meta key down/up tracking and blur reset.
src/hooks/tests/useAnimatedPresence.test.ts Adds tests for mount/unmount timing, exit delay, cancellation, and reduced-motion behavior.
src/hooks/tests/usePromptAutocomplete.test.ts Adds tests for token extraction and basic open/close/accept/dismiss behavior.
src/hooks/tests/useWaveform.test.ts Adds tests for null key behavior, async waveform loading, and default numPeaks.
src/hooks/tests/useReducedMotion.test.ts Adds tests for matchMedia listener wiring and store updates respecting override state.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/hooks/__tests__/useWaveform.test.ts Outdated
Comment on lines +31 to +37
it('loads peaks for valid audioKey', async () => {
const { result } = renderHook(() => useWaveform('audio-key-1', 100));
// Wait for async loading
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
});
expect(result.current).toHaveLength(100);
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

These tests wait for the async effect by sleeping setTimeout(..., 10), which is timing-dependent and can be flaky on slow/loaded CI. Prefer waitFor (e.g., wait until result.current becomes non-null/has expected length) or await the mocked promises directly so the test only proceeds when the state update has happened.

Copilot uses AI. Check for mistakes.
Comment on lines +40 to +47
it('resets to null when audioKey changes to null', async () => {
const { result, rerender } = renderHook(
({ key }) => useWaveform(key),
{ initialProps: { key: 'audio-1' as string | null } },
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

Same as above: relying on an arbitrary 10ms timeout to let effects run makes this test timing-dependent. Use waitFor (or another deterministic mechanism) to wait for the hook state to update before asserting, and to ensure the rerender-to-null path is actually processed after the effect runs.

Copilot uses AI. Check for mistakes.
Comment thread src/hooks/__tests__/useWaveform.test.ts Outdated
Comment on lines +54 to +60
it('uses default numPeaks of 100', async () => {
const { computeWaveformPeaks } = await import('../../utils/waveformPeaks');
const { result } = renderHook(() => useWaveform('audio-1'));
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
});
expect(vi.mocked(computeWaveformPeaks)).toHaveBeenCalledWith(
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This test also uses a fixed setTimeout(..., 10) to wait for async work, which can be flaky. Prefer waitFor(() => expect(computeWaveformPeaks).toHaveBeenCalled()) (or waiting for result.current to be non-null) so the assertion is synchronized to the actual completion of the effect.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +17
beforeEach(() => {
vi.useFakeTimers();
// Ensure prefers-reduced-motion is not set
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
})),
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

window.matchMedia is overwritten in beforeEach, but never restored. This can leak into other test files (and into store initializers that read matchMedia at import time), making the suite order-dependent. Save the original matchMedia and restore it in afterEach (or use vi.stubGlobal + vi.unstubAllGlobals() / vi.restoreAllMocks()).

Copilot uses AI. Check for mistakes.
Comment on lines +55 to +57
if (result.current.suggestions.length > 0) {
expect(result.current.isOpen).toBe(true);
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

This test conditionally asserts isOpen only when suggestions.length > 0, which means it can silently pass even if the hook incorrectly returns no suggestions for a known token (e.g. "pop"). Since getPromptSuggestions('pop') is expected to return at least one result, assert suggestions.length > 0 and isOpen === true unconditionally to make the test meaningful.

Suggested change
if (result.current.suggestions.length > 0) {
expect(result.current.isOpen).toBe(true);
}
expect(result.current.suggestions.length).toBeGreaterThan(0);
expect(result.current.isOpen).toBe(true);

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +22
beforeEach(() => {
addListenerSpy = vi.fn();
removeListenerSpy = vi.fn();
useUIStore.setState({ reducedMotionOverride: false });
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
addEventListener: addListenerSpy,
removeEventListener: removeListenerSpy,
})),
});
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

useUIStore initializes reducedMotion from window.matchMedia(...) at module import time, but this file only stubs window.matchMedia inside beforeEach (after the store has already been imported). That makes the store’s initial reducedMotion value depend on whichever matchMedia was present earlier in the test run. To keep tests deterministic, explicitly reset useUIStore state (at least reducedMotion + reducedMotionOverride) in beforeEach, and restore the original window.matchMedia in afterEach so it doesn’t leak to other suites.

Copilot uses AI. Check for mistakes.
- usePromptAutocomplete: make assertion unconditional — assert
  suggestions.length > 0 and isOpen === true for 'pop' token
- useWaveform: replace setTimeout(10) with waitFor() for deterministic
  async assertions (3 occurrences)
- useAnimatedPresence: save/restore original window.matchMedia in
  afterEach to prevent leaking into other test suites
- useReducedMotion: explicitly reset reducedMotion + reducedMotionOverride
  in beforeEach, restore matchMedia in afterEach

https://claude.ai/code/session_01AAbmUU34sN8t5XRPsvuyzh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

test: Hooks test coverage sprint — 6 hooks, 50 new tests

2 participants