feat(lib-core): biblioteca atomica @pulse-libs/core v1.0.0-beta.1
Esta commit conteudo a estrutura atomica completa:
- types: Result<T,E>, AsyncState<T>, Paginated<T>, SortConfig<T>
- utils: date, str, num, cn, debounce, throttle, storage, arr, obj
- validators: Zod schemas — email, password, uuid, url, phone, CPF/CNPJ, sanitizedStr, safeParse
- hooks: useToggle, useAsync, useDebounce, useLocalStorage, useMedia, useInterval, useOnClickOutside, useClipboard, useFetch
- components: Button, Input, Alert, Card, Spinner (atomic design pattern)
- build: tsup v8 ESM+CJS + DTS + sourcemaps — 0 erros
- tests: 57 testes 100% usuarios
- docker: multi-stage Dockerfile (node 20-alpine)
- config: vitest, tsup, tsconfig strict, .npmignore
Filosofia atomica:/utils ← /types ← /validators ← /hooks ← /components
Build: npm run build | Test: npm test | Publish: npm publish
🤖 Generated with Pulse (openclaw + nova-self-improver)
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,156 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
emailSchema,
|
||||
passwordSchema,
|
||||
passwordConfirmSchema,
|
||||
uuidSchema,
|
||||
urlSchema,
|
||||
phoneSchema,
|
||||
documentoSchema,
|
||||
sanitizedStr,
|
||||
required,
|
||||
safeParse,
|
||||
} from '../src/validators';
|
||||
|
||||
describe('emailSchema', () => {
|
||||
it('aceita email valido e normaliza para lowercase', () => {
|
||||
const r = safeParse(emailSchema, 'TESTE@EXAMPLE.COM');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBe('teste@example.com');
|
||||
});
|
||||
|
||||
it('rejeita email invalido', () => {
|
||||
expect(safeParse(emailSchema, 'not-an-email').success).toBe(false);
|
||||
});
|
||||
|
||||
it('aceita email com dominio internacionalizado', () => {
|
||||
expect(safeParse(emailSchema, 'user@mail.co.uk').success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passwordSchema', () => {
|
||||
it('rejeita senha menor que 8 caracteres', () => {
|
||||
expect(safeParse(passwordSchema, 'Abc1234').success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejeita senha sem numero', () => {
|
||||
expect(safeParse(passwordSchema, 'Abcdefgh').success).toBe(false);
|
||||
});
|
||||
|
||||
it('rejeita senha sem letra maiuscula', () => {
|
||||
expect(safeParse(passwordSchema, 'abcdefgh123').success).toBe(false);
|
||||
});
|
||||
|
||||
it('aceita senha valida', () => {
|
||||
expect(safeParse(passwordSchema, 'Minha#Senh@1234').success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('passwordConfirmSchema', () => {
|
||||
it('retorna erro quando senhas nao coincidem', () => {
|
||||
const r = safeParse(
|
||||
passwordConfirmSchema,
|
||||
{ password: 'Abc12345', confirm: 'Zxy98765' },
|
||||
);
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it('aceita quando senhas coincidem', () => {
|
||||
const r = safeParse(
|
||||
passwordConfirmSchema,
|
||||
{ password: 'Abc12345', confirm: 'Abc12345' },
|
||||
);
|
||||
expect(r.success).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('uuidSchema', () => {
|
||||
it('aceita UUID valido', () => {
|
||||
expect(
|
||||
safeParse(uuidSchema, '550e8400-e29b-41d4-a716-446655440000').success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('rejeita string que nao e UUID', () => {
|
||||
expect(safeParse(uuidSchema, 'nao-e-uuid').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlSchema', () => {
|
||||
it('aceita URL valida', () => {
|
||||
const r = safeParse(urlSchema, 'https://exemplo.com');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBe('https://exemplo.com');
|
||||
});
|
||||
|
||||
it('transforma string vazia em undefined', () => {
|
||||
const r = safeParse(urlSchema, '');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('phoneSchema', () => {
|
||||
it('aceita telefone formato (11) 98765-4321', () => {
|
||||
expect(safeParse(phoneSchema, '(11) 98765-4321').success).toBe(true);
|
||||
});
|
||||
|
||||
it('aceita telefone sem parentesis nem tracos', () => {
|
||||
expect(safeParse(phoneSchema, '11987654321').success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejeita telefone com poucos digitos', () => {
|
||||
expect(safeParse(phoneSchema, '123').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('documentoSchema', () => {
|
||||
it('aceita CPF com 11 digitos', () => {
|
||||
const r = safeParse(documentoSchema, '123.456.789-09');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBe('12345678909');
|
||||
});
|
||||
|
||||
it('aceita CNPJ com 14 digitos', () => {
|
||||
const r = safeParse(documentoSchema, '12.345.678/0001-95');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBe('12345678000195');
|
||||
});
|
||||
|
||||
it('rejeita documento com tamanho invalido', () => {
|
||||
expect(safeParse(documentoSchema, '12345').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizedStr', () => {
|
||||
it('remove tags HTML e mantem texto corporizado', () => {
|
||||
const r = safeParse(sanitizedStr, '<b>hello</b> world');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBe('hello world');
|
||||
});
|
||||
|
||||
it('rejeita string que fica vazia apos sanitizar', () => {
|
||||
expect(safeParse(sanitizedStr, '<p></p>').success).toBe(false);
|
||||
expect(safeParse(sanitizedStr, ' ').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('required wrapper', () => {
|
||||
it('transforma string vazia em erro', () => {
|
||||
const schema = required(emailSchema);
|
||||
expect(safeParse(schema, '').success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('safeParse', () => {
|
||||
it('retorna erro formatado quando falha', () => {
|
||||
const r = safeParse(passwordSchema, 'curta');
|
||||
expect(r.success).toBe(false);
|
||||
});
|
||||
|
||||
it('retorna dados quando sucesso', () => {
|
||||
const r = safeParse(emailSchema, 'teste@exemplo.com');
|
||||
expect(r.success).toBe(true);
|
||||
if (r.success) expect(r.data).toBe('teste@exemplo.com');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user