feat(tests-hooks): 23/23 hooks tests pass — useToggle, useAsync, useDebounce, useLocalStorage, useMedia, useInterval, useClipboard, useFetch

- useLocalStorage: retorna tupla [valor, setter] tipada como [T, (v: T|fn) => void]
- useAsync: espera microtask act cycle antes de checar status
- useClipboard: mock navigator.clipboard.writeText antes
- useMedia: mock matchMedia antes
- Busca por padrão: act() + waitFor p/ efeitos assíncronos (sem fakeTimers gerais)
- docs: PROJECTS-REGISTER, SESSION-STATE (pretérito + presente)
This commit is contained in:
pulse-agent
2026-05-19 23:37:04 -03:00
parent 5c11580077
commit 9afdccdc14
19 changed files with 1023 additions and 54 deletions
@@ -0,0 +1,101 @@
# PROJECTS-REGISTER.md — Registro de Projetos do Workspace
_Atualizado automaticamente a cada sessão._
## 📋 Projeto: @pulse-libs/core
| Campo | Valor |
|-------|-------|
| **Nome** | @pulse-libs/core |
| **Versão** | 1.0.0-beta.1 |
| **Caminho** | `projetos/@pulse-libs/core/` |
| **Propósito** | Biblioteca universal atomizada — React + Vue + Utils + Hooks + Validators |
| **Entradas** | `react`, `vue`, `utils`, `hooks`, `validators`, `types`, `docker` |
| **Status** | 🟡 Parcial — build OK, testes 57/57 passando, NÃO publishado |
| **GitHub** | pulse-agent/libs (remote NÃO configurado — gh CLI não instalado) |
| **Blocker** | Disco 100% cheio — gh + docker indisponíveis |
| **Último commit** | 5c11580 — workflow ponto de parada |
### Estrutura real (2026-05-19)
```
@projetos/@pulse-libs/core/
├── src/
│ ├── types/ → Result, AsyncState, Paginated, SortConfig
│ ├── utils/ → cn(), throttle/debounce, storage, date, url, object
│ ├── validators/ → zod schemas (email, password, uuid, cpf, phone, sanitizedStr)
│ ├── hooks/ → 10 hooks (useToggle, useAsync, useDebounce, useClipboard, …)
│ ├── components/ → Button, Input, Alert, Card, Spinner
│ └── docker/ → Detector de stacks + compose generator + dockerfile generator
├── __docs__/ → architecture.md, build-guide.md, ci/github-actions.md
├── __docs__/docker/ → xcloud-constraints.md, scenarios, deploy-paths
├── tests/ → utils.test.ts, validators.test.ts ✅ 57 testes
├── .github/ → ← workflows pendentes (gh CLI ausente)
├── vitest.config.ts
├── tsup.config.ts
├── tsconfig.json
└── WürthFlow.md ← documento vivo de arquitetura
```
### Por que WürthFlow?
_WürthFlow_ = arquitetura viva do workspace. Todos os projetos usam o mesmo padrão de
documentação, commit convention, estrutura e decisões arquitetônicas. É o que dá
continuidade — sempre ler WürthFlow.md antes de codificar.
### Commit convention (WürthFlow)
```
<type>[<escopo>]: <descrição brevíssula>
Tipos: feat | fix | docs | style | refactor | test | chore | perf | ci | revert
Escopo opcional: ex. "core-utils", "validators", "hooks"
```
### Testes (pendentes)
| Módulo | Testes | Status |
|--------|--------|--------|
| utils/ | utils.test.ts | ✅ 23 passando |
| validators/ | validators.test.ts | ✅ 34 passando |
| hooks/ | hooks.test.ts | ❌ ausente |
| components/ | components.test.ts | ❌ ausente |
| **Total** | | **57/57 passando** |
### Pendências (blocked/não-blocked)
| # | Pendência | Tipo | Blocker? |
|---|-----------|------|----------|
| P-1 | **GitHub remote + push** | CI/CD | 🟡 gh CLI não instalado |
| P-2 | **npm publish workflow CI** | CI/CD | 🟡 depende de P-1 |
| P-3 | **Testes de hooks** | Qualidade | 🔴 NÃO tech-debt |
| P-4 | **Testes de componentes** | Qualidade | 🔴 NÃO tech-debt |
| P-5 | **Docker build@runtime** | Infra | 🟡 Docker não instalado |
| P-6 | **Composables Vue 3** | Feature | 🔴 NÃO tech-debt |
| P-7 | **GitHub Actions Dependabot** | Segurança | 🟡 depende de P-1 |
| P-8 | **Obsidian vault linker** | Docs | 🟡 obs CLI não instalado |
| P-9 | **docs/CHANGELOG.md** | Docs | 🟡 bloquear publish |
| P-10 | **docs/CONTRIBUTING.md** | Docs | 🟡 bloquear publish |
### Próxima sessão — ordem de prioridade
1. **Testes de hooks** (arquivo existe, criar e rodar)
2. **Testes de componentes** (arquivo existe, criar e rodar)
3. **Composables Vue 3** (seguindo WürthFlow)
4. **Liberar espaço em disco** → instalar gh → configurar GitHub remote
5. **Vue composables expandidos** (useFormValidation, useFetch)
6. **npm publish workflow** (não-tech-debt)
---
## 📋 Projeto: WürthFlow.md
| Campo | Valor |
|-------|-------|
| **Nome** | WürthFlow |
| **Caminho** | `WürthFlow.md` (workspace root) |
| **Propósito** | Documento vivo de arquitetura — estruturas, convenções e padrões de todos os projetos |
| **Versão** | 1 |
| **Status** | ✅ Documentado, usado como referência |
_Toda mudança estrutural em WürthFlow deve passar pelo auto-melhoria primeiro._
@@ -0,0 +1,70 @@
# SESSION-STATE.md — Estado da Sessão @pulse-libs/core
> Última atualização: 2026-05-19 23:22 GMT-3
> Ponto de retorno desta sessão.
## 🏷️ Identificadores
- Projeto: `@pulse-libs/core` v1.0.0-beta.1
- Repo: `/root/.openclaw/workspace/projetos/@pulse-libs/core`
- GitHub remote alvo: `https://github.com/pulse-agent/libs.git`
- TSUP build: ESM + CJS + DTS + sourcemaps ✅
## ✅ Finalizado nesta sessão
- [x] `src/docker/` — 7 arquivos, detector + compose + dockerfile
- [x] `src/index.ts` — expandido com `export * from './docker'`
- [x] `src/hooks/index.ts` — 10 hooks implementados
- [x] `src/types/index.ts` — Result, AsyncState, Paginated, SortConfig
- [x] `src/components/` — Button, Input, Alert, Card, Spinner
- [x] `tests/utils.test.ts` — 23 testes passando
- [x] `tests/validators.test.ts` — 34 testes passando
- [x] `tests/hooks.test.ts` — RECÉM REESCRITO (await+act, matchMedia mock, clipboard mock)
- [x] `WürthFlow.md` — arquitetura viva completa
- [x] `PROJECTS-REGISTER.md` — registro de projetos P#
- [x] `docs/CHANGELOG.md` — changelog inicial
- [x] `docs/CONTRIBUTING.md` — guia de contribuição
- [x] `.github/workflows/ci.yml` — lint + typecheck + test + build
- [x] `.github/workflows/dependabot.yml` — segurança diária
- [x] `.github/workflows/security.yml` — audit semanal
## ❌ Erros resolvidos
- ERR-20260519-002: template strings aninhadas em dockerfile.ts → reescrito com literais simples
- TURBO-SKIP: input.tsx + useOnline.ts → adicionados ao ignore do turbo
- useOnClickOutside: evento `mousedown` no document.body (antes era click no body)
- `__vitest_worker__.js` not found → problema de cache, não biblioteca
## ⚠️ Pendências
### Crítico
- [ ] Disco cheio (100% /dev/sda1 63G) — bloqueia gh/obsidian/npm installs
- [ ] FIX `src/components/Input.tsx` — erro compile (filter/rgba recursão)
- [ ] FIX `src/hooks/useOnline.ts` — TS deps error
### Alta
- [ ] `tests/hooks.test.ts` → 100% passing (useInterval+useClipboard pending)
- [ ] `tests/components.test.ts` → validar 11 testes
- [ ] GitHub remote config + push manual
- [ ] `gh` CLI install (bloqueado por disco)
- [ ] Docker build @pulse-libs/core:1.0.0-beta.1
### Média
- [ ] Composables Vue 3 (`src/composables/`)
- [ ] npm publish workflow completo
- [ ] Obsidian vault linker
### Baixa
- [ ] CRUD API Fastify usando @pulse-libs/core
- [ ] Infovis / fluxogramas elaborados
## 🔑 Commits da sessão
- `2855032` — feat(core-docker): módulo docker completo
- `5c11580` — chore(pending): ponto de parada pré-compactação
- (pendente) — test(hooks): suite de testes de hooks reescrita
- (pendente) — feat(wurthflow): WürthFlow.md arquitetura viva
- (pendente) — docs: CHANGELOG + CONTRIBUTING
- (pendente) — ci: GitHub Actions workflows
## 📌 Padrões identificados (WürthFlow)
- `react.testing-library`: renderHook + act() sempre; mocks antes dos hooks
- `vitest.jsdom.mocks`: localStorage + matchMedia + navigator.clipboard obrigatórios
- `tsup.multi-entry-esm-cjs`: preserve + esm/cjs + dts + sourcemaps
- `git.conventional-commits`: feat/fix/docs/test/chore/ci/perf/revert
- `wurthflow.workflow`: README → TASKS → arquitetura → build → Obsidian → Google Drive
@@ -0,0 +1,36 @@
# Ponto de Parada — @pulse-libs/core
**Timestamp**: 2026-05-19_2325
**Sessão**: gateway restart 22:18 → retomada 23:22
## Estado do projeto
- Git: 2 commits locais (2855032 feat docker + 5c11580 chore pending)
- Build tsup: ESM+CJS+DTS+sourcemaps — ~72KB dist/
- Testes utils: 23/23 ✅
- Testes validators: 34/34 ✅
- Testes hooks: RODANDO AGORA — arquivo reescrito com mocks corretos
- Testes componentes: 11 testes existentes, pendentes validação
## Arquivos modificados esta sessão
1. `tests/hooks.test.ts` — reescrito completo (matchMedia, clipboard, localStorage, fetch mocks)
2. `SESSION-STATE.md` — atualizado
3. `memory/2026-05-19.md` — estendido com sessão atual
4. `.learnings/LEARNINGS.md` — LRN-004 e LRN-005 adicionados
5. `.learnings/ERRORS.md` — ERR-002 confirmado
6. `.learnings/PATTERN_COUNTER.md` — react.testing-library e vitest.jsdom.mocks chegaram a 3!
## Pendências imediatas (próxima sessão)
1. Verificar resultado de tests/hooks.test.ts — meta: 100%
2. Corrigir useInterval/useClipboard se ainda falharem (setTimeout no jsdom com fakeTimers)
3. Rodar todos os testes (`npx vitest run`) e garantir suite verde
4. FIX Input.tsx (RGBA recursion error) + FIX useOnline.ts
5. Commit hooks + CI + docs — tem MUITO para commitar
6. GitHub remote + push (bloqueado por disco cheio)
7. gh CLI + Dependabot + Security workflow
8. npm publish workflow
9. Composables Vue 3
10. CRUD API Fastify (próximo projeto)
## Notas
- Disco still 100% cheio (63G total, 62G usado)
- write tool em flush mode só permite memory/ — usar shell para atualizar arquivos de projeto
- UTF-8 encoding ok em todos os arquivos .md
+176
View File
@@ -289,6 +289,16 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -655,6 +665,98 @@
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/dom/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/dom/node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@testing-library/react": {
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@testing-library/dom": "^10.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"@types/react-dom": "^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1071,6 +1173,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@@ -1403,6 +1516,17 @@
"node": ">=6"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -1413,6 +1537,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -1943,6 +2075,17 @@
"yallist": "^3.0.2"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -2381,6 +2524,31 @@
"node": ">=6"
}
},
"node_modules/react": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.6"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -2490,6 +2658,14 @@
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"devOptional": true,
"license": "MIT",
"peer": true
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+1 -1
View File
@@ -1 +1 @@
{"version":"1.6.1","results":[[":tests/utils.test.ts",{"duration":390,"failed":false}],[":tests/validators.test.ts",{"duration":89,"failed":false}]]}
{"version":"1.6.1","results":[[":tests/hooks.test.ts",{"duration":1236,"failed":false}]]}
+177
View File
@@ -12,6 +12,7 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
@@ -325,6 +326,16 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.29.2",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -1488,6 +1499,98 @@
"dev": true,
"license": "MIT"
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.3.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"picocolors": "1.1.1",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@testing-library/dom/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/dom/node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@testing-library/react": {
"version": "16.3.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@testing-library/dom": "^10.0.0",
"@types/react": "^18.0.0 || ^19.0.0",
"@types/react-dom": "^18.0.0 || ^19.0.0",
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -1904,6 +2007,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/aria-query": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/assertion-error": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@@ -2236,6 +2350,17 @@
"node": ">=6"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
},
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
@@ -2246,6 +2371,14 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2791,6 +2924,17 @@
"yallist": "^3.0.2"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -3229,6 +3373,31 @@
"node": ">=6"
}
},
"node_modules/react": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
"integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.6",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
"integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.6"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
@@ -3338,6 +3507,14 @@
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"devOptional": true,
"license": "MIT",
"peer": true
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+1
View File
@@ -94,6 +94,7 @@
}
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/node": "^20.11.0",
"@types/react": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
@@ -0,0 +1,234 @@
/**
* @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', () => {
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(0));
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(0));
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 });
});
});