Files
pulse-libs/projetos/@pulse-libs/core/tests/hooks.test.ts
T
pulse-agent d1b3667755 fix(tests-hooks): useClipboard delay fix — setTimeout não atrasa para 0ms
- delay=0 no writeText mock fazia setCopied(false) disparar antes do expect
- Solução: delay=5000ms nos testes de clipboard para evitar race
- 23/23 hooks continua verde; suite total 80/80
2026-05-20 00:17:04 -03:00

237 lines
9.1 KiB
TypeScript

/**
* @pulse-libs/core — Hooks Tests (clean suite)
* @vitest-environment jsdom */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act, waitFor } from '@testing-library/react';
import {
useToggle, useAsync, useDebounce, useLocalStorage,
useMedia, useInterval, useClipboard, useFetch,
} from '../src/hooks/index';
const originalFetch = global.fetch;
// ─ mocks globais ───────────────────────────────────────────────────
const lsStore: Record<string,string> = {};
Object.defineProperty(global, 'localStorage', {
value: {
getItem: k => lsStore[k] ?? null,
setItem: (k,v) => { lsStore[k] = v; },
removeItem: k => { delete lsStore[k]; },
clear: () => { Object.keys(lsStore).forEach(k => delete lsStore[k]); },
},
writable: true,
});
Object.defineProperty(global.navigator, 'clipboard', {
value: { writeText: vi.fn().mockResolvedValue(undefined) },
configurable: true, writable: true,
});
Object.defineProperty(global, 'matchMedia', {
value: vi.fn().mockReturnValue({
matches: false, media: '',
addEventListener: vi.fn(), removeEventListener: vi.fn(),
addListener: vi.fn(), removeListener: vi.fn(),
}),
configurable: true,
});
beforeEach(() => { lsStore[''] = ''; Object.keys(lsStore).forEach(k => delete lsStore[k]); });
// ════════════════════════════════════════════
describe('useToggle', () => {
it('inicia false por default', () => {
const { result } = renderHook(() => useToggle());
expect(result.current[0]).toBe(false);
});
it('inicia true se passado', () => {
const { result } = renderHook(() => useToggle(true));
expect(result.current[0]).toBe(true);
});
it('alterna com toggle()', () => {
const { result } = renderHook(() => useToggle(false));
act(() => result.current[1]());
expect(result.current[0]).toBe(true);
act(() => result.current[1]());
expect(result.current[0]).toBe(false);
});
it('força valor com set()', () => {
const { result } = renderHook(() => useToggle(false));
act(() => result.current[2](true));
expect(result.current[0]).toBe(true);
});
});
// ════════════════════════════════════════════
describe('useAsync', () => {
afterEach(() => { global.fetch = originalFetch; });
it('começa em idling e depois transiciona', async () => {
const fn = vi.fn(async () => Promise.resolve('ok'));
const { result } = renderHook(() => useAsync(fn, []));
// Imediatamente após renderHook: efeitos ainda não rodaram (microtask)
// Espera um ciclo de event loop
await waitFor(() => {
expect(['idle','loading','success','error']).toContain(result.current.status);
});
// Com sucesso, deve estar em success
await waitFor(() => expect(result.current.status).toBe('success'), { timeout: 3000 });
if ('data' in result.current) expect(result.current.data).toBe('ok');
});
it('vai para error se promise rejeita', async () => {
const { result } = renderHook(() => useAsync(async () => { throw new Error('x'); }, []));
await waitFor(() => expect(result.current.status).toBe('error'), { timeout: 3000 });
if ('error' in result.current) expect(result.current.error).toBeTruthy();
});
it('re-executa só quando deps mudam', async () => {
const fn = vi.fn(async () => 'v');
const { rerender } = renderHook(
({ d }) => useAsync(fn, d),
{ initialProps: { d: ['a'] as string[] } }
);
await waitFor(() => expect(fn).toHaveBeenCalledTimes(1), { timeout: 3000 });
rerender({ d: ['b'] as string[] });
await waitFor(() => expect(fn).toHaveBeenCalledTimes(2), { timeout: 3000 });
});
});
// ════════════════════════════════════════════
describe('useDebounce', () => {
it('retorna valor inicial', () => {
const { result } = renderHook(() => useDebounce('x', 200));
expect(result.current).toBe('x');
});
it('atrasa atualização até o timer', async () => {
const { result, rerender } = renderHook(
({ v }) => useDebounce(v, 200),
{ initialProps: { v: 'a' } }
);
rerender({ v: 'b' });
expect(result.current).toBe('a');
await act(async () => { await new Promise(r => setTimeout(r, 300)); });
expect(result.current).toBe('b');
});
it('flushes última mudança em rápidas trocas', async () => {
const { result, rerender } = renderHook(
({ v }) => useDebounce(v, 200),
{ initialProps: { v: 1 } }
);
rerender({ v: 2 }); rerender({ v: 3 }); rerender({ v: 4 });
await act(async () => { await new Promise(r => setTimeout(r, 300)); });
expect(result.current).toBe(4);
});
});
// ════════════════════════════════════════════
describe('useLocalStorage', () => {
it('retorna tupla [valor, setter] corretamente', () => {
const { result } = renderHook(() =>
useLocalStorage('k1', 'hello' as string) as any
);
expect(Array.isArray(result.current)).toBe(true);
expect(result.current[0]).toBe('hello');
expect(typeof result.current[1]).toBe('function');
});
it('salva e lê valor', () => {
const { result } = renderHook(() =>
useLocalStorage('k2', 'init' as string) as any
);
act(() => result.current[1]('novo'));
expect(result.current[0]).toBe('novo');
});
it('persiste em sessões', () => {
lsStore['k3'] = '"saved"';
const { result } = renderHook(() =>
useLocalStorage('k3', 'def' as string) as any
);
expect(result.current[0]).toBe('saved');
});
it('aceita updater fn', () => {
const { result } = renderHook(() =>
useLocalStorage('k4', 0 as number) as any
);
act(() => result.current[1]((n: number) => n + 1));
expect(result.current[0]).toBe(1);
});
});
// ════════════════════════════════════════════
describe('useMedia', () => {
it('retorna booleano', () => {
const { result } = renderHook(() => useMedia('screen'));
expect(typeof result.current).toBe('boolean');
});
});
// ════════════════════════════════════════════
describe('useInterval', () => {
it('não executa quando ms é null', () => {
const fn = vi.fn();
renderHook(() => useInterval(fn, null));
expect(fn).not.toHaveBeenCalled();
});
it('executa imediatamente com immediate=true', () => {
const fn = vi.fn();
renderHook(() => useInterval(fn, 1000, true));
expect(fn).toHaveBeenCalledTimes(1);
});
});
// ════════════════════════════════════════════
describe('useClipboard', () => {
beforeEach(() => { (global.navigator.clipboard.writeText as any).mockResolvedValue(undefined); });
it('inicia copied=false', () => {
const { result } = renderHook(() => useClipboard(2000));
expect(result.current.copied).toBe(false);
});
it('copia texto com sucesso', async () => {
const { result } = renderHook(() => useClipboard(5000));
const ok = await act(() => result.current.copy('hello'));
expect(ok).toBe(true);
expect(result.current.copied).toBe(true);
});
it('retorna false em erro', async () => {
(global.navigator.clipboard.writeText as any).mockRejectedValue(new Error('denied'));
const { result } = renderHook(() => useClipboard(5000));
const ok = await act(() => result.current.copy('falha'));
expect(ok).toBe(false);
});
});
// ════════════════════════════════════════════
describe('useFetch', () => {
afterEach(() => { global.fetch = originalFetch; });
const r200 = (body: unknown) =>
new Response(JSON.stringify(body), { status: 200, headers: { 'Content-Type': 'application/json' } });
it('retorna success para HTTP 200', async () => {
global.fetch = vi.fn(() => Promise.resolve(r200({ ok: true }))) as any;
const { result } = renderHook(() => useFetch('/api', {}) as any);
await waitFor(() => expect(result.current.status).toBe('success'), { timeout: 4000 });
expect((result.current as any).data.ok).toBe(true);
});
it('retorna error para HTTP 404', async () => {
global.fetch = vi.fn(() => Promise.resolve(
new Response('nf', { status: 404, headers: { 'Content-Type': 'application/json' } }))) as any;
const { result } = renderHook(() => useFetch('/404', {}) as any);
await waitFor(() => expect(result.current.status).toBe('error'), { timeout: 4000 });
});
it('retorna error em falha de rede', async () => {
global.fetch = vi.fn(() => Promise.reject(new TypeError('net'))) as any;
const { result } = renderHook(() => useFetch('/fail', {}) as any);
await waitFor(() => expect(result.current.status).toBe('error'), { timeout: 4000 });
});
});