chore: document and stabilize vitest browser setup
This commit is contained in:
parent
42942350dc
commit
eee0f432e7
12 changed files with 1238 additions and 325 deletions
|
|
@ -46,6 +46,7 @@ Aplicação Next.js 15 com Convex e Better Auth para gestão de tickets da Rever
|
|||
- Índice de docs: `docs/README.md`
|
||||
- Operações (produção): `docs/operations.md`
|
||||
- Guia de DEV: `docs/DEV.md`
|
||||
- Testes automatizados (Vitest/Playwright): `docs/testes-vitest.md`
|
||||
- Stack Swarm: `stack.yml` (roteado por Traefik, rede `traefik_public`).
|
||||
|
||||
### Variáveis de ambiente
|
||||
|
|
|
|||
70
docs/testes-vitest.md
Normal file
70
docs/testes-vitest.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# Guia de Testes com Vitest 4
|
||||
|
||||
Este documento resume a configuração atual de testes e como aproveitá-la para automatizar novas verificações.
|
||||
|
||||
## Comandos principais
|
||||
|
||||
- `pnpm test` → roda a suíte unitária em ambiente Node/JSdom.
|
||||
- `pnpm test:browser` → executa os testes de navegador via Playwright (Chromium headless).
|
||||
- `pnpm test:all` → executa as duas suítes de uma vez (requer Playwright instalado).
|
||||
|
||||
> Sempre que adicionar novos testes, priorize mantê-los compatíveis com esses dois ambientes.
|
||||
|
||||
## Pré-requisitos
|
||||
|
||||
1. Dependências JavaScript já estão listadas em `package.json` (`vitest`, `@vitest/browser-playwright`, `playwright`, `jsdom`, etc.).
|
||||
2. Baixe os binários do Playwright uma vez:
|
||||
```bash
|
||||
pnpm exec playwright install chromium
|
||||
```
|
||||
3. Em ambientes Linux “puros”, instale as bibliotecas de sistema recomendadas:
|
||||
```bash
|
||||
sudo apt-get install libnspr4 libnss3 libasound2t64
|
||||
# ou
|
||||
sudo pnpm exec playwright install-deps
|
||||
```
|
||||
|
||||
Se o Playwright avisar sobre dependências ausentes ao rodar `pnpm test:browser`, instale-as e repita o comando.
|
||||
|
||||
## Estrutura de setup
|
||||
|
||||
- `vitest.setup.node.ts` → executado apenas na suíte Node. Aqui é seguro acessar `process`, configurar variáveis de ambiente, carregar `tsconfig-paths/register`, etc.
|
||||
- `tests/setup.browser.ts` → setup vazio para a suíte de navegador. Não use `process` ou APIs do Node aqui; adicione polyfills/mocks específicos do browser quando necessário.
|
||||
|
||||
O arquivo `vitest.config.mts` seleciona automaticamente o setup correto com base na env `VITEST_BROWSER`.
|
||||
|
||||
```ts
|
||||
setupFiles: process.env.VITEST_BROWSER
|
||||
? ["./tests/setup.browser.ts"]
|
||||
: ["./vitest.setup.node.ts"],
|
||||
```
|
||||
|
||||
## Boas práticas para novos testes
|
||||
|
||||
- **Aliases (`@/`)**: continuam funcionando em ambos os ambientes graças ao `vite-tsconfig-paths`.
|
||||
- **Variáveis de ambiente no browser**: use `import.meta.env.VITE_*`. Evite `process.env` no código que será executado no navegador.
|
||||
- **Mocks Playwright**: para testes de browser, use os helpers de `vitest/browser`. Exemplo:
|
||||
```ts
|
||||
import { expect, test } from "vitest"
|
||||
import { page } from "vitest/browser"
|
||||
|
||||
test("exemplo", async () => {
|
||||
await page.goto("https://example.com")
|
||||
await expect(page.getByRole("heading", { level: 1 })).toBeVisible()
|
||||
})
|
||||
```
|
||||
No nosso exemplo atual (`tests/browser/example.browser.test.ts`) manipulamos o DOM diretamente e geramos screenshots com `expect(...).toMatchScreenshot(...)`.
|
||||
- **Snapshots visuais**: os arquivos de referência ficam em `tests/browser/__screenshots__/`. Ao criar ou atualizar um snapshot, revise e commite apenas se estiver correto.
|
||||
- **Mocks que dependem de `vi.fn()`**: quando mockar classes/constructores (ex.: `ConvexHttpClient`), use funções nomeadas ou `class` ao definir a implementação para evitar os erros do Vitest 4 (“requires function or class”).
|
||||
|
||||
## Fluxo sugerido no dia a dia
|
||||
|
||||
1. Rode `pnpm test` localmente antes de abrir PRs.
|
||||
2. Para alterações visuais/lógicas que afetem UI, adicione/atualize um teste em `tests/browser` e valide com `pnpm test:browser`.
|
||||
3. Se novos snapshots forem criados ou alterados, confirme visualmente e inclua os arquivos em commit.
|
||||
4. Para tarefas de automação futuras (por exemplo, smoke-tests que renderizam componentes críticos), utilize a mesma estrutura:
|
||||
- Setup mínimo no `tests/setup.browser.ts`.
|
||||
- Testes localizados em `tests/browser/**.browser.test.ts`.
|
||||
- Utilização de Playwright para interagir com a UI e gerar screenshots/asserts.
|
||||
|
||||
Seguindo este guia, conseguimos manter a suíte rápida no ambiente Node e, ao mesmo tempo, aproveitar o modo browser do Vitest 4 para validações visuais e regressões de UI. Quilas regressões detectadas automaticamente economizam tempo de QA manual e agilizam o ciclo de entrega.***
|
||||
10
package.json
10
package.json
|
|
@ -10,6 +10,8 @@
|
|||
"prisma:generate": "prisma generate",
|
||||
"convex:dev": "convex dev",
|
||||
"test": "vitest --run --passWithNoTests",
|
||||
"test:browser": "cross-env VITEST_BROWSER=true vitest --run --browser.headless tests/browser/example.browser.test.ts --passWithNoTests",
|
||||
"test:all": "cross-env VITEST_BROWSER=true vitest --run --passWithNoTests",
|
||||
"auth:seed": "node scripts/seed-auth.mjs",
|
||||
"queues:ensure": "node scripts/ensure-default-queues.mjs",
|
||||
"desktop:dev": "pnpm --filter appsdesktop tauri dev",
|
||||
|
|
@ -80,14 +82,20 @@
|
|||
"@types/react-dom": "^18",
|
||||
"@types/sanitize-html": "^2.16.0",
|
||||
"@types/three": "^0.180.0",
|
||||
"@vitest/browser-playwright": "^4.0.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "^16.0.0",
|
||||
"eslint-plugin-react-hooks": "^5.0.0",
|
||||
"jsdom": "^27.0.1",
|
||||
"playwright": "^1.56.1",
|
||||
"prisma": "^6.16.2",
|
||||
"tailwindcss": "^4",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tw-animate-css": "^1.3.8",
|
||||
"typescript": "^5",
|
||||
"vitest": "^2.1.4"
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^4.0.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1338
pnpm-lock.yaml
generated
1338
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,11 +4,17 @@ const mutationMock = vi.fn()
|
|||
const deleteManyMock = vi.fn()
|
||||
const assertAuthenticatedSession = vi.fn()
|
||||
|
||||
vi.mock("convex/browser", () => ({
|
||||
ConvexHttpClient: vi.fn().mockImplementation(() => ({
|
||||
vi.mock("convex/browser", () => {
|
||||
const ConvexHttpClient = vi.fn(function ConvexHttpClientMock() {
|
||||
return {
|
||||
mutation: mutationMock,
|
||||
})),
|
||||
}))
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
ConvexHttpClient,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock("@/lib/prisma", () => ({
|
||||
prisma: {
|
||||
|
|
@ -32,6 +38,12 @@ describe("POST /api/admin/machines/delete", () => {
|
|||
mutationMock.mockReset()
|
||||
deleteManyMock.mockReset()
|
||||
assertAuthenticatedSession.mockReset()
|
||||
mutationMock.mockImplementation(async (_ctx, payload) => {
|
||||
if (payload && typeof payload === "object" && "machineId" in payload) {
|
||||
return { ok: true }
|
||||
}
|
||||
return { _id: "user_123" }
|
||||
})
|
||||
assertAuthenticatedSession.mockResolvedValue({
|
||||
user: {
|
||||
email: "admin@example.com",
|
||||
|
|
@ -41,8 +53,7 @@ describe("POST /api/admin/machines/delete", () => {
|
|||
avatarUrl: null,
|
||||
},
|
||||
})
|
||||
mutationMock.mockResolvedValueOnce({ _id: "user_123" })
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {})
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(function noop() {})
|
||||
restoreConsole = () => consoleSpy.mockRestore()
|
||||
})
|
||||
|
||||
|
|
@ -52,7 +63,6 @@ describe("POST /api/admin/machines/delete", () => {
|
|||
})
|
||||
|
||||
it("returns ok when the machine removal succeeds", async () => {
|
||||
mutationMock.mockResolvedValueOnce({ ok: true })
|
||||
const { POST } = await import("./route")
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/admin/machines/delete", {
|
||||
|
|
@ -69,7 +79,12 @@ describe("POST /api/admin/machines/delete", () => {
|
|||
})
|
||||
|
||||
it("still succeeds when the Convex machine is already missing", async () => {
|
||||
mutationMock.mockRejectedValueOnce(new Error("Máquina não encontrada"))
|
||||
mutationMock.mockImplementation(async (_ctx, payload) => {
|
||||
if (payload && typeof payload === "object" && "machineId" in payload) {
|
||||
throw new Error("Máquina não encontrada")
|
||||
}
|
||||
return { _id: "user_123" }
|
||||
})
|
||||
const { POST } = await import("./route")
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/admin/machines/delete", {
|
||||
|
|
@ -84,7 +99,12 @@ describe("POST /api/admin/machines/delete", () => {
|
|||
})
|
||||
|
||||
it("returns an error for other Convex failures", async () => {
|
||||
mutationMock.mockRejectedValueOnce(new Error("timeout error"))
|
||||
mutationMock.mockImplementation(async (_ctx, payload) => {
|
||||
if (payload && typeof payload === "object" && "machineId" in payload) {
|
||||
throw new Error("timeout error")
|
||||
}
|
||||
return { _id: "user_123" }
|
||||
})
|
||||
const { POST } = await import("./route")
|
||||
const response = await POST(
|
||||
new Request("http://localhost/api/admin/machines/delete", {
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
40
tests/browser/example.browser.test.ts
Normal file
40
tests/browser/example.browser.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { expect, test } from "vitest"
|
||||
|
||||
test("CTA button snapshot", async () => {
|
||||
const html = `
|
||||
<main
|
||||
style="
|
||||
font-family: 'Inter', sans-serif;
|
||||
padding: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #111827, #1f2937);
|
||||
min-height: 320px;
|
||||
"
|
||||
>
|
||||
<button
|
||||
data-testid="cta"
|
||||
style="
|
||||
font-size: 18px;
|
||||
padding: 14px 28px;
|
||||
border-radius: 9999px;
|
||||
border: none;
|
||||
color: white;
|
||||
background: #2563eb;
|
||||
box-shadow: 0 10px 20px rgba(37, 99, 235, 0.35);
|
||||
cursor: pointer;
|
||||
"
|
||||
>
|
||||
Abrir chamado
|
||||
</button>
|
||||
</main>
|
||||
`
|
||||
|
||||
document.body.innerHTML = html
|
||||
|
||||
const ctaButton = document.querySelector("[data-testid='cta']")
|
||||
expect(ctaButton).toBeTruthy()
|
||||
|
||||
await expect(document.body).toMatchScreenshot("cta-button")
|
||||
})
|
||||
2
tests/setup.browser.ts
Normal file
2
tests/setup.browser.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Browser-specific Vitest bootstrap. Keep this file free of Node globals.
|
||||
// Add global mocks or polyfills here when we start writing browser tests.
|
||||
|
|
@ -1,22 +1,61 @@
|
|||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import { defineConfig } from "vitest/config"
|
||||
import { playwright } from "@vitest/browser-playwright"
|
||||
import tsconfigPaths from "vite-tsconfig-paths"
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const srcDir = path.resolve(__dirname, "./src")
|
||||
const convexDir = path.resolve(__dirname, "./convex")
|
||||
const isCI = process.env.CI === "true"
|
||||
const isBrowserRun = process.env.VITEST_BROWSER === "true"
|
||||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
plugins: [tsconfigPaths()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@/convex": path.resolve(__dirname, "./convex"),
|
||||
"@": srcDir,
|
||||
"@/convex": convexDir,
|
||||
},
|
||||
},
|
||||
test: {
|
||||
pool: (process.env.VITEST_POOL as "threads" | "forks" | "vmThreads" | undefined) ?? "threads",
|
||||
environment: "node",
|
||||
globals: true,
|
||||
setupFiles: isBrowserRun ? ["./tests/setup.browser.ts"] : ["./vitest.setup.node.ts"],
|
||||
pool: (process.env.VITEST_POOL as "threads" | "forks" | "vmThreads" | undefined) ?? "threads",
|
||||
testTimeout: isBrowserRun ? 30000 : 15000,
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
include: ["src/**/*.{ts,tsx}", "convex/**/*.ts"],
|
||||
exclude: ["**/*.d.ts", "**/*.test.*", "tests/**"],
|
||||
reportsDirectory: "./coverage",
|
||||
},
|
||||
deps: {
|
||||
registerNodeLoader: true,
|
||||
},
|
||||
alias: {
|
||||
"@": srcDir,
|
||||
"@/convex": convexDir,
|
||||
},
|
||||
browser: {
|
||||
enabled: isBrowserRun,
|
||||
provider: playwright({
|
||||
launchOptions: {
|
||||
headless: isCI ? true : undefined,
|
||||
},
|
||||
}),
|
||||
instances: [
|
||||
{
|
||||
browser: "chromium",
|
||||
launch: {
|
||||
headless: isCI ? true : false,
|
||||
},
|
||||
},
|
||||
],
|
||||
trace: isCI ? "on-first-retry" : "off",
|
||||
},
|
||||
environment: "jsdom",
|
||||
include: ["src/**/*.test.ts", "tests/**/*.test.ts"],
|
||||
setupFiles: ["./vitest.setup.ts"],
|
||||
testTimeout: 15000,
|
||||
exclude: isBrowserRun ? [] : ["tests/browser/**/*.browser.test.ts"],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
8
vitest.setup.node.ts
Normal file
8
vitest.setup.node.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Node-only Vitest bootstrap: keep browser runs free from process dependencies.
|
||||
if (typeof process !== "undefined" && process.versions?.node) {
|
||||
await import("tsconfig-paths/register")
|
||||
|
||||
process.env.BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET ?? "test-secret"
|
||||
process.env.NEXT_PUBLIC_APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
|
||||
process.env.BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
process.env.BETTER_AUTH_SECRET = process.env.BETTER_AUTH_SECRET ?? "test-secret"
|
||||
process.env.NEXT_PUBLIC_APP_URL = process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000"
|
||||
process.env.BETTER_AUTH_URL = process.env.BETTER_AUTH_URL ?? process.env.NEXT_PUBLIC_APP_URL
|
||||
Loading…
Add table
Add a link
Reference in a new issue