Files
Pulse Agent 0889ee9117 feat(lib): add useLiveStream WS hook + useLiveMetrics + LiveMetricChart
feat(hooks): add useLiveStream generic WebSocket hook
  - supports websocket/sse/polling transports
  - exponential backoff reconnect with jitter
  - circular buffer with configurable size
  - typed filter callback per use case
  - manual disconnect + reconnect + error state

feat(hooks): add useLiveMetrics derived hook
  - sliding time-window cut
  - moving average (configurable window)
  - current / avg / min / max / ratePerSecond
  - zero allocations per tick (memoized)

feat(charts): add LiveMetricChart molecule (Recharts)
  - line + area variants, grid + tooltip
  - moving-average overlay (dashed)
  - ConnectionStatus atom in header
  - status bar + compact mode
  - 100% responsive, GPU via SVG ViewBox

feat(atoms): add ConnectionStatus indicator
  - 5 states: disconnected/connecting/connected/reconnecting/error
  - animated pulse, JetBrains Mono, pill style
  - exported helpers: formatLatency / formatBytes

docs(pkg): bump v0.1.0 → v0.2.0, add recharts peerDep
2026-05-20 22:59:10 -03:00

221 lines
5.6 KiB
TypeScript
Executable File

/**
* @vitest-environment jsdom
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
cn,
str,
num,
arr,
obj,
date,
storage,
debounce,
throttle,
} from '../src/utils/index';
describe('cn()', () => {
it('concatena classes válidas', () => {
expect(cn('foo', 'bar')).toBe('foo bar');
});
it('filtra falsy values', () => {
expect(cn('foo', false, null, undefined, 'bar')).toBe('foo bar');
});
it('flatten nested arrays', () => {
expect(cn('a', ['b', 'c'])).toBe('a b c');
});
it('normaliza espaços múltiplos', () => {
expect(cn('a', ' b ', 'c')).toBe('a b c');
});
});
describe('str utils', () => {
it('capitalize', () => {
expect(str.capitalize('pULSE')).toBe('Pulse');
expect(str.capitalize('')).toBe('');
});
it('truncate com sufixo', () => {
expect(str.truncate('abcdef', 4, '…')).toBe('abcd…');
});
it('truncate retorna original se menor', () => {
expect(str.truncate('ab', 10)).toBe('ab');
});
it('camelCase e kebabCase', () => {
expect(str.camelCase('hello-world')).toBe('helloWorld');
expect(str.kebabCase('helloWorld')).toBe('hello-world');
});
it('removeAccents', () => {
expect(str.removeAccents('coração')).toBe('coracao');
});
it('maskEmail', () => {
expect(str.maskEmail('john.doe@example.com')).toBe('j******e@example.com');
expect(str.maskEmail('ab@x.com')).toBe('a*@x.com');
});
it('slugify', () => {
expect(str.slugify('São Paulo → Cidade!')).toBe('s-o-paulo-cidade');
expect(str.slugify('Hello World')).toBe('hello-world');
});
});
describe('num utils', () => {
it('clamp — limita intervalo', () => {
expect(num.clamp(15, 0, 10)).toBe(10);
expect(num.clamp(-5, 0, 10)).toBe(0);
expect(num.clamp(5, 0, 10)).toBe(5);
});
it('rand — gera dentro do intervalo', () => {
for (let i = 0; i < 10; i++) {
const v = num.rand(1, 10);
expect(v).toBeGreaterThanOrEqual(1);
expect(v).toBeLessThanOrEqual(10);
expect(Number.isInteger(v)).toBe(true);
}
});
it('format — locale BR', () => {
expect(num.format(1000)).toContain('1');
});
it('percent', () => {
expect(num.percent(5, 0)).toBe(0);
expect(num.percent(50, 100, 0)).toBe(50);
expect(num.percent(1, 3, 0)).toBe(33);
});
});
describe('arr utils', () => {
it('unique', () => {
expect(arr.unique([1, 2, 2, 3])).toEqual([1, 2, 3]);
});
it('unique com key', () => {
const objs = [{ id: 1 }, { id: 2 }, { id: 1 }];
expect(arr.unique(objs, 'id')).toHaveLength(2);
});
it('chunk', () => {
expect(arr.chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
expect(arr.chunk([1, 2, 3, 4], 2)).toEqual([[1, 2], [3, 4]]);
});
it('shuffle mantém elementos', () => {
const original = [1, 2, 3, 4, 5];
const shuffled = arr.shuffle(original);
expect(shuffled.length).toBe(original.length);
expect(shuffled.sort()).toEqual(original.sort());
});
});
describe('obj utils', () => {
it('pick', () => {
expect(obj.pick({ a: 1, b: 2, c: 3 }, ['a'])).toEqual({ a: 1 });
});
it('omit', () => {
expect(obj.omit({ a: 1, b: 2 }, ['b'])).toEqual({ a: 1 });
});
it('isEmpty', () => {
expect(obj.isEmpty({})).toBe(true);
expect(obj.isEmpty({ a: 1 })).toBe(false);
expect(obj.isEmpty(null)).toBe(true);
expect(obj.isEmpty(undefined)).toBe(true);
});
});
describe('date utils', () => {
it('format', () => {
const d = new Date('2024-01-15T10:30:00');
expect(date.format(d, 'YYYY-MM-DD')).toBe('2024-01-15');
expect(date.format(d, 'HH:mm')).toBe('10:30');
});
it('now retorna string ISO', () => {
const result = date.now();
expect(typeof result).toBe('string');
expect(new Date(result).toISOString()).toBe(result);
});
it('isToday', () => {
expect(date.isToday(new Date())).toBe(true);
expect(date.isToday(new Date('2000-01-01'))).toBe(false);
});
it('daysBetween', () => {
const a = new Date('2024-01-01');
const b = new Date('2024-01-10');
expect(date.daysBetween(a, b)).toBe(9);
expect(date.daysBetween(b, a)).toBe(-9);
});
});
describe('storage', () => {
beforeEach(() => {
localStorage.clear();
});
it('get retorna fallback quando vazio', () => {
expect(storage.get('key', 'default')).toBe('default');
});
it('set e get', () => {
storage.set('key', { a: 1 });
expect(storage.get('key', null)).toEqual({ a: 1 });
});
it('remove', () => {
storage.set('key1', 1);
storage.remove('key1');
expect(storage.get('key1', 'x')).toBe('x');
});
it('clear', () => {
storage.set('a', 1);
storage.set('b', 2);
storage.clear();
expect(storage.get('a', null)).toBe(null);
});
it('clear com prefixo', () => {
storage.set('app_a', 1);
storage.set('app_b', 2);
storage.set('other', 3);
storage.clear('app_');
expect(storage.get('app_a', null)).toBe(null);
expect(storage.get('other', null)).toBe(3);
});
});
describe('debounce', () => {
it('atrasa execuçao — chama apenas ultima chamada', async () => {
const fn = vi.fn();
const debounced = debounce(fn, 50);
debounced('a');
debounced('b');
expect(fn).not.toHaveBeenCalled();
await new Promise(r => setTimeout(r, 100));
expect(fn).toHaveBeenCalledTimes(1);
expect(fn).toHaveBeenCalledWith('b');
});
});
describe('throttle', () => {
it('executa imediatamente na primeira chamada', () => {
const fn = vi.fn();
const throttled = throttle(fn, 50);
throttled('a');
expect(fn).toHaveBeenCalledWith('a');
expect(fn).toHaveBeenCalledTimes(1);
});
});