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: Campos de formulário usam os mesmos nomes (name="_Nome"), evitando conversão no submit. Sufixo XML nas interfaces que espelham a estrutura do backend, distinguindo-as dos tipos do front. 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: 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 interface para formato de objeto; type para união, interseção e utilitário. Interface de props do componente exportada e nomeada {Componente}Props. Tipo de formulário inferido do zod (z.infer), nunca escrito à mão — ver 11. Sufixo XML para interfaces que espelham o backend. Sufixos Request / Response para payloads de caso de uso. 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 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//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 (, 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: strict: true + noUnusedLocals + noUnusedParameters no TypeScript ESLint com --max-warnings 0 npm run build como verificação de integração (compila tudo) Verificação manual da tela contra o backend real 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 O recommended da v7 é bem mais amplo que o da v4 (que só tinha rules-of-hooks + exhaustive-deps). Ele inclui regras no estilo "React Compiler" — por exemplo react-hooks/set-state-in-effect, que acusa qualquer setStateX(...) chamado direto no corpo de um useEffect (fora de um callback de evento/subscrição). 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: Se o valor é derivado de outro estado/prop (ex.: uma lista filtrada de uma query), calcule direto no corpo do componente — não precisa de useState/useEffect nenhum. Se precisa resincronizar quando uma prop muda (ex.: um hook que lê de localStorage por key), ajuste o estado durante o render (guardando a prop anterior em outro useState e comparando), não dentro de um useEffect. Adotar o recommended completo é a recomendação — ele pega bug real de cascata de render. Se o projeto novo preferir adiar, ligue só rules-of-hooks + exhaustive-deps (equivalente ao v4) e trate o resto depois, mas isso é uma escolha explícita, não o padrão. --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: endOfLine: "crlf" — o time é Windows. Se houver dev em Linux/macOS, troque para "auto" e configure .gitattributes, senão todo commit reescreve o arquivo inteiro. bracketSameLine: true — o > de tag multi-linha fica na última prop, não em linha própria. Incomum, mas é o padrão da base. 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. 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. 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: lint — ESLint com zero warnings env:prod — gera public/config.json a partir de config/prod.json tsc — type-check completo (noEmit) 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-.js ├── index-.css └── -.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: npm run build (ou build:homolog) Copiar o conteúdo de dist/ para o diretório publicado no IIS Ajustar dist/config.json se o destino diferir do ambiente do build 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: 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 npm run build passou localmente config.json no dist/ aponta para o ambiente certo Nenhum token de outro ambiente no config.json publicado Rewrite de SPA configurado no IIS F5 numa tela interna carrega (valida o rewrite) Login funciona contra o backend do ambiente sourcemap condiz com a exposição da aplicação 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 ├── / — 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) ``` `` é o nome literal da branch atual (`git rev-parse --abbrev-ref HEAD`). **Só `harness/user_preferences.md` é local.** Tudo dentro de `harness//` é versionado — se `harness//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//plans/*.md` — planos task-by-task por feature - `harness//specs/*.md` — specs de design - `harness//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//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//state/feature_list.json` e `harness//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//state/feature_list.json` foi atualizado (`status` + `evidence`); - `harness//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//state/feature_list.json` (status + evidência); se algo chegou a `passing`, mover de `features` para `completed` no mesmo arquivo. 2. Atualizar `harness//state/progress.md`. 3. Atualizar `harness//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: Abre um caso de uso no backend Busca fornecedores por filtro Exibe o resultado em tabela Salva um fornecedor novo Camadas tocadas: constants → interfaces → hooks → schemas → página → paths → routes → menuTree. Pré-requisitos Do backend, você precisa saber: Id do caso de uso (ex.: "4821") Nomes dos requests (ex.: RM_INCLUI_OBJETO, RM_BUSCA_FORNECEDORES, RM_SALVA_OBJETO) Formato dos payloads 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(FORNECEDOR_RMS.OBTEM_DADOS); export const useBuscaFornecedores = () => useCurioMutation(FORNECEDOR_RMS.BUSCAR); export const useSalvaFornecedor = () => useCurioMutation(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; export const filtroFornecedorSchema = z.object({ _Nome: z.string(), _Ativo: z.boolean() }); export type FiltroFornecedorData = z.infer; 5. Estilos // src/pages/Cadastro/Fornecedor/FornecedorPage.styles.ts import { SxProps, Theme } from "@mui/material"; export const containerStyle: SxProps = { display: "flex", flexDirection: "column", gap: 2, p: 3 }; export const headerBarStyle: SxProps = { display: "flex", justifyContent: "flex-end", gap: 1 }; export const filtroStyle: SxProps = { display: "flex", gap: 2, alignItems: "flex-start", flexWrap: "wrap" }; export const formSectionStyle: SxProps = { 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[] = [ { 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([]); 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 ( Cadastro de fornecedor Fornecedores cadastrados ); }; const FornecedorPage: React.FC = () => { const { session } = useAuth(); return ( ); }; 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: O item "Fornecedor" aparece sob "Cadastro" no menu lateral Clicar abre a aba (não 404, não "Rota não encontrada") O select "Tipo" vem preenchido — prova que o caso de uso abriu e o request de abertura respondeu Salvar com campos vazios mostra as mensagens de validação e não envia request Salvar preenchido mostra a notificação verde Buscar preenche a tabela Buscar sem resultado mostra "Nenhum fornecedor encontrado." Nenhum erro no console Aba Network: os requests saem para a URL do config.json do ambiente ativo F5 na tela mantém a sessão (não cai no login) Fechar a aba encerra o caso de uso (se a tela usar useTabCloseCallback — ver 09) Falha proposital Com o backend inacessível, a busca mostra notificação vermelha e a aplicação não trava 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: Contexto se perde entre sessões. Cada sessão redescobre o que a anterior já sabia. Dois devs pisam no mesmo trabalho. Ninguém vê o que o outro está fazendo. "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 ├── / 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) é 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 / 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// 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// — é 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 gitlab-access.md — só se scripts do projeto usarem a API do GitLab (GITLAB_PRIVATE_TOKEN). Nunca commitar o valor do token, só a variável de ambiente. Glossário de domínio (ex.: um arquitetura-dominio-.md) — só se o domínio for genuinamente intrincado. É investigação cara; pergunte antes de criar. 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. /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//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//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//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//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/ /plans/*.md — planos de implementação task-by-task, com checkboxes. Nome: YYYY-MM-DD-nome-da-feature.md. /specs/*.md — specs de design para decisões pontuais. Nome: YYYY-MM-DD-nome-do-assunto-design.md. 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//plans/ e harness//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 /handoffs/*.md — handoff de uma feature específica, versionado. Complementa o session-handoff.md, que cobre a sessão mais recente de qualquer feature naquela branch. user_preferences.md — preferências pessoais do dev (modo padrão do init.sh, verbosidade, ambiente local, convenções de commit). Único arquivo local, criado a partir de _examples/user_preferences.example.md. Não é da branch — é da pessoa, atravessa todas as branches. 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: Criar harness/guides/ e harness/_examples/ (nomes exatos acima — não invente variação). Copiar os templates para dentro de _examples/ (state/, handoffs/, user_preferences.example.md). Adicionar harness/user_preferences.md ao .gitignore — só ele. Escrever os guias a partir do código real do projeto. Gerar o init.sh com os comandos reais — ver 19. Inserir a seção de harness no CLAUDE.md — ver 16, incluindo o passo de resolver a branch atual e criar harness//state/ a partir dos _examples/. 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 Não copie os guias de outro projeto achando que "é o mesmo framework". Pode ser outra versão do Curio, outra linguagem, outras convenções. Sempre reescreva a partir da investigação do projeto alvo. Não crie /state/feature_list.json ou progress.md preenchidos "para ajudar" — só a pasta, a partir dos _examples/; o conteúdo é de quem trabalha. Não pule a execução do init.sh. Script quebrado passa despercebido até a primeira sessão real, quando já é tarde. Não recrie global_feature_list.json. Foi removido deliberadamente — ver nota de migração abaixo. 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// 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: pwd — confirme o diretório. Resolva a branch atual (git rev-parse --abbrev-ref HEAD). Se harness//state/ não existir, crie a partir de harness/_examples/ (scaffolding mecânico, não preenchimento de conteúdo). Leia harness/user_preferences.md (se existir). Leia harness//state/feature_list.json e progress.md (se existirem). git log --oneline -5. ./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 comportamento alvo foi implementado ./init.sh passa — FAST no mínimo, FULL antes de considerar pronto para revisão A tela foi aberta no browser e o fluxo verificado contra o backend real harness//state/feature_list.json atualizado com status e evidence harness//state/progress.md atualizado 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 Atualizar harness//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). Atualizar harness//state/progress.md. Atualizar harness//state/session-handoff.md com o resultado da verificação. Commit descritivo com o repositório em estado seguro — inclui state/, plans/, specs/ e handoffs/ da branch, tudo versionado. 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: Adicione npm test ao bloco FULL do init.sh. Adicione "testes passando" à Definition of Done. 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: - → ./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 ## O que fazer ## Critérios de aceite - [ ] - [ ] ## Notas técnicas ## Referências 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: Título segue o padrão Contexto preenchido e compreensível Critérios de aceite listados e verificáveis Assignee atribuído Sem dependência bloqueante em aberto Se a tela depende de caso de uso, o id e os requests estão nas notas técnicas 3. Branch Crie a partir da issue no GitLab — o id vem automaticamente. Sempre a partir da master atualizada. - 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: : 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 Cada commit faz uma coisa só O baseline passa após cada commit A mensagem explica o porquê, não o o quê 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 ## Issue relacionada Closes # ## Como testar 1. 2. ## Screenshots (se aplicável) ## 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 Aprovação mínima de 1 reviewer Pipeline obrigatória para habilitar o merge — se houver pipeline 7. Code review Para quem revisa Leia a descrição do MR antes do código — entenda o contexto primeiro Comente com intenção explícita: nit: — sugestão menor, não bloqueia suggestion: — sugestão de melhoria, bloqueia num primeiro momento blocking: — precisa ser resolvido antes do merge Questione o porquê, não só o como Priorize lógica e segurança; estilo é papel do linter Aprove se o código está correto e legível. Não exija que seja idêntico ao que você faria Para quem abre Responda todos os comentários antes de pedir re-review Não faça force push depois de abrir o MR — quebra o histórico de revisão Marque como resolvido ao aplicar a correção Atenda ao que o revisor observa, mesmo sendo pequeno — exceto em demanda urgente 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 Só com pipeline verde — se houver pipeline Closes #123 no MR fecha a issue automaticamente 9. Fim de sessão Depois do merge, feche o ciclo no harness (19): harness/state/feature_list.json — status + evidência Se chegou a passing, copiar para harness/global_feature_list.json harness/state/progress.md — o que foi feito, bloqueios, próximo passo 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 Conceitos essenciais do Playwright Instalação Estrutura de pastas sugerida Configuração (playwright.config.ts) Variáveis de ambiente Setup de login (sessão reutilizável) Fixtures + Page Object Model Exemplo de spec genérico Seed/cleanup de dados via API (opcional, avançado) .gitignore e scripts do package.json 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. Isolamento em 3 camadas: Browser (processo do navegador, caro, 1 por worker) → BrowserContext (cookies/localStorage/sessionStorage/cache isolados, barato, 1 por teste) → Page (aba dentro do contexto, não isola nada sozinha). Auto-waiting: antes de qualquer ação (click, fill, etc.) o Playwright verifica actionability checks (visível, estável, habilitado...) e repete a checagem até passar ou estourar timeout — elimina a necessidade de sleep(). expect(locator).toBeVisible() é polling, não uma foto única; expect(await x.count()).toBe(n) é. Fixtures: injeção de dependência do Playwright (page, context, browser, request já vêm nativas). Substitui beforeEach repetido por "pedir só o que a receita precisa". Workers: cada worker é um processo do SO inteiro, com seu próprio browser; se um teste falha, o worker inteiro é descartado e recriado. Paralelismo: por padrão, arquivos diferentes rodam em workers diferentes; testes do mesmo arquivo rodam em sequência no mesmo worker, a menos que fullyParallel: true. Projects: configuração nomeada e independente (pode ter seu próprio browser/device/storageState/testMatch). É a base do padrão de setup/cleanup usado abaixo. Referência oficial de boas práticas: https://playwright.dev/docs/best-practices 2. Instalação npm init playwright@latest O instalador pergunta: TypeScript ou JavaScript; Nome do diretório de testes; Adicionar GitHub Actions workflow; Instalar os browsers do Playwright. 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/ │ └── / │ └── / │ ├── TestPage.ts # Page Object da tela │ └── .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 └── / ├── .config.ts └── .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 = new (page: Page) => T; export const test = base.extend<{ createTestPage: (PageObject: PageObjectClass) => 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( target: (this: This, ...args: Args) => Return, context: ClassMethodDecoratorContext 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: tests/support/apiClient.ts: abre uma sessão direto contra o backend (sem passar pelo código de src/), autenticando com E2E_USER/E2E_PASSWORD. Só existe porque o carregador ESM do Node (usado pelo Playwright) não resolve certos imports profundos sem extensão que o bundler do Vite tolera — se o seu backend for uma API REST comum, um fetch/client HTTP simples resolve sem essa complicação. tests/support/seeders/types.ts: contrato comum Seeder (name, seedAll(), teardownAll()). tests/support/seeders/registry.ts: lista ordenada de seeders — a ordem importa quando um seeder depende de dado criado por outro. tests/setup/seed.setup.ts (project seed, roda antes dos specs): itera SEEDERS chamando seedAll(). tests/setup/cleanup.teardown.ts (project cleanup, com teardown: "cleanup" no project principal): itera SEEDERS na ordem inversa chamando teardownAll(), para respeitar dependências ao apagar. 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 Porta do webServer/baseURL precisa bater exatamente com a porta do vite dev — é um erro fácil de cometer e só aparece depois de já commitado. Confirme antes de bater o martelo. sessionStorage vs localStorage: se a app-alvo guarda token/sessão em sessionStorage, o storageState() nativo do Playwright não funciona — é preciso capturar manualmente com page.evaluate no setup e reinjetar com page.addInitScript na fixture (seções 6 e 7). Se a app usa localStorage, prefira o mecanismo nativo, é mais simples. Nunca importar test/expect direto de @playwright/test nos specs — sempre do tests/base.ts local, senão a fixture de sessão não é aplicada. Setup/seed/cleanup como projects separados, não como hooks dentro dos specs — isso é o que permite rodar uma vez só (login) ou uma vez por suíte inteira (seed/cleanup), em vez de repetir por arquivo/teste. Teste em série (test.describe.serial) só quando o fluxo realmente depende de estado anterior (criar → editar → excluir da mesma entidade); testes independentes devem ficar fora do bloco serial para poderem paralelizar.