Qualidade e Processo

Convenções, pré-commit, build/deploy, harness e Definition of Done.

13 — Convenções

13 — Convenções

Nomenclatura de arquivos, dados, hooks, handlers e constantes. A regra que governa tudo: dado do backend mantém o nome do backend; lógica do front é em inglês. Ao final, uma lista de anti-padrões reais e recorrentes que não devem ser replicados.

Idioma

Elemento Idioma Exemplo
Propriedade vinda do backend Português _Nome, _DataExpiracao, Documentos
Interface que espelha o backend Inglês SupplierXML, SaveShipmentRequest
Variável local, função, helper Inglês isLoading, groupByLayout, handleClick
Nome de componente Inglês DataTable, LoadingButton
Nome de página Inglês IncludeSupplierPage
Texto de interface Português label="Nome do fornecedor"
Mensagem de validação Português "Informe o CNPJ"
Comentário de código Português ver estilo
Segmento de URL Inglês registration/supplier-type/add

O critério: se o nome atravessa a fronteira com o backend, ele é do backend. Se vive só no front, é inglês.

Propriedades do backend: _Prefixo

interface FornecedorXML {
  _OID: string; // primitivo → underscore
  _Nome: string; // primitivo → underscore
  _Ativo: boolean; // primitivo → underscore
  _DataCadastro: string; // primitivo → underscore

  Endereco: EnderecoXML; // objeto → sem prefixo
  Documentos: DocumentoXML[]; // coleção → sem prefixo
}

Regra do Curio: primitivo leva _, objeto complexo não leva.

Isso não é decoração — é como o payload chega. Renomear para camelCase exigiria uma camada de mapeamento em toda request e resposta. O projeto opta por não ter essa camada: o custo é conviver com _Nome no front.

Consequências:

Booleano do backend: Flag

O Curio não tem tipo booleano nativo trafegando na rede — o valor real que chega/sai é a string "S" ou "N". Nunca converta para boolean na borda; mantenha Flag do início ao fim.

type Flag = "S" | "N";

interface FornecedorXML {
  _Ativo: Flag; // não `boolean`
}

Regra: true → "S", false → "N", em qualquer campo que atravesse a fronteira com o backend (request ou response). Se o front precisar de um boolean de verdade (ex.: checked de um Checkbox), converta na borda da UI, nunca no tipo que representa o payload:

<Checkbox checked={fornecedor._Ativo === "S"} onChange={(e) => setValue("_Ativo", e.target.checked ? "S" : "N")} />

Arquivos e pastas

Item Padrão Exemplo
Pasta de componente PascalCase FormField/
Componente PascalCase.tsx FormField.tsx
Estilos PascalCase.styles.ts FormField.styles.ts
Barrel index.ts —
Hook camelCase.ts useCurioMutation.ts
Utilitário camelCase.ts formatters.ts
Tipos de uma tela interfaces.ts service/interfaces.ts
Hooks de uma tela hooks.ts service/hooks.ts
Constantes de RM/UC constants.ts service/constants.ts
Schemas de uma tela schemas.ts —
Página PascalCasePage.tsx IncluirFornecedorPage.tsx

Páginas levam sufixo Page; componentes não. Deixa óbvio, no import, o que é rota e o que é peça.

Hooks

Tipo Padrão Exemplo
Query de coleção use{Entidade}s useFornecedores
Query de item use{Entidade} useFornecedor
Mutation de caso de uso use{Verbo}{Entidade} useSalvaFornecedor
Mutation genérica use{Verbo}{Entidade}Mutation useCreateFornecedorMutation
Utilitário use{Descritor} useDebounce, useModal

Hooks de caso de uso usam verbo em português, espelhando o request do backend:

RM_INCLUI_OBJETO    → useIncluiFornecedor
RM_SALVA_OBJETO     → useSalvaFornecedor
RM_OBTEM_DOCUMENTOS → useObtemDocumentos

Isso torna rastreável qual hook corresponde a qual request sem abrir o arquivo.

Handlers e callbacks

interface Props {
  onSubmit: (data: FormData) => void; // prop → on{Ação}
  onCancel: () => void;
}

const Componente = ({ onSubmit }: Props) => {
  const handleFormSubmit = (data: FormData) => {
    // interno → handle{Ação}
    onSubmit(data);
  };
};

on* é o que o componente recebe. handle* é o que ele define. Nunca inverta — a distinção diz, na leitura, de onde vem o comportamento.

Constantes

// Módulo, imutável, conhecido em tempo de escrita → SCREAMING_SNAKE_CASE
const FORNECEDOR_RMS = { USE_CASE: "4821", SALVAR: "RM_SALVAR_DADOS_FORNECEDOR" };
export const STORAGEKEY = "br.com.nomedoprojeto";

// Objeto de configuração / default → camelCase
const defaultFormValues = { _Nome: "", _Ativo: true };
const fornecedorKeys = { all: ["fornecedores"] as const };

Ids de caso de uso e nomes de request sempre dentro de um objeto _RMS — ver 06 — nunca literal no JSX.

Tipos e interfaces

Comentários

Comentário em código segue estilo ultra-comprimido (telegráfico), não prosa:

// ERRADO
// This function is responsible for grouping the routes by their layout so that
// we can render one parent Route per layout.

// CERTO
// agrupa por layout pra montar uma <Route> pai por layout

Comente por quê, não o quê. O código já diz o quê.

Quando um eslint-disable for inevitável, justifique na mesma linha:

// eslint-disable-next-line @typescript-eslint/no-explicit-any -- @curio nao tipa o service
constructor(service: any, driver: RequestDriver) {

-- seguido do motivo é obrigatório. eslint-disable sem justificativa não passa em revisão.

Imports

Ordem, com linha em branco entre grupos:

// 1. React e libs externas
import React, { useEffect, useState } from "react";
import { Box, Button } from "@mui/material";

// 2. Aliases internos
import { useAuth } from "@context/AuthProvider";
import { FormField } from "@components/common";

// 3. Relativos da própria feature
import { useSalvaFornecedor } from "./service/hooks";
import { containerStyles } from "./IncluirFornecedorPage.styles";

Use aliases para cruzar pastas (@components/...), relativos dentro da própria feature (./service/...). Nunca ../../../hooks/useX — para isso existe @hooks.

Anti-padrões recorrentes

Implementações reais tendem a acumular as mesmas inconsistências. A coluna "Padrão correto" é o que o projeto novo adota sempre — nunca replique a coluna da esquerda.

Anti-padrão Padrão correto
Duas pastas para o mesmo domínio em idiomas diferentes (Documentos/ e Documents/) Um idioma só para nomes de pasta de página. Adote português
Constantes de rota misturando idiomas (PATHS.DOCUMENTS.* ao lado de PATHS.DOCUMENTOS.*) Um idioma só nas URLs. Adote português
CLAUDE.md/guia descreve uma pasta de requests que não existe mais no código Requests por tela em pages/<Tela>/service/ — 03
Documentação diz uma porta; vite.config.ts usa outra Documente a porta real, confira contra o código
Documentação cita uma STORAGEKEY de exemplo; o código usa outra Chave própria do projeto, documentada corretamente
Documentação diz sessionStorage; o código de logout limpa localStorage (ou vice-versa) Um storage só, consistente — 05
Logout faz localStorage.clear() (apaga tudo, não só a sessão) Remover só a chave da sessão
Barrel (index.ts) desatualizado, não exporta componente/módulo já existente Barrel atualizado no mesmo commit do componente
Campo de config tipado como number, mas o JSON de runtime entrega string Tipar como string — 04
Rota como string literal ("dashboard") em vez da constante (PATHS.DASHBOARD) Sempre a constante
Código de debug (<Profiler>, console.log) atrás de uma env var que nunca é definida Remover código morto; sem eslint-disable decorativo
Script de setup de ambiente loga o valor de um token/segredo Nunca logar segredo
Arquivo de config de ambiente com token real commitado Placeholder no repo; valor via .env — 04
Detecção de expiração de sessão comparando string de mensagem de erro Sinalização por código de erro em isAuthError
login()/connect() retornam o erro em vez de lançar Deixar lançar e tratar no hook
Estilos num objeto styles = { root, drawer, ... } por arquivo Constantes nomeadas exportadas individualmente — ver 12

Se você encontrar mais algum ao portar código, acrescente aqui em vez de resolver em silêncio.

14 — Qualidade e pré-commit

14 — Qualidade e pré-commit

Três gates: vite-plugin-checker durante o dev, pre-commit no commit, npm run build no build. Não há suíte de testes — assumido e declarado, não esquecido. Regra: o gate corrige o que dá para corrigir e barra o que não dá.

Os gates

Quando O que roda Barra?
npm run dev vite-plugin-checker (type-check contínuo) Não — mostra overlay
git commit Node version, flags de debug, lint-staged Sim
git pull post-merge → npm install se preciso Não
npm run build lint → tsc → vite build Sim

Ausência de testes — declarado

Este padrão não tem suíte de testes automatizados. Não há Vitest, Jest, Testing Library nem Playwright. Isso não é omissão da documentação: é o estado da referência.

O que substitui:

Se o projeto novo quiser testes, decida no início. Adicionar depois exige refatorar componentes que nasceram acoplados a Context e ao UseCaseManager. Se adotar, atualize 19 para incluir o comando na Definition of Done.

ESLint

Flat config (eslint.config.js) — ESLint 9+ abandonou o formato .eslintrc.json. Não é uma escolha do guia, é obrigatório a partir da v9; --ext ts,tsx também deixou de ser aceito no CLI (o escopo de arquivos agora vem do files de cada bloco de config).

// eslint.config.js
import js from "@eslint/js";
import tsPlugin from "@typescript-eslint/eslint-plugin";
import tsParser from "@typescript-eslint/parser";
import reactHooks from "eslint-plugin-react-hooks";
import prettier from "eslint-config-prettier";
import globals from "globals";

export default [
  { ignores: ["dist", "build", "vite.config.ts", "setEnvironment.js"] },
  js.configs.recommended,
  {
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      parser: tsParser,
      parserOptions: { ecmaVersion: "latest", sourceType: "module" },
      globals: { ...globals.browser, ...globals.node, ...globals.es2020 }
    },
    plugins: { "@typescript-eslint": tsPlugin, "react-hooks": reactHooks },
    rules: {
      ...tsPlugin.configs.recommended.rules,
      ...reactHooks.configs.recommended.rules,
      "no-unused-vars": "off",
      "no-undef": "off",
      "@typescript-eslint/no-unused-vars": "error",
      "react-hooks/exhaustive-deps": "warn",
      "no-debugger": "error",
      "no-console": ["error", { allow: ["warn", "error", "info"] }],
      "@typescript-eslint/no-explicit-any": "error",
      "@typescript-eslint/no-empty-interface": "error",
      "no-shadow": "off",
      "@typescript-eslint/no-shadow": "error",
      "no-throw-literal": "error",
      "no-return-await": "error",
      "no-duplicate-imports": "error",
      eqeqeq: ["error", "always"],
      "no-var": "error",
      "prefer-const": "error",
      "use-isnan": "error"
    }
  },
  prettier
];

