Files
pulse-memory/projetos/@pulse-libs/core/tests/components.test.tsx
T

400 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @pulse-libs/core — Component Tests (clean suite)
* @vitest-environment jsdom
*
* Acessa elementos exclusivamente via render().container.querySelector/querySelectorAll
* para evitar conflitos com o accessibility tree vazio do jsdom.
*/
import { vi, beforeEach } from 'vitest';
import { render, fireEvent } from '@testing-library/react';
import {
Button, Input, Alert, Card, CardHeader, CardTitle, CardBody,
} from '../src/components/index';
// ─ mocks globais ────────────────────────────────────────────────
beforeEach(() => {});
Object.defineProperty(global, 'matchMedia', {
value: vi.fn().mockReturnValue({
matches: false, media: '',
addEventListener: vi.fn(), removeEventListener: vi.fn(),
}),
configurable: true,
});
// ═══════════════════════════════════════════════════════════════
// Button
// ═══════════════════════════════════════════════════════════════
describe('Button', () => {
it('renderiza <button> e está habilitado', () => {
const b = render(<Button>Clique</Button>).container.querySelector('button')!;
expect(b.textContent).toBe('Clique');
expect(b.disabled).toBe(false);
});
it('é <button type="button"> por default', () => {
expect(render(<Button />).container.querySelector('button')!.tagName.toLowerCase()).toBe('button');
});
it('variant primary → bg-indigo-600', () => {
expect(render(<Button />).container.querySelector('button')!.classList.contains('bg-indigo-600')).toBe(true);
});
it('variant danger → bg-red-600', () => {
expect(render(<Button variant="danger" />).container.querySelector('button')!.classList.contains('bg-red-600')).toBe(true);
});
it('variant secondary → border + bg-white', () => {
const b = render(<Button variant="secondary" />).container.querySelector('button')!;
expect(b.classList.contains('border')).toBe(true);
expect(b.classList.contains('bg-white')).toBe(true);
});
it('variant ghost → text-gray-600 + hover sem bg', () => {
const b = render(<Button variant="ghost" />).container.querySelector('button')!;
expect(b.classList.contains('text-gray-600')).toBe(true);
expect(b.classList.contains('hover:bg-gray-100')).toBe(true);
});
it('variant success → bg-emerald-600', () => {
expect(render(<Button variant="success" />).container.querySelector('button')!.classList.contains('bg-emerald-600')).toBe(true);
});
it('size sm → px-2.5 py-1 text-xs', () => {
const cls = render(<Button size="sm" />).container.querySelector('button')!.className;
expect(cls).toContain('px-2.5');
expect(cls).toContain('py-1');
expect(cls).toContain('text-xs');
});
it('size md → px-4 py-2 text-sm', () => {
const cls = render(<Button />).container.querySelector('button')!.className;
expect(cls).toContain('px-4');
expect(cls).toContain('py-2');
expect(cls).toContain('text-sm');
});
it('size lg → px-6 py-3 text-base', () => {
const cls = render(<Button size="lg" />).container.querySelector('button')!.className;
expect(cls).toContain('px-6');
expect(cls).toContain('py-3');
expect(cls).toContain('text-base');
});
it('disabled → HTMLButtonElement.disabled = true', () => {
expect(render(<Button disabled />).container.querySelector('button')!.disabled).toBe(true);
});
it('loading → disabled + SVG spinner', () => {
const { container } = render(<Button loading />);
const b = container.querySelector('button')!;
expect(b.disabled).toBe(true);
expect(container.querySelector('svg.animate-spin')).toBeTruthy();
});
it('leftIcon renderizado', () => {
const { container } = render(<Button leftIcon={<span data-testid="li"></span>} />);
expect(container.querySelector('#li') || container.querySelector('[data-testid="li"]')).toBeTruthy();
});
it('rightIcon ausente durante loading', () => {
const ri = render(
<Button loading rightIcon={<span data-testid="ri"></span>} />
).container.querySelector('[data-testid="ri"]');
expect(ri).toBeNull();
});
it('rightIcon visível quando loading=false', () => {
const ri = render(
<Button rightIcon={<span data-testid="ri"></span>} onClick={() => {}} />
).container.querySelector('[data-testid="ri"]');
expect(ri).toBeTruthy();
});
it('chama onClick ao clicar', () => {
const fn = vi.fn();
render(<Button onClick={fn}>Click</Button>);
fireEvent.click(render(<Button onClick={fn}>Click</Button>).container.querySelector('button')!);
expect(fn).toHaveBeenCalledTimes(1);
});
it('NÃO chama onClick quando disabled', () => {
const fn = vi.fn();
const b = render(<Button disabled onClick={fn} />).container.querySelector('button')!;
fireEvent.click(b); // <-- chama diretamente no botão
expect(fn).not.toHaveBeenCalled();
});
it('NÃO chama onClick quando loading', () => {
const fn = vi.fn();
const b = render(<Button loading onClick={fn} />).container.querySelector('button')!;
fireEvent.click(b);
expect(fn).not.toHaveBeenCalled();
});
it('mescla className custom', () => {
expect(
render(<Button className="min-w-[200px]" />).container.querySelector('button')!.classList.contains('min-w-[200px]')
).toBe(true);
});
it('propaga aria-label e data-*', () => {
const b = render(<Button aria-label="Salvar" data-action="salvar" />).container.querySelector('button')!;
expect(b.getAttribute('aria-label')).toBe('Salvar');
expect(b.getAttribute('data-action')).toBe('salvar');
});
});
// ═══════════════════════════════════════════════════════════════
// Input
// ═══════════════════════════════════════════════════════════════
describe('Input', () => {
it('renderiza <input> quando placeholder dado', () => {
expect(render(<Input placeholder="x" />).container.querySelector('input')).toBeTruthy();
});
it('renderiza <label> quando label é fornecido', () => {
const c = render(<Input label="Nome" />).container;
expect(c.querySelector('label')).toBeTruthy();
expect(c.textContent).toContain('Nome');
});
it('label conectado ao input via htmlFor = id gerado do label', () => {
const c = render(<Input label="Senha" />).container;
const lbl = c.querySelector('label') as HTMLLabelElement;
const inp = c.querySelector('input') as HTMLInputElement;
expect(inp.id).toBe(lbl.htmlFor);
});
it('renderiza hint quando fornecido sem erro', () => {
expect(render(<Input hint="dica" />).container.textContent).toContain('dica');
});
it('erro SHOW, hint HIDE quando ambos fornecidos', () => {
const c = render(<Input error="err" hint="dica" />).container;
expect(c.textContent).toContain('err');
const spans = c.querySelectorAll('span');
const temDica = Array.from(spans).some((s) => s.textContent === 'dica');
expect(temDica).toBe(false);
});
it('span vazio quando nenhum hint nem erro', () => {
expect(render(<Input />).container.querySelectorAll('span').length).toBe(0);
});
it('id explícito não é sobrescrito pelo label', () => {
const inp = render(<Input id="custom" label="X" />).container.querySelector('input') as HTMLInputElement;
expect(inp.id).toBe('custom');
});
it('type atributo passado para <input>', () => {
expect(render(<Input type="tel" />).container.querySelector('input')!.getAttribute('type')).toBe('tel');
});
it('aciona onChange', () => {
const fn = vi.fn();
const inp = render(<Input onChange={fn} />).container.querySelector('input')!;
Object.defineProperty(inp, 'value', { writable: true, value: 'abc' }); // <-- jsdom não altera value naturalmente
fireEvent.change(inp);
expect(fn).toHaveBeenCalledTimes(1);
});
it('aciona onFocus e onBlur', () => {
const fFn = vi.fn(), bFn = vi.fn();
const inp = render(<Input onFocus={fFn} onBlur={bFn} />).container.querySelector('input')!;
fireEvent.focus(inp);
fireEvent.blur(inp);
expect(fFn).toHaveBeenCalledTimes(1);
expect(bFn).toHaveBeenCalledTimes(1);
});
it('mescla className custom', () => {
expect(
render(<Input className="text-xs" />).container.querySelector('input')!.classList.contains('text-xs')
).toBe(true);
});
it('erro aplica border-red-500 + focus:ring-red-500', () => {
const inp = render(<Input error="x" />).container.querySelector('input')!;
expect(inp.classList.contains('border-red-500')).toBe(true);
expect(inp.classList.contains('focus:ring-red-500')).toBe(true);
});
});
// ═══════════════════════════════════════════════════════════════
// Alert
// ═══════════════════════════════════════════════════════════════
describe('Alert', () => {
it('renderiza children no DOM', () => {
expect(render(<Alert>Msg</Alert>).container.textContent).toContain('Msg');
});
it('renderiza titulo + children', () => {
const c = render(<Alert title="T">B</Alert>).container;
expect(c.textContent).toContain('T');
expect(c.textContent).toContain('B');
});
function alertDiv(): HTMLElement {
return render(<Alert />).container.firstChild as HTMLElement;
}
it('variant info (default) → bg-blue-50 border-blue-200 text-blue-800', () => {
const cls = alertDiv().className;
expect(cls).toContain('bg-blue-50');
expect(cls).toContain('border-blue-200');
expect(cls).toContain('text-blue-800');
});
it('variant success → verde', () => {
const cls = render(<Alert variant="success" />).container.firstChild!.className;
expect(cls).toContain('bg-emerald-50');
expect(cls).toContain('border-emerald-200');
});
it('variant warning → amarelo', () => {
expect(render(<Alert variant="warning" />).container.firstChild!.className).toContain('bg-amber-50');
});
it('variant error → vermelho', () => {
expect(render(<Alert variant="error" />).container.firstChild!.className).toContain('bg-red-50');
});
it('botão × aparece quando onClose fornecido', () => {
// O botão close é um <button> filho direto do alert
const btns = render(<Alert onClose={() => {}} />).container.querySelectorAll('button');
expect(btns.length).toBeGreaterThan(0);
});
it('chama onClose ao clicar o botão ×', () => {
const fn = vi.fn();
const btns = render(<Alert onClose={fn} />).container.querySelectorAll('button');
fireEvent.click(btns[0]);
expect(fn).toHaveBeenCalledTimes(1);
});
it('NÃO mostra botão × quando onClose ausente', () => {
expect(render(<Alert />).container.querySelectorAll('button').length).toBe(0);
});
it('emoji de variante aparece no DOM', () => {
expect(render(<Alert variant="error" />).container.textContent).toContain('❌');
expect(render(<Alert variant="success" />).container.textContent).toContain('✅');
expect(render(<Alert variant="warning" />).container.textContent).toContain('⚠️');
expect(render(<Alert variant="info" />).container.textContent).toContain('️');
});
});
// ═══════════════════════════════════════════════════════════════
// Card
// ═══════════════════════════════════════════════════════════════
describe('Card', () => {
it('renderiza div.rounde-xl', () => {
const el = render(<Card>X</Card>).container.firstChild as HTMLElement;
expect(el.classList.contains('rounded-xl')).toBe(true);
expect(el.textContent).toContain('X');
});
it('classes base completas', () => {
const cls = render(<Card />).container.firstChild!.className;
['rounded-xl', 'border', 'border-gray-200', 'bg-white', 'shadow-sm'].forEach((c) => {
expect(cls).toContain(c);
});
});
it('mescla className custom', () => {
expect(
render(<Card className="mc" />).container.firstChild!.className
).toContain('mc');
});
it('aceita id + onClick', () => {
const fn = vi.fn();
const el = render(<Card id="c1" onClick={fn}>C</Card>).container.querySelector('#c1')!;
expect(el).toBeTruthy();
fireEvent.click(el);
expect(fn).toHaveBeenCalled();
});
it('aninhamento sem perda', () => {
const c = render(
<Card>
<CardHeader><CardTitle>T</CardTitle></CardHeader>
<CardBody>B</CardBody>
</Card>
).container;
expect(c.textContent).toContain('T');
expect(c.textContent).toContain('B');
});
});
describe('CardHeader', () => {
it('estilos px-5 py-4 border-b border-gray-100', () => {
const cls = render(<CardHeader>H</CardHeader>).container.firstChild!.className;
['px-5', 'py-4', 'border-b', 'border-gray-100'].forEach((c) => {
expect(cls).toContain(c);
});
});
});
describe('CardTitle', () => {
it('renderiza como <h3>', () => {
expect(render(<CardTitle>T</CardTitle>).container.querySelector('h3')).toBeTruthy();
});
it('texto igual ao passado', () => {
expect(render(<CardTitle>Título</CardTitle>).container.querySelector('h3')!.textContent).toBe('Título');
});
it('estilos do título', () => {
const cls = render(<CardTitle>T</CardTitle>).container.querySelector('h3')!.className;
expect(cls).toContain('font-semibold');
expect(cls).toContain('text-gray-900');
expect(cls).toContain('text-base');
});
});
describe('CardBody', () => {
it('renderiza children', () => {
expect(render(<CardBody>B</CardBody>).container.textContent).toContain('B');
});
it('estilos px-5 py-4', () => {
const cls = render(<CardBody>B</CardBody>).container.querySelector('div')!.className;
expect(cls).toContain('px-5');
expect(cls).toContain('py-4');
});
it('mescla className custom', () => {
expect(
render(<CardBody className="bs" />).container.querySelector('div')!.className
).toContain('bs');
});
});
describe('Card — stress composição', () => {
it('Header + Title + Body na mesma árvore', () => {
const c = render(
<Card>
<CardHeader><CardTitle>Título</CardTitle></CardHeader>
<CardBody>Corpo</CardBody>
</Card>
).container;
expect(c.querySelector('.shadow-sm')).toBeTruthy();
expect(c.textContent).toContain('Título');
expect(c.textContent).toContain('Corpo');
});
it('100 linhas estrutura se mantém', () => {
const lines = Array.from({ length: 100 }, (_, i) => `linha ${i + 1}`).join('\n');
const c = render(
<Card>
<CardBody style={{ overflow: 'auto' }}>{lines}</CardBody>
</Card>
).container;
expect(c.querySelector('.shadow-sm')).toBeTruthy();
expect(c.querySelector('.border')).toBeTruthy();
expect(c.textContent).toContain('linha 1');
expect(c.textContent).toContain('linha 100');
});
});