mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
119 lines
4.1 KiB
TypeScript
119 lines
4.1 KiB
TypeScript
// FE-HOOK-COUNTUP-001 to FE-HOOK-COUNTUP-006
|
|
import { act, renderHook } from '@testing-library/react';
|
|
|
|
// The hook reads navigator.userAgent once at module load to detect jsdom and
|
|
// skip the animation. Every test therefore imports it fresh, with the user
|
|
// agent already faked to whatever that test needs.
|
|
async function loadHook(userAgent: string) {
|
|
vi.resetModules();
|
|
Object.defineProperty(navigator, 'userAgent', { value: userAgent, configurable: true });
|
|
return (await import('./useCountUp')).useCountUp;
|
|
}
|
|
|
|
const BROWSER_UA = 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 Chrome/131.0.0.0';
|
|
const JSDOM_UA = 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 jsdom/26.0.0';
|
|
|
|
/** Drives requestAnimationFrame by hand so the easing is deterministic. */
|
|
function stubRaf(): { flush: (now: number) => void; cancelled: number[] } {
|
|
const pending = new Map<number, FrameRequestCallback>();
|
|
const cancelled: number[] = [];
|
|
let id = 0;
|
|
vi.stubGlobal('requestAnimationFrame', vi.fn((cb: FrameRequestCallback) => {
|
|
pending.set(++id, cb);
|
|
return id;
|
|
}));
|
|
vi.stubGlobal('cancelAnimationFrame', vi.fn((handle: number) => {
|
|
cancelled.push(handle);
|
|
pending.delete(handle);
|
|
}));
|
|
return {
|
|
flush(now: number) {
|
|
const due = [...pending.entries()];
|
|
pending.clear();
|
|
for (const [, cb] of due) cb(now);
|
|
},
|
|
cancelled,
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
Object.defineProperty(navigator, 'userAgent', { value: JSDOM_UA, configurable: true });
|
|
});
|
|
|
|
describe('useCountUp', () => {
|
|
it('FE-HOOK-COUNTUP-001: returns the target immediately under jsdom', async () => {
|
|
const useCountUp = await loadHook(JSDOM_UA);
|
|
const { result } = renderHook(() => useCountUp(42));
|
|
expect(result.current).toBe(42);
|
|
});
|
|
|
|
it('FE-HOOK-COUNTUP-002: animates from 0 to the target with an ease-out curve', async () => {
|
|
const raf = stubRaf();
|
|
const useCountUp = await loadHook(BROWSER_UA);
|
|
|
|
const { result } = renderHook(() => useCountUp(100, 1000));
|
|
expect(result.current).toBe(0);
|
|
|
|
// First frame establishes the start timestamp, so it still reads 0.
|
|
act(() => raf.flush(1000));
|
|
expect(result.current).toBe(0);
|
|
|
|
// Half way through, ease-out-quint is already at 1 - 0.5^5 = 96.875%.
|
|
act(() => raf.flush(1500));
|
|
expect(result.current).toBe(97);
|
|
|
|
act(() => raf.flush(2000));
|
|
expect(result.current).toBe(100);
|
|
});
|
|
|
|
it('FE-HOOK-COUNTUP-003: stops requesting frames once it reaches the target', async () => {
|
|
const raf = stubRaf();
|
|
const useCountUp = await loadHook(BROWSER_UA);
|
|
|
|
const { result } = renderHook(() => useCountUp(50, 400));
|
|
act(() => raf.flush(0));
|
|
act(() => raf.flush(400));
|
|
expect(result.current).toBe(50);
|
|
|
|
// No further frame was scheduled, so flushing again changes nothing.
|
|
act(() => raf.flush(800));
|
|
expect(result.current).toBe(50);
|
|
});
|
|
|
|
it('FE-HOOK-COUNTUP-004: cancels the pending frame on unmount', async () => {
|
|
const raf = stubRaf();
|
|
const useCountUp = await loadHook(BROWSER_UA);
|
|
|
|
const { unmount } = renderHook(() => useCountUp(80, 600));
|
|
act(() => raf.flush(0));
|
|
unmount();
|
|
expect(raf.cancelled.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('FE-HOOK-COUNTUP-005: skips the animation when reduced motion is preferred', async () => {
|
|
stubRaf();
|
|
const useCountUp = await loadHook(BROWSER_UA);
|
|
const matchMedia = vi.mocked(window.matchMedia);
|
|
matchMedia.mockReturnValueOnce({ matches: true } as unknown as MediaQueryList);
|
|
|
|
const { result } = renderHook(() => useCountUp(64));
|
|
expect(result.current).toBe(64);
|
|
expect(requestAnimationFrame).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('FE-HOOK-COUNTUP-006: passes non-positive targets straight through', async () => {
|
|
stubRaf();
|
|
const useCountUp = await loadHook(BROWSER_UA);
|
|
|
|
const { result, rerender } = renderHook(({ target }) => useCountUp(target), {
|
|
initialProps: { target: 0 },
|
|
});
|
|
expect(result.current).toBe(0);
|
|
|
|
rerender({ target: -5 });
|
|
expect(result.current).toBe(-5);
|
|
expect(requestAnimationFrame).not.toHaveBeenCalled();
|
|
});
|
|
});
|