Requer @eslint/js e globals como devDependencies novas (não existiam no formato antigo).

prettier (de eslint-config-prettier) por último desliga as regras de formatação do ESLint — Prettier manda no formato, ESLint manda na correção.

no-undef: "off" não é opcional. O @typescript-eslint/recommended do formato antigo (.eslintrc.json) desligava no-undef internamente ao resolver o extends; pegando o objeto de regras direto (tsPlugin.configs.recommended.rules) em flat config, esse desligamento não vem junto — sem repetir explicitamente, no-undef acusa falso positivo em tipos ambient do TS (EventListener, React usado só como tipo em .ts). O tsc já cobre isso; deixe o ESLint fora do caminho.

Regras que merecem explicação

Regra Efeito prático
no-console (permite warn/error/info) console.log esquecido barra o commit
@typescript-eslint/no-explicit-any any exige eslint-disable justificado
react-hooks/exhaustive-deps: warn Ver abaixo
eqeqeq == proibido
no-shadow Variável interna não pode mascarar externa

exhaustive-deps como "warn", não "off". Os useEffect de abertura de caso de uso (06) dependem de rodar em condições específicas, e a regra reclamaria de arrays intencionalmente incompletos — resolva com // eslint-disable-next-line justificado nesses poucos casos, não desligando a regra inteira.

eslint-plugin-react-hooks v7 trouxe regras novas — decida o alcance

Isso pega padrões legítimos de código já existente: estado 100% derivado de outro estado/prop que era sincronizado via useEffect. A correção correta não é suprimir a regra, é aplicar o padrão que o React recomienda:

--max-warnings 0

eslint . --report-unused-disable-directives --max-warnings 0

Warning é erro. Sem isso, warnings acumulam até ninguém mais ler a saída. --report-unused-disable-directives acusa eslint-disable que não suprime mais nada — remove supressão obsoleta. Sem --ext: em flat config o CLI não aceita mais essa flag, o escopo de arquivos vem do files de cada bloco em eslint.config.js.

Prettier

// .prettierrc
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "none",
  "printWidth": 120,
  "arrowParens": "always",
  "endOfLine": "crlf",
  "bracketSpacing": true,
  "bracketSameLine": true,
  "proseWrap": "preserve"
}
# .prettierignore
node_modules
dist
build
coverage

# Gerado em runtime por setEnvironment.js
public/config.json

package-lock.json

Dois pontos não-óbvios:

husky

Instalação

// package.json — repositório de módulo único
"scripts": { "prepare": "husky" }
// package.json — projeto web dentro de um monorepo
"scripts": { "prepare": "cd ../.. && husky caminho/do/modulo/.husky" }

A forma do monorepo instala os hooks apontando para .husky/ dentro do módulo web, enquanto o core.hooksPath é configurado na raiz do repositório.

Armadilha do monorepo. core.hooksPath é config local do clone, mas .husky/ é versionado. Trocar de branch pode ligar ou desligar os hooks em silêncio. Depois de clonar ou de trocar para uma branch que mexe em package.json, rode npm run prepare e confirme com git config core.hooksPath.

pre-commit

# .husky/pre-commit
# git roda o hook da raiz do repo; projeto Node pode estar em subpasta
hookdir="$(cd "$(dirname -- "$0")" && pwd)"
cd "$hookdir/.." || exit 1

# valida Node sem depender de fnm no shell
. "$hookdir/check-node-version.sh"
. "$hookdir/check-debug-flags.sh"

npx lint-staged

Três verificações, nesta ordem: versão de Node → flags de debug → lint-staged.

check-node-version.sh

# .husky/check-node-version.sh
# Sourced pelo hook, ja com cwd no projeto Node. Nao depende de fnm/nvm
# estarem carregados (GUIs de git nao herdam o profile) -- compara o node
# do PATH com .node-version (aceita >=).
expected_version=$(tr -d '[:space:]' < .node-version 2>/dev/null)
actual_version=$(node -v 2>/dev/null | sed 's/^v//')

if [ -z "$actual_version" ]; then
  echo "hook: 'node' nao encontrado no PATH. Ative o Node >= ${expected_version:-do projeto} (ex: 'fnm use')."
  exit 1
fi

if [ -n "$expected_version" ]; then
  lower_version=$(printf '%s\n%s\n' "$expected_version" "$actual_version" | sort -V | head -n1)
  if [ "$lower_version" != "$expected_version" ]; then
    echo "hook: Node v$actual_version encontrado, necessario >= v$expected_version (.node-version)."
    exit 1
  fi
fi

Resolve um problema real: commitar por GUI de Git com Node 8 ativo faz o lint-staged falhar com erro incompreensível.

check-debug-flags.sh

# .husky/check-debug-flags.sh
# Bloqueia commit com enableLogs: true (flag de depuracao de useUseCaseControls).
#
# So chama 'exit' no caminho de erro -- este arquivo e' sourced, entao
# 'exit 0' encerraria o hook antes do lint-staged rodar.

repo_root=$(git rev-parse --show-toplevel)
staged_files=$(git diff --cached --name-only --diff-filter=ACM -- '*.ts' '*.tsx')

if [ -n "$staged_files" ]; then
  matches=""
  for file in $staged_files; do
    file_match=$(grep -n "enableLogs:[[:space:]]*true" "$repo_root/$file" 2>/dev/null || true)
    if [ -n "$file_match" ]; then
      matches="$matches$file:
$file_match
"
    fi
  done

  if [ -n "$matches" ]; then
    echo "hook: 'enableLogs: true' encontrado em arquivo(s) staged - remova antes de commitar:" >&2
    printf '%s' "$matches" >&2
    exit 1
  fi
fi

O comentário sobre exit 0 não é decorativo. O arquivo é carregado com . (source). Um exit 0 no fim encerraria o pre-commit inteiro antes do lint-staged, e o commit passaria sem lint. Ao escrever um hook novo neste estilo, só chame exit no caminho de erro.

Adapte a lista de flags ao projeto: qualquer flag de depuração que não deva ser commitada entra aqui.

post-merge

# .husky/post-merge
# Instala dependencias quando o merge/pull alterou package.json/lock.
changed=$(git diff-tree -r --name-only --no-commit-id ORIG_HEAD HEAD)

case "$changed" in
  *caminho/do/modulo/package-lock.json*|*caminho/do/modulo/package.json*)
    echo "post-merge: dependencias mudaram -> rodando npm install..."
    hookdir="$(cd "$(dirname -- "$0")" && pwd)"
    cd "$hookdir/.." || exit 1
    . "$hookdir/check-node-version.sh"
    npm install
    ;;
esac

Elimina a classe de bug "puxei a branch e o app não sobe". Ajuste os caminhos do case ao layout do projeto novo.

lint-staged

// package.json
"lint-staged": {
  "src/**/*.{ts,tsx}": [
    "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
    "prettier --write ."
  ],
  "**/*.{json,css,scss,md,html,yml,yaml}": ["prettier --write ."]
}

Duas imprecisões comuns nesta configuração, que valem corrigir.

  1. Os comandos usam . (projeto inteiro) em vez dos arquivos staged. O lint-staged passa a lista de arquivos como argumento, que aqui é ignorada. Funciona, mas fica lento e formata arquivos que você não tocou.
  2. prettier --write . faz o hook reescrever e re-stagear arquivos. Um commit pode sair com formatação que você não fez.

Forma correta:

"lint-staged": {
  "src/**/*.{ts,tsx}": [
    "eslint --fix --report-unused-disable-directives --max-warnings 0",
    "prettier --write"
  ],
  "**/*.{json,css,scss,md,html,yml,yaml}": ["prettier --write"]
}

Sem ., o lint-staged anexa só os arquivos staged.

vite-plugin-checker

checker({
  typescript: true,
  overlay: { initialIsOpen: false, position: "tl" },
  terminal: true
});

Type-check contínuo durante npm run dev, em overlay e no terminal. Não substitui tsc no build — o npm run build roda tsc separadamente, porque o checker só verifica o que foi tocado na sessão.

Contornar os gates

git commit --no-verify pula os hooks. Reserve para emergência real (commit de WIP em branch pessoal). Nunca em branch compartilhada.

HUSKY=0 desabilita os hooks no ambiente — útil em CI, onde o pipeline já roda lint e build.

15 — Build e deploy

15 — Build e deploy

Builds por ambiente, o artefato gerado e como ele é servido atrás do ISAPI. A escolha de ambiente acontece em build (qual config.json é embarcado) e pode ser trocada em deploy (substituindo o arquivo) — sem recompilar. Não há pipeline de CI na referência. Isso é lacuna, não decisão.

Scripts

Comando O que faz
npm run dev env:dev + Vite dev server (porta 5000)
npm run start:homolog env:homolog + dev server apontando para homologação
npm run build lint → env:prod → tsc → vite build
npm run build:homolog env:homolog → tsc → vite build
npm run preview Serve o dist/ localmente

O build de produção inclui o lint

"build": "npm run lint && npm run env:prod && tsc && vite build"

Quatro etapas, sequenciais, qualquer falha aborta:

  1. lint — ESLint com zero warnings
  2. env:prod — gera public/config.json a partir de config/prod.json
  3. tsc — type-check completo (noEmit)
  4. vite build — bundle em dist/

build:homolog não roda o lint. É inconsistente com o build. No projeto novo, inclua o lint nos dois: "build:homolog": "npm run lint && npm run env:homolog && tsc && vite build".

Por que tsc além do vite-plugin-checker

O checker do dev só verifica o que foi tocado na sessão. tsc verifica o projeto inteiro. Um erro de tipo num arquivo que ninguém abriu passa pelo checker e é pego aqui.

O artefato

dist/
├── index.html
├── config.json          ← copiado de public/, define o backend
└── assets/
    ├── index-<hash>.js
    ├── index-<hash>.css
    └── <Pagina>-<hash>.js   ← um chunk por página (lazy)
build: { outDir: "dist", sourcemap: true }

sourcemap: true publica os .map junto do bundle — qualquer um com acesso à URL lê o código-fonte original. Aceitável em sistema interno; desligue se o app for exposto na internet.

Trocar de ambiente sem rebuild

Como o config.json é lido em runtime (fetch("./config.json") — ver 04), o mesmo dist/ serve qualquer ambiente:

# aponta um build existente para outro backend
cp config/homolog.json dist/config.json

Isso viabiliza promover exatamente o artefato testado em homologação para produção, sem recompilar.

Consequência: o config.json do build não é a verdade final. Confirme o que está no servidor, não o que foi buildado.

Deploy ISAPI

O backend Curio é servido por cxIsapiClient.dll sob IIS. O front é um SPA estático publicado no mesmo IIS, normalmente num diretório virtual ao lado do gateway.

Passos:

  1. npm run build (ou build:homolog)
  2. Copiar o conteúdo de dist/ para o diretório publicado no IIS
  3. Ajustar dist/config.json se o destino diferir do ambiente do build
  4. Validar: abrir a aplicação, fazer login, executar uma tela que chame o backend

Rewrite para SPA

O React Router usa BrowserRouter (history API). Uma URL profunda como /cadastro/fornecedor/incluir chega ao IIS como um caminho que não existe em disco — e retorna 404.

O IIS precisa de uma regra de rewrite que devolva index.html para qualquer caminho que não seja arquivo real:

<!-- web.config no diretório publicado -->
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="SPA fallback" stopProcessing="true">
          <match url=".*" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/index.html" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Sintoma de rewrite ausente: a aplicação funciona navegando pelo menu, mas F5 numa tela interna dá 404.

É comum uma implementação de referência não versionar um web.config. Se o projeto novo for publicado em IIS, versione-o junto do código — configuração de servidor não deve viver só na memória de quem publica.

base do Vite

Se a aplicação for servida em subcaminho (https://servidor/sistema/ em vez da raiz), configure:

// vite.config.ts
export default defineConfig({ base: "/sistema/" });

Sem isso, os assets/ são requisitados da raiz e não carregam. A referência assume publicação na raiz (base não é definido).

CI/CD — o que existe e o que falta

É comum encontrar .gitlab/ na raiz só com templates de issue e merge request:

.gitlab/
├── issue_templates/default.md
└── merge_request_templates/default.md

Sem .gitlab-ci.yml, não há pipeline. Se o template de MR pede "Pipeline passando" e "Testes escritos ou atualizados" sem que o projeto tenha como cumprir nenhum dos dois, é checklist aspiracional — alinhe o texto ao que existe de verdade.

Recomendação para o projeto novo

Um pipeline mínimo cobre o que hoje depende de disciplina individual:

# .gitlab-ci.yml
image: node:22

stages: [verify, build]

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths: [node_modules/]

verify:
  stage: verify
  script:
    - npm ci
    - npm run lint
    - npx tsc --noEmit
    - npm run format:check

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths: [dist/]
    expire_in: 1 week

Ajuste os caminhos se o projeto web for submódulo de um monorepo (cd caminho/do/modulo antes dos comandos).

Se adotar CI, alinhe o checklist do template de MR ao que o pipeline realmente verifica. Checklist que pede o que não existe treina o time a marcar caixas sem ler.

Antes de publicar

16 — Template de `CLAUDE.md`

16 — Template de CLAUDE.md

Arquivo pronto para colar na raiz do projeto novo. Substitua tudo entre {{ }} e apague o que não se aplicar. Inclui a seção de harness — ver 18 e 19.

Como manter

O CLAUDE.md é carregado automaticamente em toda sessão. Ele é índice, não enciclopédia: aponta para os guias, não os duplica. Duplicar cria duas versões da verdade — é assim que um CLAUDE.md diverge do código com o tempo (ver anti-padrões em 13).

Regra: se um fato pode mudar sem que ninguém lembre de atualizar o CLAUDE.md, ele não deveria estar aqui — deveria estar num guia, referenciado daqui.


Template

# CLAUDE.md

Guia para o Claude Code (claude.ai/code) trabalhar neste repositório.

Projeto: **{{NOME_DO_PROJETO}}** — SPA React que fala com backend Curio via `@curio/client`.
Stack, arquitetura e convenções detalhadas vivem em `harness/guides/` — ver tabela abaixo.

---

## Comandos

```bash
npm run dev              # ambiente dev + Vite (porta {{PORTA}})
npm run start:homolog    # ambiente de homologação
npm run build            # lint + tsc + build de produção
npm run lint             # ESLint, zero warnings
npm run format           # Prettier
npx tsc --noEmit         # type-check completo
```

Node >= {{VERSAO_NODE}} (fixado em `.node-version`). Ative antes de qualquer comando — o `pre-commit`
valida e barra o commit se estiver errado.

**Não há suíte de testes.** A verificação é `lint` + `tsc` + `build` + teste manual da tela contra o
backend real. Não afirme que algo "passou nos testes".

---

## Guide Files

Toda a documentação de harness vive em `harness/`. `CLAUDE.md` e `init.sh` ficam na raiz — são os
pontos de entrada.

```
harness/
├── guides/                   — referência técnica — versionado
├── _examples/
│   ├── state/                 — feature_list.example.json, progress.example.md, session-handoff.example.md
│   ├── handoffs/               — handoff.example.md
│   └── user_preferences.example.md
├── <branch>/                 — tudo que a branch produziu — VERSIONADO
│   ├── state/                  — feature_list.json, progress.md, session-handoff.md
│   ├── plans/                   — planos de implementação
│   ├── specs/                    — specs de design
│   └── handoffs/                  — handoffs de sessão
└── user_preferences.md       — preferências do dev — ÚNICO ARQUIVO LOCAL (gitignored)
```

`<branch>` é o nome literal da branch atual (`git rev-parse --abbrev-ref HEAD`). **Só
`harness/user_preferences.md` é local.** Tudo dentro de `harness/<branch>/` é versionado — se
`harness/<branch>/state/` não existir ainda, **crie a pasta a partir de `_examples/`** (mecânico); não
invente conteúdo de `feature_list.json`/`progress.md`.

### Guias técnicos

- `harness/guides/commands.md` — build, run, ambientes, variáveis
- `harness/guides/architecture.md` — camadas do front, fluxo até o backend, autenticação
- `harness/guides/curio-framework-guide.md` — sessão, caso de uso, `useCurioMutation`, React Query
- `harness/guides/dev-environment.md` — Node/fnm, `.env`, acesso aos servidores
- `harness/guides/husky-git-hooks.md` — o que cada hook faz e como configurar
- `harness/guides/gitlab-access.md` — autenticação com a API do GitLab {{REMOVER_SE_NAO_USA}}

### Estado de features

- `harness/<branch>/plans/*.md` — planos task-by-task por feature
- `harness/<branch>/specs/*.md` — specs de design
- `harness/<branch>/handoffs/*.md` — handoffs por feature (versionado)

Quando existir plano/spec da feature ativa, ele tem precedência sobre o `feature_list.json` genérico
para o detalhe fino — mas atualize os dois ao final da sessão. Um plano/spec fica na branch onde
nasceu — não realoque se o trabalho continuar em outra branch depois.

### Sem histórico compartilhado entre branches

Não existe mais `global_feature_list.json`. Cada branch é autocontida — decisão deliberada em troca de
simplicidade (ver [18](18-HARNESS.md#reestruturação-2026-08-25-por-que-branch-primeiro)). Não recrie
esse arquivo.

---

## Harness — Estado, Verificação e Módulos

### Startup Workflow

Antes de escrever código:

1. Confirme o diretório com `pwd`.
2. Resolva a branch atual (`git rev-parse --abbrev-ref HEAD`). Se `harness/<branch>/state/` não
   existir, crie a partir de `harness/_examples/` (scaffolding — não preencha conteúdo).
3. Leia `harness/user_preferences.md` (se existir).
4. Leia `harness/<branch>/state/feature_list.json` e `harness/<branch>/state/progress.md` (se existirem).
5. Revise commits recentes: `git log --oneline -5`.
6. Rode `./init.sh` (FAST por padrão; `FULL=1 ./init.sh` para a suíte completa).

Se o baseline já estiver quebrado, corrija antes de implementar feature nova.

### Interação com o usuário

Ao levantar decisões ou pontos em aberto, **faça uma pergunta por vez** e espere a resposta antes da
próxima. Não empacote várias perguntas numa lista só.

### Convenções de código

Comentários em código seguem estilo ultra-comprimido (telegráfico), não prosa. Comente **por quê**,
não **o quê**. Ver `harness/guides/` e a seção de convenções da documentação do projeto.

### Módulos (fonte de verdade)

| Módulo           | Path       | Doc                                       | Verificação                                          |
| ---------------- | ---------- | ----------------------------------------- | ---------------------------------------------------- |
| Frontend (React) | `{{PATH}}` | `harness/guides/curio-framework-guide.md` | `npm run lint` (rápido) / `npm run build` (completo) |
| Configuração     | `config/`  | `harness/guides/commands.md`              | `npm run env:dev` gera `public/config.json`          |

Antes de editar um módulo, leia o guia correspondente.

**Nunca editar à mão:** `public/config.json` — é gerado por `setEnvironment.js` a partir de
`config/{env}.json`. Toda mudança de configuração vai no arquivo de origem.

### Definition of Done

Uma tarefa só está concluída quando:

- o comportamento alvo foi implementado;
- `./init.sh` passa (ao menos FAST; FULL antes de considerar pronto para revisão);
- a tela foi **aberta no browser** e o fluxo verificado contra o backend real — sem suíte de testes,
  esta é a única evidência funcional que existe;
- `harness/<branch>/state/feature_list.json` foi atualizado (`status` + `evidence`);
- `harness/<branch>/state/progress.md` foi atualizado ao fim da sessão.

Não marque como `passing` só porque o código foi escrito.

### Fim de sessão

1. Atualizar `harness/<branch>/state/feature_list.json` (status + evidência); se algo chegou a
   `passing`, mover de `features` para `completed` no mesmo arquivo.
2. Atualizar `harness/<branch>/state/progress.md`.
3. Atualizar `harness/<branch>/state/session-handoff.md` com o resultado da verificação.
4. Commit descritivo com o repo em estado seguro — inclui `state/`, `plans/`, `specs/` e `handoffs/`
   da branch, tudo versionado.
5. A próxima sessão deve conseguir rodar `./init.sh` imediatamente.

---

## Arquitetura — resumo

```
Componente → hook de caso de uso → useCurioMutation → UseCaseManager → @curio/client → Backend
```

- **Não é REST.** Casos de uso por id numérico, requests nomeados (`RM_SALVA_OBJETO`).
- **Erro é global.** `queryClient` captura e notifica; páginas não fazem `try/catch` para exibir erro.
- **Config em runtime.** `public/config.json` é lido por `fetch`, não embutido no bundle.
- **Propriedades do backend:** `_Prefixo` para primitivos, sem prefixo para objetos; nomes em português.

Detalhe completo em `harness/guides/curio-framework-guide.md`.

---

## Aliases

| Alias         | Path              |
| ------------- | ----------------- |
| `@`           | `src/`            |
| `@components` | `src/components/` |
| `@pages`      | `src/pages/`      |
| `@hooks`      | `src/hooks/`      |
| `@utils`      | `src/utils/`      |
| `@types`      | `src/types/`      |
| `@api`        | `src/api/`        |
| `@context`    | `src/context/`    |
| `@theme`      | `src/theme/`      |

Todo alias novo entra em `vite.config.ts` **e** `tsconfig.json`.

Se o projeto já tem um CLAUDE.md

Insira a seção de harness nele, não sobrescreva. Leia o arquivo inteiro antes, para não duplicar uma seção "Harness" criada numa tentativa anterior.

17 — Primeira tela

17 — Primeira tela

Receita end-to-end: uma tela de busca + cadastro de Fornecedor, tocando todas as camadas. Copie, troque Fornecedor pela sua entidade e o id do caso de uso. Ao final há um checklist de verificação — a tela só está pronta quando ele passa.

O que vamos construir

Uma tela que:

  1. Abre um caso de uso no backend
  2. Busca fornecedores por filtro
  3. Exibe o resultado em tabela
  4. Salva um fornecedor novo

Camadas tocadas: constants → interfaces → hooks → schemas → página → paths → routes → menuTree.

Pré-requisitos

Do backend, você precisa saber:

Não invente. Pergunte a quem implementou o caso de uso ou descubra com enableLogs (06).

Sem esse contrato ainda? Construa com placeholders

Se o backend real não existe ou ainda não foi definido, não pule esta receita — construa-a mesmo assim, com todo id de caso de uso e nome de RM como placeholder explícito ("SUBSTITUA_USE_CASE_ID", "RM_SUBSTITUA_SALVAR"), numa entidade que não possa ser confundida com domínio real (Exemplo, não Fornecedor). Isso é o passo 11 do bootstrap (02) — prova, com tsc/lint/build reais, que a cadeia inteira compila e roteia antes de qualquer feature de negócio existir, e dá ao time algo executável para copiar. Comente no topo do arquivo que é placeholder, registre a rota normalmente, e apague quando a primeira tela de negócio nascer.

1. Constantes

// src/pages/Cadastro/Fornecedor/service/constants.ts
export const FORNECEDOR_RMS = {
  USE_CASE: "4821",
  OBTEM_DADOS: "RM_INCLUI_OBJETO",
  BUSCAR: "RM_BUSCA_FORNECEDORES",
  SALVAR: "RM_SALVA_OBJETO"
};

Ver 06. Nenhum id ou nome de request literal fora deste arquivo.

2. Interfaces

// src/pages/Cadastro/Fornecedor/service/interfaces.ts

export interface FornecedorXML {
  _OID: string;
  _Nome: string;
  _CNPJ: string;
  _Ativo: boolean;
  TipoFornecedor?: TipoFornecedorXML;
}

export interface TipoFornecedorXML {
  _OID: string;
  _Titulo: string;
}

// abertura: backend devolve objeto novo + listas de apoio
export interface FornecedorInicialResponse {
  Fornecedor: FornecedorXML;
  TiposFornecedor: TipoFornecedorXML[];
}

export interface BuscaFornecedoresRequest {
  OBJECTID: {
    _Nome: string;
    _Ativo: boolean;
  };
}

export interface BuscaFornecedoresResponse {
  Response: FornecedorXML[];
}

export interface SalvaFornecedorRequest {
  Fornecedor: {
    _OID: string;
    _Nome: string;
    _CNPJ: string;
    TipoFornecedor?: { _OID: string };
  };
}

3. Hooks de caso de uso

// src/pages/Cadastro/Fornecedor/service/hooks.ts
import { useCurioMutation } from "@/hooks/useCurioMutation";
import { FORNECEDOR_RMS } from "./constants";
import {
  BuscaFornecedoresRequest,
  BuscaFornecedoresResponse,
  FornecedorInicialResponse,
  SalvaFornecedorRequest
} from "./interfaces";

export const useIncluiFornecedor = () => useCurioMutation<FornecedorInicialResponse, void>(FORNECEDOR_RMS.OBTEM_DADOS);

export const useBuscaFornecedores = () =>
  useCurioMutation<BuscaFornecedoresResponse, BuscaFornecedoresRequest>(FORNECEDOR_RMS.BUSCAR);

export const useSalvaFornecedor = () => useCurioMutation<void, SalvaFornecedorRequest>(FORNECEDOR_RMS.SALVAR);

4. Schemas

// src/pages/Cadastro/Fornecedor/schemas.ts
import { z } from "zod";

export const fornecedorSchema = z.object({
  _Nome: z.string().min(1, "Informe o nome").max(120, "Máximo de 120 caracteres"),
  _CNPJ: z
    .string()
    .min(1, "Informe o CNPJ")
    .regex(/^\d{2}\.\d{3}\.\d{3}\/\d{4}-\d{2}$/, "CNPJ inválido"),
  _TipoFornecedor: z.string().min(1, "Selecione o tipo")
});

export type FornecedorFormData = z.infer<typeof fornecedorSchema>;

export const filtroFornecedorSchema = z.object({
  _Nome: z.string(),
  _Ativo: z.boolean()
});

export type FiltroFornecedorData = z.infer<typeof filtroFornecedorSchema>;

5. Estilos

// src/pages/Cadastro/Fornecedor/FornecedorPage.styles.ts
import { SxProps, Theme } from "@mui/material";

export const containerStyle: SxProps<Theme> = {
  display: "flex",
  flexDirection: "column",
  gap: 2,
  p: 3
};

export const headerBarStyle: SxProps<Theme> = {
  display: "flex",
  justifyContent: "flex-end",
  gap: 1
};

export const filtroStyle: SxProps<Theme> = {
  display: "flex",
  gap: 2,
  alignItems: "flex-start",
  flexWrap: "wrap"
};

export const formSectionStyle: SxProps<Theme> = {
  display: "grid",
  gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" },
  gap: 2
};

Nomes de constante nomeada, não um objeto styles — ver 12.

6. A página

// src/pages/Cadastro/Fornecedor/FornecedorPage.tsx
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Box, Button, Divider, Typography } from "@mui/material";
import { Save, Search } from "@mui/icons-material";
import { UseCaseManager } from "@curio/client/react";

import { useAuth } from "@context/AuthProvider";
import { useUseCaseControls, useValidatedForm } from "@hooks/index";
import { DataTable, FormField, SelectInput, type Column, type SelectOption } from "@components/common";

import {
  fornecedorSchema,
  filtroFornecedorSchema,
  type FornecedorFormData,
  type FiltroFornecedorData
} from "./schemas";
import { FORNECEDOR_RMS } from "./service/constants";
import { useBuscaFornecedores, useIncluiFornecedor, useSalvaFornecedor } from "./service/hooks";
import type { FornecedorXML } from "./service/interfaces";
import { containerStyle, filtroStyle, formSectionStyle, headerBarStyle } from "./FornecedorPage.styles";

const colunas: Column<FornecedorXML>[] = [
  { id: "_Nome", label: "Nome", minWidth: 200 },
  { id: "_CNPJ", label: "CNPJ", minWidth: 160 },
  { id: "_Ativo", label: "Ativo", align: "center", format: (value) => (value ? "Sim" : "Não") }
];

const FornecedorContent: React.FC = () => {
  const { open, status } = useUseCaseControls();
  const [fornecedores, setFornecedores] = useState<FornecedorXML[]>([]);

  const { data: dadosIniciais, mutate: incluirFornecedor, isPending: isIniciando } = useIncluiFornecedor();
  const { data: resultadoBusca, mutate: buscarFornecedores, isPending: isBuscando } = useBuscaFornecedores();
  const { mutateAsync: salvarAsync, isPending: isSalvando } = useSalvaFornecedor();

  // refs impedem reabrir/reinicializar a cada render
  const hasAttemptedOpenRef = useRef(false);
  const hasInitializedRef = useRef(false);

  // maquina de estados: idle -> abre; open -> inicializa
  useEffect(() => {
    if (status === "idle" && !hasAttemptedOpenRef.current) {
      hasAttemptedOpenRef.current = true;
      open();
    } else if (status === "open" && !hasInitializedRef.current) {
      hasInitializedRef.current = true;
      incluirFornecedor();
    }
  }, [status, open, incluirFornecedor]);

  useEffect(() => {
    if (resultadoBusca) setFornecedores(resultadoBusca.Response ?? []);
  }, [resultadoBusca]);

  const tiposFornecedor: SelectOption[] = useMemo(
    () => (dadosIniciais?.TiposFornecedor ?? []).map((tipo) => ({ value: tipo._OID, label: tipo._Titulo })),
    [dadosIniciais]
  );

  const form = useValidatedForm({
    schema: fornecedorSchema,
    defaultValues: { _Nome: "", _CNPJ: "", _TipoFornecedor: "" }
  });

  const filtroForm = useValidatedForm({
    schema: filtroFornecedorSchema,
    defaultValues: { _Nome: "", _Ativo: true }
  });

  const handleBuscar = (data: FiltroFornecedorData) => {
    buscarFornecedores({ OBJECTID: { _Nome: data._Nome, _Ativo: data._Ativo } });
  };

  const handleSalvar = async (data: FornecedorFormData) => {
    await salvarAsync({
      Fornecedor: {
        _OID: String(dadosIniciais?.Fornecedor._OID ?? ""),
        _Nome: data._Nome,
        _CNPJ: data._CNPJ,
        TipoFornecedor: { _OID: data._TipoFornecedor }
      },
      msgSucesso: "Fornecedor salvo com sucesso!"
    });

    form.reset();
    incluirFornecedor(); // novo objeto pro proximo cadastro
    filtroForm.handleSubmit(handleBuscar)();
  };

  const isAnyLoading = isIniciando || isBuscando || isSalvando;

  return (
    <Box sx={containerStyle}>
      <Box sx={headerBarStyle}>
        <Button
          variant="contained"
          startIcon={<Save />}
          onClick={form.handleSubmit(handleSalvar)}
          disabled={isAnyLoading}>
          Salvar
        </Button>
      </Box>

      <Typography variant="h3">Cadastro de fornecedor</Typography>

      <Box sx={formSectionStyle}>
        <FormField name="_Nome" control={form.control} label="Nome" size="small" fullWidth />
        <FormField name="_CNPJ" control={form.control} label="CNPJ" size="small" fullWidth />
        <SelectInput
          name="_TipoFornecedor"
          control={form.control}
          label="Tipo"
          options={tiposFornecedor}
          size="small"
          fullWidth
        />
      </Box>

      <Divider />

      <Typography variant="h4">Fornecedores cadastrados</Typography>

      <Box sx={filtroStyle}>
        <FormField name="_Nome" control={filtroForm.control} label="Filtrar por nome" size="small" />
        <Button
          variant="outlined"
          startIcon={<Search />}
          onClick={filtroForm.handleSubmit(handleBuscar)}
          disabled={isBuscando}>
          Buscar
        </Button>
      </Box>

      <DataTable
        columns={colunas}
        data={fornecedores}
        loading={isBuscando}
        emptyMessage="Nenhum fornecedor encontrado."
        stickyHeader
        maxHeight={400}
      />
    </Box>
  );
};

const FornecedorPage: React.FC = () => {
  const { session } = useAuth();

  return (
    <UseCaseManager session={session} useCaseId={FORNECEDOR_RMS.USE_CASE} autoClose={false} openOnMount={false}>
      <FornecedorContent />
    </UseCaseManager>
  );
};

export default FornecedorPage;

7. Barrel da página

// src/pages/Cadastro/Fornecedor/index.ts
export { default } from "./FornecedorPage";

Obrigatório. Sem ele, o lazy(() => import("@pages/Cadastro/Fornecedor")) falha em runtime sem erro de compilação — o TypeScript não verifica o alvo do import dinâmico.

8. Caminho

// src/routes/paths.ts
export const PATHS = {
  LOGIN: "login",
  DASHBOARD: "dashboard",
  CADASTRO: {
    FORNECEDOR: "cadastro/fornecedor" // novo
  }
} as const;

9. Rota

// src/routes/routes.ts
export const routes: AppRoute[] = [
  // ...
  {
    path: PATHS.CADASTRO.FORNECEDOR,
    element: lazy(() => import("@pages/Cadastro/Fornecedor")),
    guard: "protected"
  }
];

10. Menu

// src/routes/menuTree.ts
export const menuTree: MenuNode[] = [
  { label: "Dashboard", path: PATHS.DASHBOARD, mode: "route" },
  {
    label: "Cadastro",
    children: [{ label: "Fornecedor", path: PATHS.CADASTRO.FORNECEDOR }] // modo default: "tab"
  }
];

Árvore final

src/pages/Cadastro/Fornecedor/
├── FornecedorPage.tsx
├── FornecedorPage.styles.ts
├── schemas.ts
├── index.ts
└── service/
    ├── constants.ts
    ├── hooks.ts
    └── interfaces.ts

Mais três arquivos tocados: routes/paths.ts, routes/routes.ts, routes/menuTree.ts.

Verificação

Sem suíte de testes, esta é a evidência. Rode tudo.

Estático

npm run lint && npx tsc --noEmit

Funcional — npm run dev, então:

Falha proposital

Só depois de tudo isso registre a feature como passing no harness/state/feature_list.json — ver 19.

Erros comuns

Sintoma Causa
Tela em branco, console: hook fora de contexto useCurioMutation no mesmo componente que renderiza o UseCaseManager
Select "Tipo" vazio Caso de uso não abriu; verifique status e o id
Import dinâmico falha em runtime Faltou index.ts na pasta da página
Item no menu leva a 404 ou "Rota não encontrada" Registrado em menuTree mas não em routes.ts
Rota duplica barra (//cadastro) PATHS com barra inicial
Requests repetidos em loop Faltou o useRef de guarda no useEffect
Backend recebe id/RM diferente do esperado Constante duplicada fora de service/constants.ts, divergindo do _RMS

18 — Harness

18 — Harness

Disciplina de sessão: guias versionados, estado por branch, tudo versionado exceto preferências pessoais do dev. Ao criar em projeto novo, invoque a skill anthropic-skills:curio-harness-bootstrap — ela é a fonte canônica dos templates.

Reestruturado em 2026-08-25 (branch-primeiro, sem global_feature_list.json) — ver nota de migração ao final.

O problema que isso resolve

Em projeto grande tocado por várias pessoas com Claude Code, três coisas quebram:

  1. Contexto se perde entre sessões. Cada sessão redescobre o que a anterior já sabia.
  2. Dois devs pisam no mesmo trabalho. Ninguém vê o que o outro está fazendo.
  3. "Está pronto" vira afirmação sem evidência. Código escrito é confundido com código verificado.

A solução não é framework — é convenção de arquivos. Guias versionados (o que o Claude precisa saber antes de mexer), estado de sessão por branch (sem conflito entre branches), e um init.sh que verifica baseline antes e depois.

Estrutura

Fica na raiz do repositório, ao lado de CLAUDE.md e init.sh. Não dentro de src/ nem de docs/.

projeto/
├── CLAUDE.md                      ponto de entrada (carregado automaticamente)
├── init.sh                        verificação de baseline — ver 19
└── harness/
    ├── guides/                    referência técnica — VERSIONADO
    ├── _examples/
    │   ├── state/                 templates: feature_list.example.json, progress.example.md,
    │   │                          session-handoff.example.md
    │   ├── handoffs/              template: handoff.example.md
    │   └── user_preferences.example.md
    ├── <branch>/                  tudo que a branch produziu — VERSIONADO
    │   ├── state/                 feature_list.json, progress.md, session-handoff.md
    │   ├── plans/                 planos de implementação (task-by-task)
    │   ├── specs/                 specs de design
    │   └── handoffs/              handoffs de sessão
    └── user_preferences.md        preferências do dev — ÚNICO ARQUIVO LOCAL (gitignored)

<branch> é o nome literal da branch (git rev-parse --abbrev-ref HEAD). Branch com / no nome vira subpasta naturalmente — feature/x → harness/feature/x/.

A distinção que importa

Versionado Local (gitignored)
guides/, _examples/, tudo dentro de <branch>/ user_preferences.md
Todo o trabalho de sessão, por branch Preferência pessoal do dev, não é da branch

Trocar de branch agora troca de estado automaticamente — git checkout já isola o harness/<branch>/ de qualquer outra. Não existe mais "estado local do dev": o estado é da branch, e é versionado com ela.

.gitignore

# Harness (Claude Code) — so preferencia pessoal e' local
harness/user_preferences.md

Todo o resto — guides/, _examples/, e tudo dentro de harness/<branch>/ — é versionado.

guides/ — quais escrever

Um projeto fullstack usa guias de backend além dos de frontend. Um projeto só de frontend usa um subconjunto.

Escrever

Guia Conteúdo
commands.md Build, run, ambientes, variáveis. Comandos reais, confirmados
architecture.md Camadas do front, fluxo até o backend, autenticação, roteamento
curio-framework-guide.md Sessão, caso de uso, useCurioMutation, React Query, tratamento de erro
dev-environment.md Node/fnm, .env, acesso aos servidores por ambiente
husky-git-hooks.md O que cada hook faz, como configurar, armadilhas de sh -e e source

Não escrever (são de backend)

curio-backend-syntax.md, curio-spring-boot-guide.md, staruml-tooling.md, test-data-scripts.md. Se o projeto novo tiver backend próprio, aí sim — mas isso está fora do escopo deste guia.

Condicionais

Regra ao escrever um guia

Investigue o código real. Não escreva de memória do que "Curio costuma ser" — pode ser outra versão do framework, outra convenção, outro layout. Se um comando não foi confirmado (rodado ou lido do package.json), não o documente.

<branch>/state/ — estado da branch

feature_list.json

{
  "project": "{{NOME_DO_PROJETO}}",
  "project_type": "frontend",
  "last_updated": "YYYY-MM-DD",
  "rules": {
    "passing_requires_evidence": true,
    "do_not_skip_verification": true
  },
  "status_legend": {
    "not_started": "Work has not begun.",
    "in_progress": "The feature is the current active task.",
    "blocked": "Work cannot continue until a documented blocker is resolved.",
    "passing": "Required verification has passed and evidence is recorded."
  },
  "features": [
    {
      "id": "exemplo-001",
      "type": "ui",
      "area": "nome-da-area",
      "title": "Título curto da feature",
      "behavior": "Descrição do comportamento esperado/pendência.",
      "verification": "Como confirmar que está correto (comando, tela, checklist).",
      "status": "not_started",
      "evidence": "Preenchido só quando houver evidência real de verificação."
    }
  ],
  "completed": []
}

Num projeto só de frontend, type é predominantemente "ui" — a verificação é abrir a tela e conferir o fluxo, não rodar teste.

O array features guarda o que ainda não é passing. Ao concluir, a entrada sai de features e entra em completed — histórico da branch, versionado, sem cópia para nenhum arquivo global.

progress.md

# Session Progress Log (versionado — por branch)

## Current State

**Last Updated:** YYYY-MM-DD
**Active Feature:** o que você está trabalhando agora

## Sessão YYYY-MM-DD (parte N) — título curto

- O que foi feito, decisões tomadas, bugs encontrados.
- Comandos de verificação rodados e resultado.
- Pendências para a próxima sessão.

## Status

### What's Done

- [ ] ...

### What's In Progress

- [ ] ...

### What's Next

1. ...

## Blockers / Risks

- [ ] ...

session-handoff.md

# Session Handoff (versionado — por branch)

## Current Objective

- Goal: ...
- Active feature (from harness/<branch>/state/feature_list.json): ...
- Branch / commit: ...

## Completed This Session

- [x] ...

## Verification Evidence

| Check | Command | Result | Notes |
| ----- | ------- | ------ | ----- |
| ...   | ...     | ...    | ...   |

## Files Changed

- ...

## Decisions Made

- ...

## Blockers / Risks

- ...

## Next Session Startup

1. Ler `CLAUDE.md` (seção "Harness")
2. Resolver a branch atual (`git rev-parse --abbrev-ref HEAD`) e ler `harness/user_preferences.md`
   (se existir) e `harness/<branch>/state/progress.md`
3. Rodar `./init.sh` (FAST) antes de editar

## Recommended Next Step

- ...

Regra inegociável

feature_list.json e progress.md nunca nascem pré-preenchidos. Só os _examples/ são criados no bootstrap. harness/<branch>/state/ é criado (scaffolding a partir dos _examples/) na primeira sessão daquela branch — mas o conteúdo é preenchido por quem trabalha, não pelo Claude "para ajudar".

Se harness/<branch>/state/ não existir ainda, o Claude deve criar a pasta a partir dos _examples/ (mecânico) e recomendar preencher — não inventar feature_list.json/progress.md com valores supostos.

plans/ e specs/

Quando existir plano ou spec da feature ativa, ele tem precedência sobre o feature_list.json genérico para o detalhe fino. Atualize os dois ao final da sessão.

Regra de proveniência: um plano/spec fica na branch onde foi criado, mesmo que o trabalho continue depois em outra branch. Não é para realocar quando isso acontecer — é só registro de onde nasceu.

Se o projeto já tem planos/specs soltos em outro lugar (ex.: docs/superpowers/plans/ e docs/superpowers/specs/, de um uso anterior da skill superpowers), esse conteúdo passa a viver em harness/<branch>/plans/ e harness/<branch>/specs/.

Nota de compatibilidade: a skill superpowers pode assumir o caminho antigo docs/superpowers/. Se ela não encontrar os arquivos no novo local, ajuste a skill — não volte a mover os arquivos.

handoffs/ e user_preferences.md

Bootstrap em projeto novo

Invoque a skill anthropic-skills:curio-harness-bootstrap. Ela traz os templates já validados, o init.sh.template e a seção de CLAUDE.md. Não reconstrua a estrutura lendo outro projeto na mão — use os templates da skill como fonte, para não divergir silenciosamente.

Passos:

  1. Criar harness/guides/ e harness/_examples/ (nomes exatos acima — não invente variação).
  2. Copiar os templates para dentro de _examples/ (state/, handoffs/, user_preferences.example.md).
  3. Adicionar harness/user_preferences.md ao .gitignore — só ele.
  4. Escrever os guias a partir do código real do projeto.
  5. Gerar o init.sh com os comandos reais — ver 19.
  6. Inserir a seção de harness no CLAUDE.md — ver 16, incluindo o passo de resolver a branch atual e criar harness/<branch>/state/ a partir dos _examples/.
  7. Rodar o init.sh (FAST) e confirmar que passa.

Não existe mais passo de criar global_feature_list.json — foi eliminado (ver nota abaixo).

O que não fazer

Reestruturação 2026-08-25: por que branch-primeiro

Problema encontrado: harness/state/feature_list.json (+ progress.md, session-handoff.md) era um único arquivo, compartilhado entre todas as branches — apesar do .gitignore marcar state/* como local, os arquivos já estavam commitados de antes. Trocar de branch sobrescrevia o estado de outra branch. Essa foi a causa raiz de um bug real observado num projeto que segue este padrão.

Decisão: layout branch-primeiro. Cada branch ganha harness/<branch>/ com state/, plans/, specs/ e handoffs/ próprios, tudo versionado — sem gitignore para nada disso.

O que foi removido: global_feature_list.json (histórico compartilhado entre devs/branches de features passing). Decisão explícita: aceitar perder a visibilidade cross-branch em troca de simplicidade — cada branch agora é autocontida. Não recrie esse arquivo achando que é uma omissão.

Migração de histórico pré-existente: se um projeto já tem harness no formato antigo (um state/ único, global_feature_list.json) e vai migrar para este layout, os plans//specs/ antigos devem ser reatribuídos à branch onde nasceram — via git log --merges (merge commits guardam a branch de origem mesmo depois dela ser deletada: para cada arquivo, ache o commit de criação e caminhe pelos merges até achar aquele em que o commit é ancestral só do segundo parent, não do primeiro). Não deixe esse histórico solto numa pasta _legacy — reatribua de fato.

19 — `init.sh` e Definition of Done

19 — init.sh e Definition of Done

Um script que verifica o baseline do projeto antes e depois de cada sessão, em dois modos. E a definição de quando uma tarefa está de fato concluída. Um init.sh que não roda é pior que nenhum. Execute-o antes de dizer que terminou.

Por que existe

Sem baseline verificado, uma sessão começa sobre terreno já quebrado e o dev gasta uma hora depurando um erro que não era dele. O init.sh responde a uma pergunta em segundos: o projeto está são agora?

Roda no início da sessão (o terreno está limpo?) e no fim (eu quebrei algo?).

FAST e FULL

A distinção é o que faz o script ser usado de fato, em vez de pulado por demorar.

Modo Invocação O que roda Quando
FAST ./init.sh Lint + type-check Toda hora, várias vezes ao dia
FULL FULL=1 ./init.sh + format:check + build completo Antes de considerar pronto

FAST precisa terminar em segundos. Se passar de ~30s, algo está no modo errado.

O script

#!/usr/bin/env bash
# init.sh — verifica o baseline do projeto antes/depois de uma sessao.
#
# Uso:
#   ./init.sh          # rapido (padrao): lint + type-check
#   FULL=1 ./init.sh   # completo: + format:check + build
#
# Pre-requisitos:
#   - Node >= 24 no PATH (gerenciado via fnm — ver harness/guides/dev-environment.md)
#   - npm install ja rodado

set -euo pipefail

FULL="${FULL:-0}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

echo "== {{NOME_DO_PROJETO}} — init.sh (modo $([ "$FULL" = "1" ] && echo FULL || echo FAST)) =="

echo ""
echo "--- [1/4] Node ---"
node_expected=$(tr -d '[:space:]' < "$ROOT_DIR/.node-version")
node_actual=$(node -v 2>/dev/null | sed 's/^v//')
if [ -z "$node_actual" ]; then
  echo "ERRO: 'node' nao encontrado no PATH. Rode 'fnm use'."
  exit 1
fi
lower=$(printf '%s\n%s\n' "$node_expected" "$node_actual" | sort -V | head -n1)
if [ "$lower" != "$node_expected" ]; then
  echo "ERRO: Node v$node_actual encontrado, necessario >= v$node_expected."
  exit 1
fi
echo "Node v$node_actual OK"

echo ""
echo "--- [2/4] Lint ---"
(cd "$ROOT_DIR" && npm run lint)

echo ""
echo "--- [3/4] Type-check ---"
(cd "$ROOT_DIR" && npx tsc --noEmit)

if [ "$FULL" = "1" ]; then
  echo ""
  echo "--- [4/4] Formatacao + build ---"
  (cd "$ROOT_DIR" && npm run format:check)
  (cd "$ROOT_DIR" && npm run build)
else
  echo ""
  echo "--- [4/4] Build — PULADO (rode com FULL=1 para incluir) ---"
fi

echo ""
echo "== init.sh OK =="

Projeto web dentro de um monorepo

Se o web for submódulo de um monorepo, o init.sh fica na raiz do repositório e entra na pasta do módulo:

WEB_DIR="$ROOT_DIR/caminho/do/modulo"

echo "--- [2/4] Lint ---"
(cd "$WEB_DIR" && npm run lint)

Decisões do script

Item Razão
set -euo pipefail Aborta no primeiro erro; variável indefinida vira falha, não string vazia
Checagem de Node primeiro Erro claro em vez de falha incompreensível do npm
npx tsc --noEmit no FAST Barato e pega o que o checker do dev não viu
build só no FULL Demora demais para rodar a toda hora
format:check, não format Verificação não deve reescrever arquivo

Não copie comandos de outro projeto (ex.: ./mvnw compile, mvn test de um projeto fullstack Java+React). Os comandos precisam ser os reais deste projeto.

Rodar o script

chmod +x init.sh
./init.sh

No Windows, execute pelo Git Bash. Se o time for todo Windows, considere um init.ps1 equivalente — mas mantenha um só como fonte de verdade, para não divergirem.

Execute o script depois de gerá-lo. Isso não é sugestão: um init.sh que nunca rodou tem probabilidade alta de estar quebrado (caminho errado, script npm inexistente, set -e derrubando num comando que retorna não-zero legitimamente).

Startup Workflow

Antes de escrever código:

  1. pwd — confirme o diretório.
  2. Resolva a branch atual (git rev-parse --abbrev-ref HEAD). Se harness/<branch>/state/ não existir, crie a partir de harness/_examples/ (scaffolding mecânico, não preenchimento de conteúdo).
  3. Leia harness/user_preferences.md (se existir).
  4. Leia harness/<branch>/state/feature_list.json e progress.md (se existirem).
  5. git log --oneline -5.
  6. ./init.sh.

Se o baseline já estiver quebrado, corrija antes de implementar feature nova. Misturar correção de baseline com feature nova produz um diff que ninguém consegue revisar.

Definition of Done

Uma tarefa só está concluída quando:

O item do browser não é opcional

Não há suíte de testes (14). lint e tsc provam que o código compila, não que funciona. A única evidência funcional que existe neste padrão é abrir a tela e verificar o fluxo. Ver o checklist de 17.

Evidência é texto, não adjetivo

// RUIM — nao e' evidencia
"evidence": "Implementado e testado."

// BOM — verificavel
"evidence": "Implementado em 2026-08-18. ./init.sh FULL verde. Tela cadastro/fornecedor aberta em dev:
busca com filtro 'ACME' retornou 3 registros; salvar com CNPJ invalido bloqueou no zod; salvar valido
mostrou notificacao verde e o registro apareceu na tabela apos rebusca. Sem erro no console."

Não marque como passing só porque o código foi escrito. É o erro mais comum, e o que torna o feature_list.json inútil.

Fim de sessão

  1. Atualizar harness/<branch>/state/feature_list.json (status + evidência): se algo chegou a passing, mover a entrada de features para completed no mesmo arquivo — sem cópia para nenhum arquivo global, não existe mais (ver 18).
  2. Atualizar harness/<branch>/state/progress.md.
  3. Atualizar harness/<branch>/state/session-handoff.md com o resultado da verificação.
  4. Commit descritivo com o repositório em estado seguro — inclui state/, plans/, specs/ e handoffs/ da branch, tudo versionado.
  5. A próxima sessão deve conseguir rodar ./init.sh imediatamente.

O passo 5 é o teste do fim de sessão: se a próxima pessoa não consegue verificar o baseline sem antes consertar algo, a sessão não fechou.

Se o projeto adotar testes

Se 14 for revisto e o projeto passar a ter suíte:

  1. Adicione npm test ao bloco FULL do init.sh.
  2. Adicione "testes passando" à Definition of Done.
  3. Mantenha o item da verificação no browser — teste unitário não prova integração com o Curio.

20 — Ciclo de desenvolvimento

20 — Ciclo de desenvolvimento

Do recebimento da tarefa ao merge na master: issue, branch, commits, MR, review. Processo pensado para projeto só de frontend, com o harness integrado. Este documento cobre como o trabalho flui; os anteriores cobrem como o código se organiza.

Referência rápida

tarefa recebida
    → clarificação (contexto, escopo, critérios, dependências)
    → issue aberta com template + labels + assignee
    → branch criada: <id-issue>-<descricao-curta>
    → ./init.sh antes de começar (baseline limpo)
    → sync com master
    → desenvolvimento com commits semânticos
    → ./init.sh FAST antes de cada commit
    → ./init.sh FULL + verificação no browser antes do MR
    → sync com master antes de abrir MR
    → MR aberto com template + reviewer
    → code review → ajustes → aprovação
    → merge → issue fechada
    → fim de sessão do harness (feature_list, progress, handoff)

1. Recebimento da tarefa

Antes de abrir issue, você precisa conseguir responder as quatro perguntas. Se não conseguir, peça uma reunião curta (15 min) com quem trouxe a demanda.

Pergunta O que responder
Contexto Por que essa tarefa existe? Que problema de negócio resolve?
Escopo O que está dentro e fora dessa entrega?
Critérios de aceite Como saberemos que está pronto? Quem valida?
Dependências Algo precisa estar pronto antes? Outro time envolvido?

Num projeto Curio há uma quinta pergunta, específica: o caso de uso do backend já existe? Se a tela depende de um caso de uso ainda não implementado, isso é dependência bloqueante — registre.

2. Issue no GitLab

Template

.gitlab/issue_templates/default.md:

## Contexto

<!-- Por que essa tarefa existe? O que motivou essa demanda? -->

## O que fazer

<!-- Descrição objetiva e clara do que precisa ser entregue. -->

## Critérios de aceite

- [ ]
- [ ]

## Notas técnicas

<!-- Id do caso de uso, nomes de request, impacto em outras telas.
     Deixe em branco se não houver. -->

## Referências

<!-- Design no Figma, documentação, issue relacionada. -->

Título

Padrão [Tipo] Verbo + objeto:

[Feature] Adicionar tela de cadastro de fornecedor
[Bug] Corrigir data de expiração enviada sem sufixo de hora
[Chore] Atualizar dependências para versão LTS
[Refactor] Extrair filtro de busca para componente comum

Labels

Dois eixos:

Eixo Labels
Tipo type::feature type::bug type::chore type::refactor
Prioridade priority::critical priority::high priority::medium priority::low

Assignee é obrigatório. Issue sem dono não entra no sprint.

Definition of Ready

Uma issue só entra no sprint quando:

3. Branch

Crie a partir da issue no GitLab — o id vem automaticamente. Sempre a partir da master atualizada.

<id-issue>-<descricao-curta>

123-cadastro-fornecedor
456-corrigir-data-expiracao
789-atualizar-dependencias

4. Antes de começar

Rode o Startup Workflow do harness (19):

git fetch origin && git merge origin/master
./init.sh

Se o baseline já estiver quebrado, corrija antes de começar a feature. Misturar conserto de baseline com feature nova produz um diff irrevisável.

Mantenha a branch sincronizada com a master ao menos uma vez por dia, e sempre antes de abrir o MR.

5. Commits

Conventional Commits:

<tipo>: descrição curta
Tipo Quando usar
feat nova funcionalidade
fix correção de bug
refactor refatoração sem mudança de comportamento
docs documentação
chore manutenção, dependências
perf melhoria de performance
test testes — só se o projeto adotar suíte
feat: adicionar tela de cadastro de fornecedor
fix: corrigir data de expiração enviada sem sufixo de hora
refactor: extrair filtro de busca para componente comum

Princípios

Verificação antes de cada commit

./init.sh

O pre-commit já roda lint-staged, versão de Node e checagem de flags de debug (14) — mas ele só vê os arquivos staged. O init.sh FAST vê o projeto inteiro.

Não suba código que quebra o baseline. Isso bloqueia o time e polui o histórico.

Se este projeto for só frontend, ignore quaisquer instruções de build Java/Delphi herdadas de um processo combinado com backend — não se aplicam.

6. Merge Request

Template

.gitlab/merge_request_templates/default.md:

## O que foi feito

<!-- Breve descrição das mudanças. -->

## Issue relacionada

Closes #

## Como testar

1.
2.

## Screenshots (se aplicável)

<!-- Antes/depois para mudanças visuais. -->

## Checklist

- [ ] `./init.sh FULL` passando localmente
- [ ] Tela aberta no browser e fluxo verificado contra o backend real
- [ ] Sem `console.log` ou código de debug (`enableLogs: true`)
- [ ] Documentação atualizada (se necessário)
- [ ] Branch atualizada com a master
- [ ] `harness/state/feature_list.json` e `progress.md` atualizados

Ajuste o checklist ao que o projeto realmente tem. Um template herdado de outro processo pode pedir itens como "Testes escritos ou atualizados" e "Pipeline passando" mesmo quando o projeto não tem suíte de testes nem .gitlab-ci.yml (15). Checklist que pede o inexistente treina o time a marcar caixa sem ler. Se o projeto novo adotar CI, acrescente a linha; se não, não a coloque.

Configuração no GitLab

7. Code review

Para quem revisa

Para quem abre

O que observar

Categoria O que verificar
Baseline ./init.sh passa; sem warning novo
Lógica Edge cases não cobertos, condições incorretas
Curio Caso de uso aberto antes do request; guarda por useRef; sem reabrir
Erro Sem try/catch só para exibir mensagem — o global já notifica (07)
Segurança Dado exposto, validação ausente, segredo commitado
Performance Chamada redundante, request em loop, lista sem virtualização
Legibilidade Nome autoexplicativo, função com responsabilidade única
Convenções _Prefixo, barrel atualizado, alias nos dois configs (13)

8. Merge

9. Fim de sessão

Depois do merge, feche o ciclo no harness (19):

  1. harness/state/feature_list.json — status + evidência
  2. Se chegou a passing, copiar para harness/global_feature_list.json
  3. harness/state/progress.md — o que foi feito, bloqueios, próximo passo
  4. harness/state/session-handoff.md — resultado da verificação

A próxima sessão precisa conseguir rodar ./init.sh imediatamente.

22 — Testes E2E com Playwright

22 — Testes E2E com Playwright

Guia para configurar Playwright do zero em projeto Vite + React. Consolidado a partir de uma configuração real, testada de verdade — as armadilhas na seção 11 aconteceram de fato, não são hipotéticas.

Sumário

  1. Conceitos essenciais do Playwright
  2. Instalação
  3. Estrutura de pastas sugerida
  4. Configuração (playwright.config.ts)
  5. Variáveis de ambiente
  6. Setup de login (sessão reutilizável)
  7. Fixtures + Page Object Model
  8. Exemplo de spec genérico
  9. Seed/cleanup de dados via API (opcional, avançado)
  10. .gitignore e scripts do package.json
  11. Decisões e armadilhas comuns

1. Conceitos essenciais do Playwright

O Playwright Test é um framework de testes ponta a ponta (E2E) com test runner, asserções, isolamento, paralelização e um conjunto rico de ferramentas. Suporta Chromium, Firefox e WebKit (Windows/Linux/macOS/CI), com emulação móvel nativa.

Referência oficial de boas práticas: https://playwright.dev/docs/best-practices

2. Instalação

npm init playwright@latest

O instalador pergunta:

Dependências relevantes (via devDependencies):

"@playwright/test": "^1.62.1",
"dotenv": "^16.4.5",
"@types/node": "^24.13.3"

dotenv é necessário porque o playwright.config.ts lê variáveis de um .env próprio dos testes (credenciais de login E2E), separado da config runtime da aplicação.

Comandos de execução:

npx playwright test        # roda a suíte
npx playwright test --ui   # interface gráfica (recomendada pela doc oficial)

Extensão VS Code: instale a extensão oficial "Playwright Test for VSCode" — permite gravar interações (Record new) e gera o código do teste automaticamente a partir de cliques reais na aplicação.

3. Estrutura de pastas sugerida

tests/
├── base.ts                        # fixtures customizadas + decorator @step
├── e2e/
│   └── <dominio>/
│       └── <feature>/
│           ├── <Feature>TestPage.ts   # Page Object da tela
│           └── <feature>.spec.ts      # spec que usa a Page Object
├── setup/
│   ├── login.setup.ts             # roda antes de tudo, gera sessão reutilizável
│   ├── seed.setup.ts              # opcional — garante dados de apoio via API
│   └── cleanup.teardown.ts        # opcional — apaga dados de apoio via API
└── support/
    ├── apiClient.ts                # cliente HTTP/RPC isolado, sem depender de src/
    └── seeders/
        ├── types.ts                # contrato Seeder
        ├── registry.ts             # lista ordenada de seeders
        └── <dominio>/
            ├── <dominio>.config.ts
            └── <dominio>.seeder.ts

O que importa reter dessa organização: um Page Object por tela testada, testes de setup/seed/cleanup como projects isolados (não como beforeAll/afterAll dentro do spec), e o cliente de API de teste separado de qualquer código de src/.

4. Configuração (playwright.config.ts)

import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "path";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, ".env") });

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 4 : undefined,
  reporter: process.env.CI
    ? [["junit", { outputFile: "results.xml" }], ["html", { open: "never" }]]
    : "html",
  expect: { timeout: 10_000 },
  use: {
    baseURL: "http://localhost:5173", // ajuste para a porta real do `vite dev`
    trace: "on-first-retry"
  },
  projects: [
    {
      name: "login",
      use: { ...devices["Desktop Chrome"] },
      testMatch: /login\.setup\.ts/
    },
    {
      name: "chromium",
      use: { ...devices["Desktop Chrome"] },
      dependencies: ["login"],
      testMatch: /.*\.spec\.ts/
    }
    // Adicione "seed"/"cleanup" como projects extras só se o projeto
    // realmente precisar de dados de apoio via API (ver seção 9).
  ],
  webServer: {
    command: "npm run dev",
    url: "http://localhost:5173", // igual ao baseURL acima
    reuseExistingServer: !process.env.CI
  }
});

Atenção à porta: é um erro comum apontar essa configuração para uma porta padrão (localhost:3000) quando o Vite do projeto usa outra. Confirme a porta real do npm run dev do seu projeto antes de fixar baseURL/webServer.url — é fácil de repetir.

Os projects login/chromium com dependencies são o mecanismo central: o Playwright sempre roda o project de login primeiro, e os specs (chromium) só começam depois que ele termina — sem precisar refazer login em cada teste.

5. Variáveis de ambiente

Crie um .env.example na raiz do projeto de testes (versionado) e um .env real (ignorado pelo git):

# Credenciais usadas pelo setup de login dos testes E2E (Playwright)
E2E_USER="USUARIO_DE_TESTE"
E2E_PASSWORD="SENHA_DE_TESTE"

6. Setup de login (sessão reutilizável)

tests/setup/login.setup.ts:

import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { test as setup, expect } from "@playwright/test";

const user = process.env.E2E_USER;
const password = process.env.E2E_PASSWORD;

if (user === undefined) throw new Error("E2E_USER não definido");
if (password === undefined) throw new Error("E2E_PASSWORD não definido");

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sessionFilePath = path.resolve(__dirname, "../../playwright/.auth/sessionStorage.json");

setup("Setup Inicial de Login do Sistema", async ({ page }) => {
  await page.goto("/login");

  await page.getByLabel("Email").fill(user);
  await page.getByLabel("Senha").fill(password);
  await page.getByRole("button", { name: "Entrar" }).click();

  await expect(page.getByText("Nome do Sistema")).toBeVisible();

  // Se a app usa sessionStorage (não localStorage) para guardar o token,
  // `storageState()` nativo do Playwright não serve — precisa salvar na mão:
  const sessionStorageData = await page.evaluate(() => JSON.stringify(window.sessionStorage));

  fs.mkdirSync(path.dirname(sessionFilePath), { recursive: true });
  fs.writeFileSync(sessionFilePath, sessionStorageData, "utf-8");
});

Se a aplicação alvo usa localStorage em vez de sessionStorage, use o storageState() nativo do Playwright (context.storageState({ path })) — é mais simples e não exige o addInitScript manual do passo 7. O caminho manual só é necessário quando o token fica em sessionStorage.

7. Fixtures + Page Object Model

tests/base.ts — fixture customizada (createTestPage) que injeta a sessão salva e fornece um decorator @step para nomear passos no relatório do Playwright:

import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { test as base, expect, Page } from "@playwright/test";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const sessionFilePath = path.resolve(__dirname, "../playwright/.auth/sessionStorage.json");

type PageObjectClass<T> = new (page: Page) => T;

export const test = base.extend<{
  createTestPage: <T>(PageObject: PageObjectClass<T>) => T;
}>({
  page: async ({ page }, use) => {
    if (fs.existsSync(sessionFilePath)) {
      const sessionStorageData = fs.readFileSync(sessionFilePath, "utf-8");

      await page.addInitScript((storage) => {
        const entries = JSON.parse(storage);
        for (const [key, value] of Object.entries(entries)) {
          window.sessionStorage.setItem(key, value as string);
        }
      }, sessionStorageData);
    }

    await page.goto("/");
    await use(page);
  },
  createTestPage: async ({ page }, use) => {
    await use((PageObject) => new PageObject(page));
  }
});

export { expect };

/** Decorator que envolve um método de Page Object num `test.step` nomeado. */
export function step(stepName?: string) {
  return function decorator<This, Args extends unknown[], Return>(
    target: (this: This, ...args: Args) => Return,
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
  ) {
    function replacementMethod(this: This, ...args: Args): Return {
      const name = `${stepName || (context.name as string)}`;
      return test.step(name, async () => {
        return target.call(this, ...args);
      }) as Return;
    }
    return replacementMethod;
  };
}

Todo spec deve importar test/expect deste arquivo, nunca direto de @playwright/test — é o que garante que a sessão salva seja injetada.

Page Object — uma classe por tela, cada teste vira um método público:

export class ItemTestPage {
  constructor(private page: Page) {}

  private async fillForm(name: string) {
    // ... lógica de preenchimento
  }

  async setup() {
    // ... navegação até a tela, pré-condições
  }

  async testPageLoad() {
    // ... asserção de que a página carregou
  }

  async testCreate() {
    // ... teste de criação
  }

  async testEdit() {
    // ... teste de edição
  }

  async testDelete() {
    // ... teste de exclusão
  }
}

8. Exemplo de spec genérico

import { test } from "../../base";
import { ItemTestPage } from "./ItemTestPage";

test.describe("Testes para Cadastro de Item", () => {
  test("Carregamento da página", async ({ createTestPage }) => {
    const itemPage = createTestPage(ItemTestPage);
    await itemPage.setup();

    await itemPage.testPageLoad();
  });

  /**
   * Testes em série — dependem do estado deixado pelo teste anterior.
   * Fluxo: criar → editar → excluir.
   */
  test.describe.serial("Testes de Fluxo do Usuário", () => {
    test("Cadastro de Item", async ({ createTestPage }) => {
      const itemPage = createTestPage(ItemTestPage);
      await itemPage.setup();
      await itemPage.testCreate();
    });

    test("Edição de Item", async ({ createTestPage }) => {
      const itemPage = createTestPage(ItemTestPage);
      await itemPage.setup();
      await itemPage.testEdit();
    });

    test("Exclusão de Item", async ({ createTestPage }) => {
      const itemPage = createTestPage(ItemTestPage);
      await itemPage.setup();
      await itemPage.testDelete();
    });
  });
});

test.describe.serial é o que garante a ordem e compartilha falha em cadeia — se "Cadastro" falhar, os demais são pulados em vez de rodar contra um estado inconsistente.

9. Seed/cleanup de dados via API (opcional, avançado)

Só vale a pena se os testes dependem de dados de apoio (ex.: uma entidade pai que precisa existir antes da tela ser testável) e você quer evitar que cada spec crie/apague seus próprios dados.

Padrão recomendado:

No playwright.config.ts, isso vira:

projects: [
  { name: "login", testMatch: /login\.setup\.ts/, use: { ...devices["Desktop Chrome"] } },
  { name: "seed", testMatch: /seed\.setup\.ts/ },
  {
    name: "chromium",
    use: { ...devices["Desktop Chrome"] },
    dependencies: ["login", "seed"],
    teardown: "cleanup",
    testMatch: /.*\.spec\.ts/
  },
  { name: "cleanup", testMatch: /cleanup\.teardown\.ts/ }
]

Se o projeto novo não tiver essa necessidade de dados de apoio compartilhados, pule esta seção inteira — é a parte mais específica/avançada deste setup, não um requisito do Playwright em si.

10. .gitignore e scripts do package.json

Adicionar ao .gitignore:

# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/

É comum não haver um script dedicado ("test": "playwright test") no package.json, rodando os testes via npx playwright test diretamente. Vale considerar adicionar esse script no projeto novo desde o início:

"scripts": {
  "test:e2e": "playwright test",
  "test:e2e:ui": "playwright test --ui"
}

11. Decisões e armadilhas comuns