) { return <>{children(data)}>; } ``` *** ## Checklist TypeScript [Seção intitulada “Checklist TypeScript”](#checklist-typescript) Antes de commitar código: * [ ] Nenhum uso de `any` (exceto casos extremos justificados) * [ ] Todos os tipos exportados estão documentados * [ ] Try-catch em todas operações assíncronas * [ ] Erros tipados adequadamente * [ ] Type guards onde necessário * [ ] Uso de utility types quando apropriado * [ ] Interfaces prefiram `readonly` onde aplicável * [ ] Discriminated unions para estados complexos * [ ] Exhaustive checking em switches * [ ] Props de componentes totalmente tipadas *** ## Erros Comuns a Evitar [Seção intitulada “Erros Comuns a Evitar”](#erros-comuns-a-evitar) ### ❌ NUNCA faça [Seção intitulada “❌ NUNCA faça”](#-nunca-faça) ```typescript // 1. Usar any const data: any = fetchData(); // 2. Type assertion desnecessário const user = data as User; // Perigoso se data não for User // 3. Non-null assertion sem garantia const value = possiblyNull!; // Pode quebrar em runtime // 4. Ignorar erros try { await operation(); } catch {} // Silenciar erro // 5. Tipos muito amplos function process(data: object) {} // object é muito genérico // 6. Mutação de readonly function modify(arr: readonly string[]) { arr.push("new"); // Erro! } ``` ### ✅ SEMPRE faça [Seção intitulada “✅ SEMPRE faça”](#-sempre-faça) ```typescript // 1. Type guards if (isUser(data)) { Logger.debug(data.email); } // 2. Validação adequada const user = validateUser(data); // Lança erro se inválido // 3. Null checking const value = possiblyNull ?? defaultValue; // 4. Log apropriado try { await operation(); } catch (error) { logger.error("Operation failed", { error }); throw error; } // 5. Tipos específicos interface ProcessData { id: string; value: number; } function process(data: ProcessData) {} // 6. Criar nova array function modify(arr: readonly string[]): string[] { return [...arr, "new"]; } ``` *** ## Scripts e Comandos [Seção intitulada “Scripts e Comandos”](#scripts-e-comandos) ### Scripts (root) [Seção intitulada “Scripts (root)”](#scripts-root) ```bash npm run build # Build todos workspaces npm run lint # Biome lint + typecheck npm run test # Rodar testes npm run serve:demo # Dev apontando para stg ``` ### Ambientes [Seção intitulada “Ambientes”](#ambientes) * **local** - desenvolvimento local * **test/dev** - ambiente de desenvolvimento * **demo/stg** - staging * **live/prd** - produção *** ## EZ4 - Infrastructure as Code [Seção intitulada “EZ4 - Infrastructure as Code”](#ez4---infrastructure-as-code) O projeto usa **[EZ4](https://github.com/sbalmt/ez4)** para deploy na AWS. ### Pacotes EZ4 usados [Seção intitulada “Pacotes EZ4 usados”](#pacotes-ez4-usados) * `@ez4/common` - Tipos e utilitários * `@ez4/utils` - Funções gerais * `@ez4/project` - Config de projeto * `@ez4/aws-bucket` - S3 Bucket * `@ez4/aws-cloudfront` - CloudFront (CDN) * `@ez4/distribution` - Contrato de distribuição *** ## ✅ Checklist Antes de Implementar [Seção intitulada “✅ Checklist Antes de Implementar”](#-checklist-antes-de-implementar) ### IMPORTANTE [Seção intitulada “IMPORTANTE”](#importante) Este documento contém diretrizes obrigatórias. Claude Code deve seguir TODOS os padrões. ### Antes de QUALQUER implementação [Seção intitulada “Antes de QUALQUER implementação”](#antes-de-qualquer-implementação) * [ ] Li arquivos relevantes na pasta? * [ ] Identifiquei componentes/código similar? * [ ] Identifiquei padrões de nomenclatura? * [ ] Identifiquei estrutura de código? * [ ] Mostrei exemplos do código existente? * [ ] Recebi confirmação? ### Durante implementação [Seção intitulada “Durante implementação”](#durante-implementação) * [ ] Seguindo EXATAMENTE os padrões identificados? * [ ] Reutilizando código ao invés de duplicar? * [ ] TypeScript com tipagem completa? * [ ] Usando Biome (não ESLint/Prettier)? * [ ] Usando TailwindCSS (não CSS modules)? * [ ] Usando bibliotecas corretas (Vitest, não Jest)? * [ ] Nomenclatura correta? * [ ] Componentes base: `[pasta]/component.tsx`? * [ ] Features: `[feature]/kebab-case.tsx`? * [ ] Stores com devtools + version? * [ ] Usando shallow em Zustand? *** ## O QUE NUNCA FAZER [Seção intitulada “O QUE NUNCA FAZER”](#o-que-nunca-fazer) * ❌ Nunca use ESLint ou Prettier (use Biome) * ❌ Nunca use Jest (use Vitest) * ❌ Nunca use CSS modules (use Tailwind) * ❌ Nunca use moment.js/dayjs (use date-fns) * ❌ Nunca implemente sem analisar código existente * ❌ Nunca duplique código que já existe * ❌ Nunca crie padrões novos sem justificativa * ❌ Nunca use Context API (use Zustand) * ❌ Nunca faça fetch direto (use React Query) * ❌ Nunca commite código sem tipagem TypeScript * ❌ Nunca commite secrets/API keys * ❌ Nunca crie store Zustand sem devtools + version * ❌ Nunca use múltiplos valores Zustand sem shallow * ❌ Nunca ignore a estrutura de pastas do projeto *** ## Perguntas Frequentes [Seção intitulada “Perguntas Frequentes”](#perguntas-frequentes) **P: Onde criar componentes reutilizáveis?** R: Em `src/components/[categoria]/component.tsx` **P: Onde criar features complexas?** R: Em `src/features/[feature-name]/` **P: Como nomear arquivos de componentes?** R: Componentes base: `component.tsx`. Features: `kebab-case.tsx` **P: Onde ficam as stores Zustand?** R: Dentro das features: `src/features/[feature]/store/` **P: Como usar ícones?** R: Lucide React (já instalado) **P: Como estilizar?** R: TailwindCSS + cva para variantes *** ## Referências [Seção intitulada “Referências”](#referências) * [EZ4 GitHub](https://github.com/sbalmt/ez4) * [Radix UI](https://www.radix-ui.com/) * [shadcn/ui](https://ui.shadcn.com/) * [TanStack Query](https://tanstack.com/query/latest) * [Zustand](https://docs.pmnd.rs/zustand) * [Biome](https://biomejs.dev/) * [Vitest](https://vitest.dev/) * [Doppler](https://www.doppler.com/) * [TypeScript Handbook](https://www.typescriptlang.org/docs/handbook/intro.html) * [TypeScript Deep Dive](https://basarat.gitbook.io/typescript/) * [Type Challenges](https://github.com/type-challenges/type-challenges) * [Total TypeScript](https://www.totaltypescript.com/)
# Internacionalização (i18n)
> Guia completo para implementação de internacionalização na plataforma Gaio.
* [Introdução](#introdu%C3%A7%C3%A3o) * [Arquitetura](#arquitetura) * [Hooks e Funções](#hooks-e-fun%C3%A7%C3%B5es) * [Padrões de Tradução](#padr%C3%B5es-de-tradu%C3%A7%C3%A3o) * [Namespaces](#namespaces) * [Integração com Zod](#integra%C3%A7%C3%A3o-com-zod) * [Pluralização e Interpolação](#pluraliza%C3%A7%C3%A3o-e-interpola%C3%A7%C3%A3o) * [Melhores Práticas de Interpolação](#melhores-pr%C3%A1ticas-de-interpola%C3%A7%C3%A3o) * [Formatação HTML em Traduções](#formata%C3%A7%C3%A3o-html-em-tradu%C3%A7%C3%B5es) * [Caching com localStorage](#caching-com-localstorage) * [Seletor de Idioma](#seletor-de-idioma) * [CLI e Extração](#cli-e-extra%C3%A7%C3%A3o) * [Validação i18n (CI/CD)](#valida%C3%A7%C3%A3o-i18n-cicd) * [Testes](#testes) * [Anti-patterns](#anti-patterns) * [Tipagem TypeScript (Intellisense)](#tipagem-typescript-intellisense) *** ## Introdução [Seção intitulada “Introdução”](#introdução) ### Stack [Seção intitulada “Stack”](#stack) | Biblioteca | Versão | Propósito | | ---------------------------------- | ------ | --------------------------- | | `i18next` | 25.x | Core de internacionalização | | `react-i18next` | 16.x | Bindings React | | `i18next-browser-languagedetector` | 8.x | Detecção automática | | `i18next-chained-backend` | 5.x | Múltiplos backends | | `i18next-localstorage-backend` | 4.x | Cache em localStorage | | `i18next-resources-to-backend` | 1.x | Converter recursos inline | | `i18next-cli` | 1.x | Extração de strings (CLI) | ### Idiomas Suportados [Seção intitulada “Idiomas Suportados”](#idiomas-suportados) | Código | Nome | Status | | ------- | ------------------ | -------- | | `pt-BR` | Português (Brasil) | Fonte | | `en` | English | Tradução | ### Arquitetura [Seção intitulada “Arquitetura”](#arquitetura) ```plaintext ┌─────────────────────────────────────────────────────────────┐ │ @gaio/i18n │ │ (Package dedicado para internacionalização) │ └─────────────────────────┬───────────────────────────────────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌─────────────────┐ ┌───────────┐ ┌───────────────┐ │ I18nProvider │ │ useT │ │ zodMessages │ │ (Wrapper App) │ │ (Hook) │ │ (Validação) │ └─────────────────┘ └───────────┘ └───────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ locales/ │ │ pt-BR/common.json │ en/common.json │ ... │ └─────────────────────────────────────────────────────────────┘ ``` ### Imports Padrão [Seção intitulada “Imports Padrão”](#imports-padrão) O package é dividido em dois módulos: ```typescript // ============================================ // @gaio/i18n/core - SEM dependência de React // Use em: utils, services, schemas Zod, constantes // ============================================ import { i18n, zodMessages, SUPPORTED_LOCALES, LOCALE_LABELS, clearI18nCache, getCacheVersion, } from "@gaio/i18n/core"; // ============================================ // @gaio/i18n/react - COM dependência de React // Use em: componentes, hooks, providers // ============================================ import { I18nProvider, useT, useLocale, changeLocale } from "@gaio/i18n/react"; // Componente Trans (re-export do react-i18next) import { Trans } from "@gaio/i18n/react"; ``` **Quando usar cada um:** | Módulo | Uso | Exemplos | | ------------------ | ------------------ | ---------------------------------------- | | `@gaio/i18n/core` | Arquivos sem React | Schemas Zod, utils, services, constantes | | `@gaio/i18n/react` | Componentes React | Hooks, Provider, componentes UI | *** ## Arquitetura [Seção intitulada “Arquitetura”](#arquitetura-1) ### Estrutura do Package [Seção intitulada “Estrutura do Package”](#estrutura-do-package) ```plaintext packages/i18n/ ├── package.json ├── tsconfig.json ├── i18next.config.ts └── src/ ├── core/ # @gaio/i18n/core (sem React) │ ├── index.ts # Exports: i18n, zodMessages, constantes, cache utils │ ├── constants.ts # SUPPORTED_LOCALES, NAMESPACES │ ├── config.ts # Setup i18next com ChainedBackend │ └── validation.ts # zodMessages │ ├── react/ # @gaio/i18n/react (com React) │ ├── index.ts # Exports: Provider, hooks, Trans │ ├── provider.tsx # I18nProvider │ └── hooks.ts # useT, useLocale, changeLocale │ ├── types/ # Tipagem TypeScript │ └── resources.d.ts # Declaração para intellisense │ └── locales/ # Traduções ├── pt-BR/ │ ├── index.ts │ ├── common.json │ ├── validation.json │ ├── errors.json │ ├── auth.json │ ├── inbox.json │ ├── workflows.json │ └── settings.json └── en/ └── ... (mesmos arquivos) ``` ### Exports do Package [Seção intitulada “Exports do Package”](#exports-do-package) package.json ```json { "exports": { "./core": "./src/core/index.ts", "./react": "./src/react/index.ts" } } ``` ### Integração com Console [Seção intitulada “Integração com Console”](#integração-com-console) O `I18nProvider` deve ser o provider mais externo na aplicação: packages/console/src/providers.tsx ```tsx import { I18nProvider } from "@gaio/i18n/react"; export function AppProviders({ children }: { children: ReactNode }) { return ( {children} ); } ``` *** ## Hooks e Funções [Seção intitulada “Hooks e Funções”](#hooks-e-funções) ### useT [Seção intitulada “useT”](#uset) Hook principal para acessar traduções. Wrapper do `useTranslation` do react-i18next. ```typescript import { useT } from '@gaio/i18n/react'; function MyComponent() { const { t } = useT(); return ; } ``` #### Com Namespace Específico [Seção intitulada “Com Namespace Específico”](#com-namespace-específico) ```typescript // Namespace único const { t } = useT("validation"); t("required"); // validation:required // Múltiplos namespaces const { t } = useT(["common", "validation"]); t("common:actions.save"); t("validation:required"); ``` #### Retorno do useT [Seção intitulada “Retorno do useT”](#retorno-do-uset) | Propriedade | Tipo | Descrição | | ----------- | --------------------------- | -------------------------- | | `t` | `(key, options?) => string` | Função de tradução | | `i18n` | `i18n` | Instância i18next | | `ready` | `boolean` | Se traduções estão prontas | ### useLocale [Seção intitulada “useLocale”](#uselocale) Hook para obter o idioma atual. **Localização:** `@gaio/i18n/react` ```typescript import { useLocale } from '@gaio/i18n/react'; function LanguageIndicator() { const locale = useLocale(); // 'pt-BR' | 'en-US' return Idioma: {locale}; } ``` ### changeLocale [Seção intitulada “changeLocale”](#changelocale) Função para trocar o idioma programaticamente. **Localização:** `@gaio/i18n/react` ```typescript import { changeLocale } from "@gaio/i18n/react"; import type { SupportedLocale } from "@gaio/i18n/core"; function handleLanguageChange(locale: SupportedLocale) { changeLocale(locale); // Atualiza i18n + cookie } ``` *** ## Padrões de Tradução [Seção intitulada “Padrões de Tradução”](#padrões-de-tradução) ### Convenção de Keys [Seção intitulada “Convenção de Keys”](#convenção-de-keys) **IMPORTANTE:** Todas as keys devem estar em **inglês** usando `snake_case`: ```typescript // ✅ CORRETO - keys em inglês, descritivas t("validation:required"); t("validation:invalid_email"); t("common:actions.save"); t("common:messages.success.saved"); // ❌ ERRADO - keys em português t("validation:campo_obrigatorio"); t("validation:email_invalido"); t("common:acoes.salvar"); ``` ### Texto Simples [Seção intitulada “Texto Simples”](#texto-simples) ```tsx import { useT } from "@gaio/i18n/react"; function SaveButton() { const { t } = useT(); return ; } ``` locales/pt-BR/common.json ```json { "actions": { "save": "Salvar", "cancel": "Cancelar", "confirm": "Confirmar" } } ``` ### Com Interpolação [Seção intitulada “Com Interpolação”](#com-interpolação) ```tsx const { t } = useT(); // Simples t("welcome", { name: "João" }); // "Olá, João!" // Múltiplas variáveis t("summary", { count: 5, total: 100 }); // "5 de 100 itens" ``` ```json { "welcome": "Olá, {{name}}!", "summary": "{{count}} de {{total}} itens" } ``` ### JSX Complexo (Trans) [Seção intitulada “JSX Complexo (Trans)”](#jsx-complexo-trans) Use o componente `Trans` quando precisar de JSX dentro da tradução: ```tsx import { Trans } from "@gaio/i18n/react"; Ao continuar, você aceita os Termos de Uso ; ``` ```json { "terms": "Ao continuar, você aceita os <1>Termos de Uso1>" } ``` ### Toast Notifications [Seção intitulada “Toast Notifications”](#toast-notifications) ```tsx import { useT } from "@gaio/i18n/react"; import { toast } from "sonner"; function MyComponent() { const { t } = useT(); const handleSave = async () => { try { await saveData(); toast.success(t("common:messages.success.saved")); } catch { toast.error(t("common:messages.error.save")); } }; } ``` ### Fora de Componentes React [Seção intitulada “Fora de Componentes React”](#fora-de-componentes-react) Para usar traduções fora de componentes (utils, services), importe a instância `i18n` do módulo core: ```typescript import { i18n } from "@gaio/i18n/core"; export function formatError(code: string): string { return i18n.t(`errors:${code}`); } ``` *** ## Namespaces [Seção intitulada “Namespaces”](#namespaces) Os namespaces organizam traduções por domínio/feature: | Namespace | Conteúdo | Uso | | ------------ | ---------------------- | ------------------------------ | | `common` | Botões, labels, status | UI compartilhada | | `validation` | Mensagens de validação | Schemas Zod | | `errors` | Erros HTTP/API | Error handlers | | `auth` | Login, registro | `screens/auth/` | | `inbox` | Chat, mensagens | `screens/dashboard/inbox/` | | `workflows` | Editor de automação | `screens/dashboard/workflows/` | | `settings` | Configurações | `screens/dashboard/settings/` | ### Vocabulário Padrão [Seção intitulada “Vocabulário Padrão”](#vocabulário-padrão) O namespace `common` contém elementos reutilizáveis em toda a aplicação. **Sempre use essas keys ao invés de criar duplicatas.** #### Ações (Botões) [Seção intitulada “Ações (Botões)”](#ações-botões) common.json ```json { "actions": { "save": "Salvar", "cancel": "Cancelar", "confirm": "Confirmar", "create": "Criar", "edit": "Editar", "delete": "Excluir", "remove": "Remover", "add": "Adicionar", "send": "Enviar", "back": "Voltar", "next": "Próximo", "previous": "Anterior", "close": "Fechar", "open": "Abrir", "search": "Buscar", "filter": "Filtrar", "clear": "Limpar", "copy": "Copiar", "paste": "Colar", "duplicate": "Duplicar", "refresh": "Atualizar", "load_more": "Carregar mais", "view_more": "Ver mais", "view_less": "Ver menos", "expand": "Expandir", "collapse": "Recolher", "export": "Exportar", "import": "Importar", "download": "Baixar", "upload": "Enviar arquivo" } } ``` **Uso:** ```tsx t("common:actions.save"); // "Salvar" t("common:actions.cancel"); // "Cancelar" ``` #### Status [Seção intitulada “Status”](#status) common.json ```json { "status": { "active": "Ativo", "inactive": "Inativo", "pending": "Pendente", "approved": "Aprovado", "rejected": "Rejeitado", "completed": "Concluído", "in_progress": "Em andamento", "paused": "Pausado", "canceled": "Cancelado", "archived": "Arquivado", "draft": "Rascunho", "published": "Publicado", "scheduled": "Agendado", "expired": "Expirado", "error": "Erro", "success": "Sucesso", "loading": "Carregando...", "processing": "Processando..." } } ``` **Uso:** ```tsx t("common:status.active"); // "Ativo" t("common:status.pending"); // "Pendente" ``` #### Labels de Campos [Seção intitulada “Labels de Campos”](#labels-de-campos) common.json ```json { "fields": { "name": "Nome", "email": "E-mail", "phone": "Telefone", "password": "Senha", "confirm_password": "Confirmar senha", "description": "Descrição", "title": "Título", "date": "Data", "time": "Hora", "start_date": "Data de início", "end_date": "Data de término", "address": "Endereço", "city": "Cidade", "state": "Estado", "country": "País", "zip_code": "CEP", "cpf": "CPF", "cnpj": "CNPJ", "notes": "Observação", "attachment": "Anexo", "file": "Arquivo", "image": "Imagem", "link": "Link", "url": "URL", "quantity": "Quantidade", "value": "Valor", "price": "Preço", "total": "Total" } } ``` **Uso:** ```tsx t("common:fields.name"); // "Nome" t("common:fields.email"); // "E-mail" ``` #### Placeholders [Seção intitulada “Placeholders”](#placeholders) common.json ```json { "placeholders": { "search": "Buscar...", "select": "Selecione...", "type": "Digite...", "email": "exemplo@email.com", "phone": "(00) 00000-0000", "date": "DD/MM/AAAA" } } ``` **Uso:** ```tsx ``` #### Mensagens Genéricas [Seção intitulada “Mensagens Genéricas”](#mensagens-genéricas) common.json ```json { "messages": { "success": { "saved": "Salvo com sucesso", "created": "Criado com sucesso", "updated": "Atualizado com sucesso", "deleted": "Excluído com sucesso", "sent": "Enviado com sucesso", "copied": "Copiado para a área de transferência" }, "error": { "generic": "Ocorreu um erro. Tente novamente.", "connection": "Erro de conexão. Verifique sua internet.", "load": "Erro ao carregar dados", "save": "Erro ao salvar", "delete": "Erro ao excluir", "permission": "Você não tem permissão para esta ação" }, "confirmation": { "delete": "Tem certeza que deseja excluir?", "cancel": "Tem certeza que deseja cancelar?", "exit": "Tem certeza que deseja sair?", "discard": "Descartar alterações não salvas?" }, "empty": { "list": "Nenhum item encontrado", "search": "Nenhum resultado para sua busca", "data": "Sem dados para exibir" } } } ``` **Uso:** ```tsx toast.success(t("common:messages.success.saved")); toast.error(t("common:messages.error.generic")); ``` #### Tempo e Datas [Seção intitulada “Tempo e Datas”](#tempo-e-datas) common.json ```json { "time": { "today": "Hoje", "yesterday": "Ontem", "tomorrow": "Amanhã", "now": "Agora", "just_now": "Há pouco", "seconds": "{{count}} segundo", "seconds_plural": "{{count}} segundos", "minutes": "{{count}} minuto", "minutes_plural": "{{count}} minutos", "hours": "{{count}} hora", "hours_plural": "{{count}} horas", "days": "{{count}} dia", "days_plural": "{{count}} dias", "weeks": "{{count}} semana", "weeks_plural": "{{count}} semanas", "months": "{{count}} mês", "months_plural": "{{count}} meses" } } ``` **Uso:** ```tsx t("common:time.days", { count: 5 }); // "5 dias" t("common:time.today"); // "Hoje" ``` #### Paginação e Listas [Seção intitulada “Paginação e Listas”](#paginação-e-listas) common.json ```json { "pagination": { "page": "Página {{current}} de {{total}}", "items": "{{count}} item", "items_plural": "{{count}} itens", "showing": "Mostrando {{from}} a {{to}} de {{total}}", "per_page": "Por página", "first": "Primeira", "last": "Última" } } ``` #### Regra de Ouro [Seção intitulada “Regra de Ouro”](#regra-de-ouro) ```tsx // ❌ ERRADO - Criar key específica para algo comum t("workflows:save"); t("inbox:cancel_button"); t("settings:delete_account"); // ✅ CORRETO - Usar vocabulary padrão t("common:actions.save"); t("common:actions.cancel"); t("common:actions.delete"); // ✅ CORRETO - Key específica apenas para contexto único t("settings:account.delete_permanently"); // Ação específica com consequência diferente ``` ### Criar Novo Namespace [Seção intitulada “Criar Novo Namespace”](#criar-novo-namespace) Guia passo a passo para adicionar um novo escopo de tradução. #### Criar Arquivos JSON [Seção intitulada “Criar Arquivos JSON”](#criar-arquivos-json) ```bash # Estrutura de arquivos packages/i18n/src/locales/ ├── pt-BR/ │ └── campaigns.json # NOVO └── en/ └── campaigns.json # NOVO ``` packages/i18n/src/locales/pt-BR/campaigns.json ```json { "title": "Campanhas", "create": "Criar campanha", "edit": "Editar campanha", "delete": "Excluir campanha", "status": { "active": "Ativa", "paused": "Pausada", "finished": "Finalizada" }, "messages": { "created_success": "Campanha criada com sucesso", "updated_success": "Campanha atualizada com sucesso", "deleted_success": "Campanha excluída com sucesso", "error_create": "Erro ao criar campanha" } } ``` packages/i18n/src/locales/en/campaigns.json ```json { "title": "Campaigns", "create": "Create campaign", "edit": "Edit campaign", "delete": "Delete campaign", "status": { "active": "Active", "paused": "Paused", "finished": "Finished" }, "messages": { "created_success": "Campaign created successfully", "updated_success": "Campaign updated successfully", "deleted_success": "Campaign deleted successfully", "error_create": "Error creating campaign" } } ``` #### Registrar no Index de Locales [Seção intitulada “Registrar no Index de Locales”](#registrar-no-index-de-locales) packages/i18n/src/locales/pt-BR/index.ts ```typescript import auth from "./auth.json"; import campaigns from "./campaigns.json"; import common from "./common.json"; import errors from "./errors.json"; import inbox from "./inbox.json"; import settings from "./settings.json"; import validation from "./validation.json"; import workflows from "./workflows.json"; export default { auth, campaigns, common, errors, inbox, settings, validation, workflows, }; ``` #### Adicionar nas Constantes [Seção intitulada “Adicionar nas Constantes”](#adicionar-nas-constantes) packages/i18n/src/core/constants.ts ```typescript export const NAMESPACES = [ "auth", "campaigns", "common", "errors", "inbox", "settings", "validation", "workflows", ] as const; ``` #### Atualizar Tipagem TypeScript [Seção intitulada “Atualizar Tipagem TypeScript”](#atualizar-tipagem-typescript) packages/i18n/src/types/resources.d.ts ```typescript import "i18next"; import auth from "../locales/pt-BR/auth.json"; import campaigns from "../locales/pt-BR/campaigns.json"; import common from "../locales/pt-BR/common.json"; import errors from "../locales/pt-BR/errors.json"; import inbox from "../locales/pt-BR/inbox.json"; import settings from "../locales/pt-BR/settings.json"; import validation from "../locales/pt-BR/validation.json"; import workflows from "../locales/pt-BR/workflows.json"; declare module "i18next" { interface CustomTypeOptions { defaultNS: "common"; resources: { auth: typeof auth; campaigns: typeof campaigns; common: typeof common; errors: typeof errors; inbox: typeof inbox; settings: typeof settings; validation: typeof validation; workflows: typeof workflows; }; } } ``` #### Usar o Novo Namespace [Seção intitulada “Usar o Novo Namespace”](#usar-o-novo-namespace) packages/console/src/screens/dashboard/campaigns/index.tsx ```tsx import { useT } from "@gaio/i18n/react"; function CampaignsPage() { const { t } = useT("campaigns"); return (
{t("title")}
); } ``` ```tsx // Acesso a keys aninhadas t("campaigns:status.active"); // "Ativa" t("campaigns:messages.created_success"); // "Campanha criada com sucesso" ``` #### Checklist Novo Namespace [Seção intitulada “Checklist Novo Namespace”](#checklist-novo-namespace) * [ ] Criar `pt-BR/{namespace}.json` * [ ] Criar `en/{namespace}.json` * [ ] Adicionar import em `locales/pt-BR/index.ts` * [ ] Adicionar import em `locales/en/index.ts` * [ ] Adicionar em `NAMESPACES` em `constants.ts` * [ ] Adicionar em `resources.d.ts` para intellisense * [ ] Testar com `useT('namespace')` no componente *** ## Integração com Zod [Seção intitulada “Integração com Zod”](#integração-com-zod) O `zodMessages` fornece mensagens de validação traduzidas para schemas Zod. ### Mensagens Disponíveis [Seção intitulada “Mensagens Disponíveis”](#mensagens-disponíveis) | Função | Exemplo de Uso | Key | | --------------------- | --------------------------------------- | -------------------------------- | | `required()` | `.min(1, zodMessages.required())` | `validation:required` | | `minLength(n)` | `.min(3, zodMessages.minLength(3))` | `validation:min_length` | | `maxLength(n)` | `.max(100, zodMessages.maxLength(100))` | `validation:max_length` | | `invalidEmail()` | `.email(zodMessages.invalidEmail())` | `validation:invalid_email` | | `invalidUrl()` | `.url(zodMessages.invalidUrl())` | `validation:invalid_url` | | `invalidPhone()` | Custom validator | `validation:invalid_phone` | | `passwordMismatch()` | Refine de confirmação | `validation:password_mismatch` | | `passwordMinLength()` | `.min(8, ...)` | `validation:password_min_length` | | `passwordUppercase()` | Regex validator | `validation:password_uppercase` | | `passwordLowercase()` | Regex validator | `validation:password_lowercase` | | `passwordNumber()` | Regex validator | `validation:password_number` | | `passwordSpecial()` | Regex validator | `validation:password_special` | ### Exemplo de Schema [Seção intitulada “Exemplo de Schema”](#exemplo-de-schema) ```typescript import { z } from "zod"; import { zodMessages } from "@gaio/i18n/core"; export const RegisterSchema = z .object({ name: z .string() .min(1, zodMessages.required()) .max(100, zodMessages.maxLength(100)), email: z .string() .min(1, zodMessages.required()) .email(zodMessages.invalidEmail()), password: z .string() .min(8, zodMessages.passwordMinLength()) .regex(/[A-Z]/, zodMessages.passwordUppercase()) .regex(/[a-z]/, zodMessages.passwordLowercase()) .regex(/[0-9]/, zodMessages.passwordNumber()), confirmPassword: z.string(), }) .refine((data) => data.password === data.confirmPassword, { message: zodMessages.passwordMismatch(), path: ["confirmPassword"], }); ``` ### Mensagens Customizadas [Seção intitulada “Mensagens Customizadas”](#mensagens-customizadas) Para mensagens específicas de um form, use `i18n.t()` diretamente: ```typescript import { i18n, zodMessages } from "@gaio/i18n/core"; const WorkflowSchema = z.object({ name: z .string() .min(1, zodMessages.required()) .max(50, () => i18n.t("workflows:name_max_50")), }); ``` *** ## Pluralização e Interpolação [Seção intitulada “Pluralização e Interpolação”](#pluralização-e-interpolação) ### Pluralização [Seção intitulada “Pluralização”](#pluralização) i18next usa sufixos para pluralização: locales/pt-BR/common.json ```json { "message": "{{count}} mensagem", "message_plural": "{{count}} mensagens", "message_zero": "Nenhuma mensagem" } ``` ```typescript t("message", { count: 0 }); // "Nenhuma mensagem" t("message", { count: 1 }); // "1 mensagem" t("message", { count: 5 }); // "5 mensagens" ``` ### Regras de Sufixos [Seção intitulada “Regras de Sufixos”](#regras-de-sufixos) | Sufixo | Quando usado | | --------- | ---------------------- | | (nenhum) | count === 1 | | `_plural` | count !== 1 | | `_zero` | count === 0 (opcional) | ### Interpolação com Formatação [Seção intitulada “Interpolação com Formatação”](#interpolação-com-formatação) ```json { "price": "R$ {{value, number}}", "date": "{{date, datetime}}" } ``` ```typescript t("price", { value: 1234.56 }); // "R$ 1.234,56" (formatado pelo locale) ``` ### Context (Gênero) [Seção intitulada “Context (Gênero)”](#context-gênero) ```json { "user_updated": "Usuário atualizado", "user_updated_male": "Usuário atualizado", "user_updated_female": "Usuária atualizada" } ``` ```typescript t("user_updated", { context: "female" }); // "Usuária atualizada" ``` *** ## Melhores Práticas de Interpolação [Seção intitulada “Melhores Práticas de Interpolação”](#melhores-práticas-de-interpolação) ### Quando Usar Interpolação [Seção intitulada “Quando Usar Interpolação”](#quando-usar-interpolação) Use interpolação **apenas** para valores dinâmicos que só podem ser conhecidos em runtime: ```typescript // ✅ CORRETO - valor dinâmico (nome do usuário vem do banco) t("welcome", { name: user.name }); // ✅ CORRETO - valores numéricos dinâmicos t("items_count", { count: items.length }); // ✅ CORRETO - timestamps e datas t("last_updated", { date: formatDate(updatedAt) }); ``` ### Quando NÃO Usar Interpolação [Seção intitulada “Quando NÃO Usar Interpolação”](#quando-não-usar-interpolação) Evite interpolação para valores conhecidos no momento da tradução. Use chaves separadas: ```typescript // ❌ ERRADO - concatenação de strings t("error_prefix") + errorType + t("error_suffix"); // ❌ ERRADO - interpolação para valores conhecidos t("file_type_error", { type: "PDF" }); // ✅ CORRETO - chaves separadas e autocontidas t("errors.invalid_pdf_file"); t("errors.invalid_image_file"); t("errors.invalid_document_file"); ``` ### Tabela de Decisão [Seção intitulada “Tabela de Decisão”](#tabela-de-decisão) | Cenário | Abordagem | Exemplo | | ----------------------------- | --------------------------- | -------------------------------------------- | | Nome de usuário | Interpolação | `Olá, {{name}}` | | Contagens | Interpolação + Pluralização | `{{count}} mensagem` / `{{count}} mensagens` | | Tipos de arquivo conhecidos | Chaves separadas | `invalid_pdf_file`, `invalid_image_file` | | Status conhecidos | Chaves separadas | `status.active`, `status.inactive` | | Mensagens de erro específicas | Chaves separadas | `errors.network_error`, `errors.auth_error` | | Métodos de pagamento | Chaves separadas | `payment.credit_card`, `payment.paypal` | *** ## Formatação HTML em Traduções [Seção intitulada “Formatação HTML em Traduções”](#formatação-html-em-traduções) Quando traduções precisam conter formatação (negrito, itálico, links), use o componente `Trans` do `react-i18next` em vez de `dangerouslySetInnerHTML`. ### defaultTransComponents [Seção intitulada “defaultTransComponents”](#defaulttranscomponents) Use o helper `defaultTransComponents` para elementos HTML padrão: ```typescript import { defaultTransComponents } from "@gaio/i18n/react"; // Elementos suportados: // e → // e → //
→
// → ``` ### Exemplo Completo [Seção intitulada “Exemplo Completo”](#exemplo-completo) ```tsx import { Trans, defaultTransComponents } from "@gaio/i18n/react"; export const InviteDialog = () => { return (
); }; ``` ### Quando Usar `Trans` vs `t()` [Seção intitulada “Quando Usar Trans vs t()”](#quando-usar-trans-vs-t) | Cenário | Use | Exemplo | | -------------------------------------- | ------------------------------- | ------------------------------------------------------------- | | Texto simples sem formatação | `t()` | `t('common:labels.save')` | | Texto com interpolação simples | `t()` | `t('messages.hello', { name })` | | Texto com **negrito**, *itálico*, etc. | `Trans` | `` | | Texto com links | `Trans` + `createLinkComponent` | Ver exemplo acima | *** ## Seletor de Idioma [Seção intitulada “Seletor de Idioma”](#seletor-de-idioma) ### Componente de Seleção [Seção intitulada “Componente de Seleção”](#componente-de-seleção) ```tsx import { useT, useLocale, changeLocale } from "@gaio/i18n/react"; import { LOCALE_LABELS, SUPPORTED_LOCALES } from "@gaio/i18n/core"; export function LanguageSelector() { const { t } = useT(); const currentLocale = useLocale(); return ( ); } ``` ### Persistência [Seção intitulada “Persistência”](#persistência) A preferência de idioma é salva automaticamente em um cookie `ga:i18n` (duração: 1 ano). Ordem de detecção: 1. `cookie` (preferência salva) 2. `navigator.language` (idioma do navegador) 3. `pt-BR` (fallback) *** ## CLI e Extração [Seção intitulada “CLI e Extração”](#cli-e-extração) ### Comandos [Seção intitulada “Comandos”](#comandos) ```bash # Extrair strings do código para os JSONs npm run i18n:extract -w @gaio/i18n # Verificar se há strings não extraídas (CI) npm run i18n:check -w @gaio/i18n ``` ### Workflow de Tradução [Seção intitulada “Workflow de Tradução”](#workflow-de-tradução) 1. Desenvolver feature com strings usando `t('key')` 2. Rodar `npm run i18n:extract -w @gaio/i18n` 3. Preencher traduções em `pt-BR/*.json` *** ## Validação i18n (CI/CD) [Seção intitulada “Validação i18n (CI/CD)”](#validação-i18n-cicd) ### Comandos da CLI Gaio [Seção intitulada “Comandos da CLI Gaio”](#comandos-da-cli-gaio) ```bash gaio i18n validate # Validação completa (usado no CI) gaio i18n validate --fix # Validação com auto-fix de chaves órfãs gaio i18n extract # Extrair chaves de tradução do código gaio i18n status # Ver status das traduções (completude) gaio i18n lint # Detectar hardcoded strings ``` ### O que é Validado [Seção intitulada “O que é Validado”](#o-que-é-validado) 1. **Chaves Órfãs** - Chaves que existem nos JSONs mas não são usadas no código 2. **Hardcoded Strings** - Strings visíveis ao usuário que não usam `t()` 3. **Status das Traduções** - Chaves faltantes entre idiomas ### Workflow de Desenvolvimento [Seção intitulada “Workflow de Desenvolvimento”](#workflow-de-desenvolvimento) 1. `gaio i18n extract` - Extrair novas chaves 2. Preencher traduções em ambos idiomas 3. `gaio i18n validate` - Validar localmente 4. Corrigir problemas com `--fix` se necessário *** ## Testes [Seção intitulada “Testes”](#testes) ### Test Provider [Seção intitulada “Test Provider”](#test-provider) ```tsx import { I18nProvider } from "@gaio/i18n/react"; export function TestI18nWrapper({ children }: { children: ReactNode }) { return {children}; } render( , ); ``` ### Mock de Traduções [Seção intitulada “Mock de Traduções”](#mock-de-traduções) ```typescript vi.mock("@gaio/i18n/react", () => ({ useT: () => ({ t: (key: string) => key, }), })); ``` *** ## Anti-patterns [Seção intitulada “Anti-patterns”](#anti-patterns) ```tsx // ❌ ERRADO - Strings hardcoded ; // ✅ CORRETO const { t } = useT(); ; ``` ```tsx // ❌ ERRADO - Concatenação de strings t("you_have") + count + t("messages"); // ✅ CORRETO - Interpolação t("you_have_messages", { count }); ``` ```tsx // ❌ ERRADO - Keys em português t("validation:campo_obrigatorio"); // ✅ CORRETO t("validation:required"); ``` ```tsx // ❌ ERRADO - useT em loops { items.map((item) => { const { t } = useT(); // Hook em cada iteração! return {t("item")}; }); } // ✅ CORRETO const { t } = useT(); { items.map((item) => {t("item")}); } ``` *** ## TypeScript [Seção intitulada “TypeScript”](#typescript) Para ter intellisense das variáveis de interpolação em cada tradução, configure os tipos: packages/i18n/src/types/resources.d.ts ```typescript import "i18next"; import common from "../locales/pt-BR/common.json"; // ... outros imports declare module "i18next" { interface CustomTypeOptions { defaultNS: "common"; resources: { common: typeof common; // ... outros namespaces }; } } ``` ### Benefícios [Seção intitulada “Benefícios”](#benefícios) ```typescript // ✅ Autocomplete de keys t("welcome"); // Sugere todas as keys disponíveis // ✅ Erro se key não existe t("key_inexistente"); // TypeScript error! // ✅ Autocomplete de variáveis de interpolação t("welcome", { name: "João" }); // TypeScript sabe que 'name' é obrigatório ``` *** ## Referências [Seção intitulada “Referências”](#referências) * **i18next:** * **react-i18next:** * **TypeScript setup:**
# Visão Geral
> Arquitetura e conceitos do frontend GAIO
Documentação do frontend do ecossistema GAIO. ## Índice [Seção intitulada “Índice”](#índice) * [Stack Técnica](/frontend/general/) — Tecnologias, padrões e convenções do projeto * [Gaio CLI](/cli/overview/) — Comandos e ferramentas da CLI * [Internacionalização (i18n)](/frontend/i18n/) — Guia completo de i18n com i18next * [Formulários](/frontend/forms/) — Padrões com React Hook Form + Zod * [Token Refresh](/frontend/auth-refresh/) — Coordenação de refresh com single-flight pattern ### Testing [Seção intitulada “Testing”](#testing) * [Arquitetura de Testes](/frontend/testing/overview/) — Estrutura e organização dos testes * [Shared Utilities](/frontend/testing/utils/shared/) — Utilitários compartilhados entre testes * [Utils](/frontend/testing/utils/overview/) — Vitest e MSW helpers * [E2E](/frontend/testing/e2e/) — Testes end-to-end com Playwright * [API Mocks](/frontend/testing/api-mocks/) — Padrões de mock para E2E ### Templates [Seção intitulada “Templates”](#templates) * [Fluxos](/frontend/templates/flows/) — Fluxos do funil de templates * [KPIs](/frontend/templates/kpis-funnel/) — Métricas e KPIs do funil * [Tracking](/frontend/templates/tracking/) — Eventos e analytics com PostHog
# Fluxos
> Fluxos de usuário e jornadas
Este documento descreve o fluxo de instalação de templates. O objetivo é suportar instalação com ou sem plano/cupom, usando o caminho: **Auth → Profile → Checkout → Success**. ## Etapas do Funil [Seção intitulada “Etapas do Funil”](#etapas-do-funil) | Step | Descrição | Rota | | ------------------ | ------------------------------------------- | ------------------------------------- | | `template_view` | Visualização do template | `/t/{slug}` | | `auth_pending` | Autenticação | `/t/{slug}/install/` | | `profile_pending` | Completar perfil + aceitar compartilhamento | `/t/{slug}/install/profile` | | `checkout_pending` | Checkout | `/t/{slug}/install/checkout` | | `success` | Sucesso | `/t/{subscriptionId}/install/success` | ## Transições principais [Seção intitulada “Transições principais”](#transições-principais) | Estado Atual | Evento | Próximo Estado | Observação | | ------------------ | ----------------- | ------------------ | ----------------------------- | | `template_view` | `install_click` | `auth_pending` | Início do funil | | `auth_pending` | `auth_success` | `profile_pending` | Usuário sem perfil completo | | `auth_pending` | `auth_success` | `checkout_pending` | Usuário com perfil completo | | `auth_pending` | `auth_error` | `auth_pending` | Exibe erro e permite retry | | `profile_pending` | `profile_submit` | `checkout_pending` | Dados válidos + consentimento | | `checkout_pending` | `payment_success` | `success` | Assinatura criada | | `checkout_pending` | `payment_error` | `checkout_pending` | Exibe erro e permite retry | ## Regras de navegação [Seção intitulada “Regras de navegação”](#regras-de-navegação) * Se o usuário já estiver autenticado e com perfil completo, pode ir direto para `checkout_pending`. * Se não houver plano/cupom, o checkout pode ser apresentado como **R$ 0** ou com plano nulo (de acordo com o backend). * O `subscriptionId` é usado apenas na rota de sucesso. * O consentimento de compartilhamento é obrigatório para avançar do perfil para o checkout. ## Estado mínimo do funil [Seção intitulada “Estado mínimo do funil”](#estado-mínimo-do-funil) ```ts interface InstallationState { sessionId: string | null; currentStep: | "template_view" | "auth_pending" | "profile_pending" | "checkout_pending" | "success"; completedSteps: Array< | "template_view" | "auth_pending" | "profile_pending" | "checkout_pending" | "success" >; templateId?: string; templateSlug?: string; planId?: string | null; couponId?: string | null; consent?: boolean; } ``` ## Eventos de tracking [Seção intitulada “Eventos de tracking”](#eventos-de-tracking) Os eventos estão em [Tracking](/frontend/templates/tracking). Eventos adicionais podem ser criados para insights mais detalhados, mas não devem ser usados para controle de fluxo.
# KPIs
> Métricas e conversão
Este documento descreve os KPIs mínimos para o funil de templates, alinhados ao tracking. ## Etapas [Seção intitulada “Etapas”](#etapas) ### Template [Seção intitulada “Template”](#template) * **Install Clicks** = `COUNT(template_install_click)` ### Auth [Seção intitulada “Auth”](#auth) * **Auth Starts** = `COUNT(auth_register_start) + COUNT(auth_login_start) + COUNT(auth_instagram_start)` * **Auth Completions** = `COUNT(auth_register_success) + COUNT(auth_login_success) + COUNT(auth_instagram_success)` * **Auth Errors** = `COUNT(auth_register_failure) + COUNT(auth_login_failure) + COUNT(auth_instagram_failure)` * **Auth Success Rate** = `auth_completions / auth_starts` ### Profile (via Pageviews) [Seção intitulada “Profile (via Pageviews)”](#profile-via-pageviews) * **Profile Views** = `COUNT($pageview WHERE $current_url CONTAINS '/install/profile')` * **Profile Completed** = `COUNT(profile_completed)` * **Drop-off Auth → Profile** = `1 - (profile_views / auth_completions)` ### Checkout [Seção intitulada “Checkout”](#checkout) * **Checkout Views** = `COUNT(checkout_viewed)` * **Checkout Submissions** = `COUNT(checkout_submitted)` * **Checkout Completions** = `COUNT(checkout_completed)` * **Checkout Error Rate** = `checkout_errors / checkout_submitted` * **Checkout Conversion Rate** = `checkout_completions / checkout_views` ### Funil completo [Seção intitulada “Funil completo”](#funil-completo) * **Total Funnel Conversion** = `template_signup_complete / template_install_click` * **Churn por Etapa** = Usar funil de pageviews (Auth → Profile → Consent → Checkout → Success) ## Dashboard PostHog [Seção intitulada “Dashboard PostHog”](#dashboard-posthog) O dashboard **Funil de Templates** contém todos os insights necessários: ### Insights de Checkout [Seção intitulada “Insights de Checkout”](#insights-de-checkout) * **Funil de Checkout Detalhado** - checkout\_viewed → submitted → completed * **Erros vs Submissões** - Taxa de erro ao longo do tempo * **Tipos de Erro** - Breakdown por error\_message * **Views vs Conclusões** - Gap de conversão no checkout ### Insights de Pageviews [Seção intitulada “Insights de Pageviews”](#insights-de-pageviews) * **Funil Completo por Páginas** - Conversão entre todas as etapas * **Pageviews por Etapa** - Volume de cada etapa ao longo do tempo ### Insights de Auth [Seção intitulada “Insights de Auth”](#insights-de-auth) * **Distribuição de Métodos** - Register vs Login vs Instagram * **Funil de Register** - auth\_register\_start → success * **Funil de Login** - auth\_login\_start → success ## Segmentações recomendadas [Seção intitulada “Segmentações recomendadas”](#segmentações-recomendadas) * `plan_id` (com plano vs sem plano) * `coupon_id` (com cupom vs sem cupom) * `is_zero_cost` (cupom 100% de desconto) * `auth_method` (registration, login, instagram) * `error_message` (tipos de erro no checkout) ## Eventos usados [Seção intitulada “Eventos usados”](#eventos-usados) Todos os eventos estão definidos em [Tracking](/frontend/templates/tracking). **Eventos principais**: * `template_install_click`, `template_signup_complete` * Auth: `auth_{method}_{outcome}` (register/login/instagram + start/success/failure) * Profile: `profile_completed`, `consent_accepted` * Checkout: `checkout_viewed`, `checkout_submitted`, `checkout_completed`, `checkout_error` * Pageviews: `$pageview` com filtros de URL para cada etapa do funil
# Tracking
> Eventos e monitoramento
O objetivo é medir toda a jornada do usuário ao ativar um template **com ou sem plano/cupom**, sem eventos desnecessários. ## Escopo [Seção intitulada “Escopo”](#escopo) Eventos **implementados**: * `template_install_click` - Clique no botão instalar * `template_signup_complete` - Conclusão com sucesso * **Auth Registration**: `auth_register_start`, `auth_register_success`, `auth_register_failure` * **Auth Login**: `auth_login_start`, `auth_login_success`, `auth_login_failure` * **Auth Instagram**: `auth_instagram_start`, `auth_instagram_success`, `auth_instagram_failure` * **Profile**: `profile_completed` - Conclusão do perfil * **Consent**: `consent_accepted` - Aceitação de compartilhamento * **Checkout**: `checkout_viewed`, `checkout_submitted`, `checkout_completed`, `checkout_error` ## Propriedades comuns [Seção intitulada “Propriedades comuns”](#propriedades-comuns) Todos os eventos incluem, quando disponíveis: * `template_id` - ID único do template * `template_slug` - Slug amigável do template * `plan_id` - ID do plano (se aplicável) * `amount_in_cents` - Valor do plano em centavos * `coupon_id` - ID do cupom (se aplicável) * `percent_off` - Percentual de desconto do cupom * `duration_in_months` - Duração do desconto em meses * `applies_first_month_only` - Se o desconto aplica apenas ao primeiro mês **Nota**: O `funnel_session_id` (PostHog session) é gerenciado automaticamente pelo SDK ## Eventos e payloads [Seção intitulada “Eventos e payloads”](#eventos-e-payloads) ### Eventos do Funil Principal [Seção intitulada “Eventos do Funil Principal”](#eventos-do-funil-principal) #### `template_install_click` [Seção intitulada “template\_install\_click”](#template_install_click) Disparado quando o usuário clica no botão instalar. ```ts interface TemplateInstallClickEvent { template_id: string; template_slug: string; plan_id?: string; coupon_id?: string; amount_in_cents?: number; } ``` #### `template_form_start` [Seção intitulada “template\_form\_start”](#template_form_start) Disparado quando o usuário inicia o preenchimento do formulário de perfil. ```ts interface TemplateFormStartEvent { template_id: string; template_slug: string; plan_id?: string; coupon_id?: string; } ``` #### `template_form_drop` [Seção intitulada “template\_form\_drop”](#template_form_drop) Disparado quando o usuário abandona o funil (inatividade ou navegação para fora). ```ts interface TemplateFormDropEvent { template_id: string; template_slug: string; plan_id?: string; coupon_id?: string; abandonment_point: 'account' | 'contact' | 'wizard_profile' | 'wizard_discovery' | 'activation'; } ``` #### `template_signup_complete` [Seção intitulada “template\_signup\_complete”](#template_signup_complete) Disparado quando o usuário chega na página de sucesso após ativação. ```ts interface TemplateSignupCompleteEvent { template_id: string; template_slug: string; plan_id?: string; coupon_id?: string; amount_in_cents?: number; } ``` *** ### Eventos de Autenticação [Seção intitulada “Eventos de Autenticação”](#eventos-de-autenticação) Todos os eventos de auth seguem o padrão: `auth_{method}_{outcome}` onde: * **method**: `register`, `login`, `instagram` * **outcome**: `start`, `success`, `failure` #### `auth_register_start` / `auth_register_success` / `auth_register_failure` [Seção intitulada “auth\_register\_start / auth\_register\_success / auth\_register\_failure”](#auth_register_start--auth_register_success--auth_register_failure) ```ts interface AuthRegisterEvent { auth_method: 'registration'; template_id?: string; template_slug?: string; error_code?: string; // apenas em failure error_message?: string; // apenas em failure } ``` #### `auth_login_start` / `auth_login_success` / `auth_login_failure` [Seção intitulada “auth\_login\_start / auth\_login\_success / auth\_login\_failure”](#auth_login_start--auth_login_success--auth_login_failure) ```ts interface AuthLoginEvent { auth_method: 'login'; template_id?: string; template_slug?: string; error_code?: string; // apenas em failure error_message?: string; // apenas em failure } ``` #### `auth_instagram_start` / `auth_instagram_success` / `auth_instagram_failure` [Seção intitulada “auth\_instagram\_start / auth\_instagram\_success / auth\_instagram\_failure”](#auth_instagram_start--auth_instagram_success--auth_instagram_failure) ```ts interface AuthInstagramEvent { auth_method: 'instagram'; template_id?: string; template_slug?: string; error_code?: string; // apenas em failure error_message?: string; // apenas em failure } ``` *** ### Eventos de Community Modal [Seção intitulada “Eventos de Community Modal”](#eventos-de-community-modal) #### `community_modal_view` [Seção intitulada “community\_modal\_view”](#community_modal_view) Disparado quando o modal de preview é aberto ou fechado. ```ts interface CommunityModalViewEvent { template_id: string; template_slug: string; modal_state: 'open' | 'close'; } ``` #### `community_modal_install_click` [Seção intitulada “community\_modal\_install\_click”](#community_modal_install_click) Disparado quando o usuário clica em instalar dentro do modal da comunidade. ```ts interface CommunityModalInstallClickEvent { template_id: string; template_slug: string; plan_id?: string; coupon_id?: string; } ``` ## Tracking por Pageviews [Seção intitulada “Tracking por Pageviews”](#tracking-por-pageviews) Para medir churn entre etapas do funil, use **pageviews automáticos** do PostHog ao invés de eventos customizados: ### URLs do Funil [Seção intitulada “URLs do Funil”](#urls-do-funil) * **Auth**: `/t/{slug}/install` - Página de autenticação * **Profile**: `/t/{slug}/install/profile` - Página de perfil * **Consent**: `/t/{slug}/install/consent` - Página de consentimento * **Checkout**: `/t/{slug}/install/checkout` - Página de checkout * **Success**: `/t/{slug}/install/success` - Página de sucesso ### Insights Baseados em Pageviews [Seção intitulada “Insights Baseados em Pageviews”](#insights-baseados-em-pageviews) 1. **Funil Completo por Páginas** - Mede conversão Auth → Profile → Consent → Checkout → Success 2. **Pageviews por Etapa - Tendência** - Compara volume de cada etapa ao longo do tempo 3. **Churn entre Etapas** - Diferença entre pageviews consecutivas identifica abandono **Vantagens**: * Sem código adicional (autocapture do PostHog) * Tempo médio entre etapas calculado automaticamente * Sessões rastreadas automaticamente * Menos eventos customizados = menos manutenção
# API Mocks
> Helpers para mockar APIs em testes E2E
Helpers reutilizáveis para mockar APIs nos testes Playwright. **Use sempre esses helpers** em vez de criar mocks manualmente. > **Ver também**: [Arquitetura de Testes](/frontend/testing/overview) | [Guia E2E](/frontend/testing/e2e) ## Objetivo [Seção intitulada “Objetivo”](#objetivo) * ✅ **Consistência**: Todos os testes usam os mesmos mocks * ✅ **Manutenibilidade**: Mocks centralizados em um único lugar * ✅ **Produtividade**: Setup rápido com funções pré-configuradas * ✅ **Redução de código**: Menos boilerplate nos testes ## Uso Rápido [Seção intitulada “Uso Rápido”](#uso-rápido) ### Setup Completo (Fluxo de Template) [Seção intitulada “Setup Completo (Fluxo de Template)”](#setup-completo-fluxo-de-template) ```typescript import { setupTemplateFlowMocks } from "../helpers"; test.beforeEach(async ({ page }) => { await setupTemplateFlowMocks(page, { templateId: "template-123", templateSlug: "test-template", templateName: "My Template", planId: null, // FREE ou 'plan-id' para PAID }); }); ``` ### Setup Modular [Seção intitulada “Setup Modular”](#setup-modular) ```typescript import { setupCommonApiMocks, setupAuthMocks, setupTemplateMocks, } from "../helpers"; test.beforeEach(async ({ page }) => { await setupCommonApiMocks(page); await setupAuthMocks(page); await setupTemplateMocks(page, { templateId: "my-template" }); }); ``` ## Helpers Disponíveis [Seção intitulada “Helpers Disponíveis”](#helpers-disponíveis) ### `setupCommonApiMocks(page)` [Seção intitulada “setupCommonApiMocks(page)”](#setupcommonapimockspage) Mocks básicos necessários para páginas autenticadas: * ✅ Current user * ✅ Connections * ✅ Subscription status * ✅ Account info * ✅ Catch-all para endpoints não mockados **Quando usar**: Em praticamente todos os testes. *** ### `setupAuthMocks(page, options?)` [Seção intitulada “setupAuthMocks(page, options?)”](#setupauthmockspage-options) Mocks de autenticação: * ✅ Login (`/api/console/login`) * ✅ Sign-in (`/api/console/sign-in`) * ✅ Sign-up (`/api/console/sign-up`) * ✅ Email verification **Opções**: ```typescript { invalidCredentials?: string[], // Emails com credenciais inválidas existingEmails?: string[], // Emails já cadastrados unverifiedEmails?: string[] // Emails não verificados } ``` **Exemplo**: ```typescript await setupAuthMocks(page, { invalidCredentials: ["wrong@example.com"], existingEmails: ["existing@example.com"], }); ``` *** ### `setupProfileMocks(page)` [Seção intitulada “setupProfileMocks(page)”](#setupprofilemockspage) Mocks de atualização de perfil: * ✅ Contact info (`/api/console/profile/contact-info`) * ✅ Account metadata (`/api/console/account/metadata`) * ✅ Update user (`/api/console/update-user`) **Quando usar**: Testes que envolvem edição de perfil ou wizard. *** ### `setupTemplateMocks(page, options?)` [Seção intitulada “setupTemplateMocks(page, options?)”](#setuptemplatemockspage-options) Mocks relacionados a templates: * ✅ Template details (public) * ✅ Template installation * ✅ Template list **Opções**: ```typescript { templateId?: string, templateSlug?: string, templateName?: string, planId?: string | null, // null = FREE, string = PAID planPrice?: number | null, couponId?: string | null, isInstalled?: boolean } ``` **Exemplo - Template FREE**: ```typescript await setupTemplateMocks(page, { templateSlug: "free-template", planId: null, }); ``` **Exemplo - Template PAID**: ```typescript await setupTemplateMocks(page, { templateSlug: "premium-template", planId: "plan-premium", planPrice: 9900, couponId: "WELCOME50", }); ``` *** ### `setupPaymentMocks(page, options?)` [Seção intitulada “setupPaymentMocks(page, options?)”](#setuppaymentmockspage-options) Mocks de pagamento (Stripe): * ✅ Setup intent * ✅ Payment method attach * ✅ Create subscription **Opções**: ```typescript { setupIntentSecret?: string, paymentMethodId?: string, subscriptionId?: string, shouldFailPayment?: boolean // Simular erro de pagamento } ``` **Exemplo - Pagamento com sucesso**: ```typescript await setupPaymentMocks(page); ``` **Exemplo - Simular falha**: ```typescript await setupPaymentMocks(page, { shouldFailPayment: true, }); ``` *** ### `setupTrackingMocks(page)` [Seção intitulada “setupTrackingMocks(page)”](#setuptrackingmockspage) Mocks de analytics/tracking: * ✅ Track events (`/api/*/track`) * ✅ Identify (`/api/*/identify`) **Quando usar**: Sempre (já incluído em `setupTemplateFlowMocks`). *** ### `setupTemplateFlowMocks(page, options?)` [Seção intitulada “setupTemplateFlowMocks(page, options?)”](#setuptemplateflowmockspage-options) **Helper all-in-one** que combina todos os mocks acima. **Use este para testes de template**. Inclui: * ✅ Common API mocks * ✅ Auth mocks * ✅ Profile mocks * ✅ Template mocks * ✅ Payment mocks * ✅ Tracking mocks **Opções**: Mesmas de `setupTemplateMocks()`. **Exemplo**: ```typescript test.beforeEach(async ({ page }) => { await setupTemplateFlowMocks(page, { templateSlug: "my-template", planId: "plan-123", }); }); ``` ## Exemplos Práticos [Seção intitulada “Exemplos Práticos”](#exemplos-práticos) ### Teste de Template Gratuito [Seção intitulada “Teste de Template Gratuito”](#teste-de-template-gratuito) ```typescript import { setupTemplateFlowMocks } from "../helpers"; test.describe("Free Template Flow", () => { test.beforeEach(async ({ page }) => { await setupTemplateFlowMocks(page, { templateId: "free-123", templateSlug: "free-template", planId: null, // FREE }); }); test("should install free template", async ({ page }) => { // Teste aqui... }); }); ``` ### Teste de Template Pago [Seção intitulada “Teste de Template Pago”](#teste-de-template-pago) ```typescript import { setupTemplateFlowMocks } from "../helpers"; test.describe("Paid Template Flow", () => { test.beforeEach(async ({ page }) => { await setupTemplateFlowMocks(page, { templateId: "paid-123", templateSlug: "premium-template", planId: "plan-premium", planPrice: 9900, }); }); test("should show checkout", async ({ page }) => { // Teste aqui... }); }); ``` ### Teste com Erro de Login [Seção intitulada “Teste com Erro de Login”](#teste-com-erro-de-login) ```typescript import { setupCommonApiMocks, setupAuthMocks } from "../helpers"; test.describe("Login Errors", () => { test.beforeEach(async ({ page }) => { await setupCommonApiMocks(page); await setupAuthMocks(page, { invalidCredentials: ["wrong@example.com"], }); }); test("should show error for invalid credentials", async ({ page }) => { // Teste aqui... }); }); ``` ### Customizando Tracking [Seção intitulada “Customizando Tracking”](#customizando-tracking) ```typescript import { setupTemplateFlowMocks } from "../helpers"; test.beforeEach(async ({ page }) => { const trackedEvents: any[] = []; await setupTemplateFlowMocks(page, { templateSlug: "test-template", }); // Override tracking para capturar eventos await page.route("**/api/*/track", async (route) => { const data = route.request().postDataJSON(); trackedEvents.push(data); await route.fulfill({ status: 200, body: "{}" }); }); }); ``` ## Boas Práticas [Seção intitulada “Boas Práticas”](#boas-práticas) ### ✅ Fazer [Seção intitulada “✅ Fazer”](#-fazer) ```typescript // ✅ Usar helpers pré-configurados await setupTemplateFlowMocks(page, { templateSlug: "test" }); // ✅ Customizar apenas o necessário await setupAuthMocks(page, { invalidCredentials: ["bad@test.com"] }); // ✅ Setup modular quando não precisa de tudo await setupCommonApiMocks(page); await setupAuthMocks(page); ``` ### ❌ Evitar [Seção intitulada “❌ Evitar”](#-evitar) ```typescript // ❌ Não criar mocks manualmente await page.route('**/api/console/login', async (route) => { await route.fulfill({ ... }); // Use setupAuthMocks() em vez disso }); // ❌ Não duplicar código entre testes test.beforeEach(async ({ page }) => { // 50 linhas de mocks manuais... ❌ }); ``` ## Estendendo os Helpers [Seção intitulada “Estendendo os Helpers”](#estendendo-os-helpers) Se precisar de novos mocks, **adicione no arquivo `api-mocks.ts`**: ```typescript export const setupMyNewMocks = async (page: Page) => { await page.route("**/api/my-endpoint", async (route) => { await createJsonResponse(route, { data: "test" }); }); }; ``` ## Referências [Seção intitulada “Referências”](#referências) * [Playwright Route Mocking](https://playwright.dev/docs/network#handle-requests) * [Utilitários Compartilhados](/frontend/testing/utils/shared) - Fixtures compartilhadas
# E2E
> Testes End-to-End com Playwright
Este diretório contém os testes End-to-End (E2E) da aplicação Gaio Console usando Playwright. > 📚 **Documentação completa**: Veja [Arquitetura de Testes](/frontend/testing/overview/) para arquitetura geral e guias de testes. ## Estrutura [Seção intitulada “Estrutura”](#estrutura) ```plaintext e2e/ ├── pages/ # Page Objects (seguindo o Page Object Model) │ ├── base.page.ts # Classe base com funcionalidades comuns │ ├── register.page.ts │ ├── verify-email-*.page.ts │ ├── forgot-password-*.page.ts │ ├── reset-password.page.ts │ ├── accept-invite.page.ts │ ├── template-preview.page.ts │ ├── template-install-login.page.ts │ ├── template-install-contact.page.ts │ ├── template-install-wizard-profile.page.ts │ ├── template-install-wizard-discovery.page.ts │ ├── template-install-checkout.page.ts │ └── template-install-activation.page.ts ├── specs/ # Arquivos de teste │ ├── signup-verify-email.spec.ts │ ├── forgot-password.spec.ts │ ├── accept-invite.spec.ts │ ├── template-install-free.spec.ts # Testes de instalação de template gratuito │ └── template-install-paid.spec.ts # Testes de instalação de template pago └── README.md ``` ## Como Executar [Seção intitulada “Como Executar”](#como-executar) ### Pré-requisitos [Seção intitulada “Pré-requisitos”](#pré-requisitos) Instale os browsers do Playwright (apenas necessário na primeira vez): ```bash npx playwright install ``` ### Executar Testes [Seção intitulada “Executar Testes”](#executar-testes) ```bash # Executar todos os testes E2E npm run test:e2e # Executar com interface visual npm run test:e2e:ui # Executar em modo debug npm run test:e2e:debug # Ver relatório após execução npm run test:e2e:report # Code generator (para criar novos testes) npm run test:e2e:codegen ``` ### Executar do Diretório Raiz [Seção intitulada “Executar do Diretório Raiz”](#executar-do-diretório-raiz) ```bash # Do diretório raiz do monorepo npm run test:e2e # Roda testes em todos os workspaces npm run test:e2e:ui # Roda com UI em todos os workspaces ``` ## Page Object Model [Seção intitulada “Page Object Model”](#page-object-model) Os testes seguem o padrão **Page Object Model (POM)**, onde cada página da aplicação tem uma classe correspondente que encapsula: * Seletores de elementos * Ações (click, fill, etc.) * Verificações específicas da página ## API Mocks Padronizados [Seção intitulada “API Mocks Padronizados”](#api-mocks-padronizados) Todos os testes E2E devem usar os **helpers de API mocks** para garantir consistência. Veja [helpers/api-mocks.ts](./helpers/api-mocks.ts). ### Exemplo Básico [Seção intitulada “Exemplo Básico”](#exemplo-básico) ```typescript import { setupTemplateFlowMocks } from '../helpers'; test.beforeEach(async ({ page }) => { // Setup completo de mocks para fluxo de template await setupTemplateFlowMocks(page, { templateId: 'template-123', templateSlug: 'test-template', planId: null // FREE template }); }); ``` ### Exemplo Avançado (Customizado) [Seção intitulada “Exemplo Avançado (Customizado)”](#exemplo-avançado-customizado) ```typescript import { setupCommonApiMocks, setupAuthMocks, setupTemplateMocks } from '../helpers'; test.beforeEach(async ({ page }) => { // Setup modular de mocks await setupCommonApiMocks(page); await setupAuthMocks(page, { invalidCredentials: ['invalid@example.com'], existingEmails: ['existing@example.com'] }); await setupTemplateMocks(page, { templateId: 'my-template', planId: 'plan-123', planPrice: 9900 }); }); ``` ### Helpers Disponíveis [Seção intitulada “Helpers Disponíveis”](#helpers-disponíveis) | Helper | Descrição | | -------------------------- | ------------------------------------------ | | `setupCommonApiMocks()` | Mocks básicos (user, connections, account) | | `setupAuthMocks()` | Login, signup, email verification | | `setupProfileMocks()` | Update de perfil e contato | | `setupTemplateMocks()` | Templates e instalação | | `setupPaymentMocks()` | Stripe, subscriptions | | `setupTrackingMocks()` | Analytics e tracking | | `setupTemplateFlowMocks()` | **Tudo acima** (fluxo completo) | Veja documentação completa em [helpers/api-mocks.ts](./helpers/api-mocks.ts). ### Exemplo [Seção intitulada “Exemplo”](#exemplo) ```typescript import { ForgotPasswordRequestPage } from '../pages/forgot-password-request.page'; test('should submit email', async ({ page }) => { const requestPage = new ForgotPasswordRequestPage(page); await requestPage.goto(); await requestPage.submitEmail('user@example.com'); // Página navega automaticamente para /verify }); ``` ## Browsers Suportados [Seção intitulada “Browsers Suportados”](#browsers-suportados) Os testes rodam em múltiplos browsers e viewports: * **Desktop**: Chromium, Firefox, WebKit (Safari) * **Mobile**: Chrome (Pixel 5), Safari (iPhone 12) Para rodar apenas em um browser específico: ```bash npx playwright test --project=chromium npx playwright test --project=firefox npx playwright test --project=webkit ``` ## Configuração [Seção intitulada “Configuração”](#configuração) A configuração está em [playwright.config.ts](../playwright.config.ts). ### Principais Configurações [Seção intitulada “Principais Configurações”](#principais-configurações) * **baseURL**: `http://localhost:5173` * **Retry**: 2 vezes apenas no CI * **Screenshot**: Capturado apenas em falhas * **Trace**: Coletado apenas no primeiro retry * **Video**: Mantido apenas em falhas * **Dev Server**: Inicia automaticamente com `npm run serve` ## Escrevendo Novos Testes [Seção intitulada “Escrevendo Novos Testes”](#escrevendo-novos-testes) 1. **Criar Page Object** (se necessário): e2e/pages/my-page.page.ts ```typescript import { BasePage } from './base.page'; export class MyPage extends BasePage { readonly myButton: Locator; constructor(page: Page) { super(page); this.myButton = page.getByRole('button', { name: /my button/i }); } async clickMyButton() { await this.myButton.click(); } } ``` 2. **Criar Teste**: e2e/specs/my-feature.spec.ts ```typescript import { test, expect } from '@playwright/test'; import { MyPage } from '../pages/my-page.page'; test('should do something', async ({ page }) => { const myPage = new MyPage(page); await myPage.goto('/my-path'); await myPage.clickMyButton(); await expect(myPage.myButton).toBeDisabled(); }); ``` ## Dicas [Seção intitulada “Dicas”](#dicas) * Use `test.only()` para rodar apenas um teste específico * Use `test.skip()` para pular um teste temporariamente * Use `page.pause()` para debug interativo * Use code generator para gerar seletores: `npm run test:e2e:codegen` ## Testes de Template Installation [Seção intitulada “Testes de Template Installation”](#testes-de-template-installation) Os testes de instalação de templates cobrem o fluxo completo de instalação de templates **gratuitos** e **pagos**: ### Fluxo de Template Gratuito (`template-install-free.spec.ts`) [Seção intitulada “Fluxo de Template Gratuito (template-install-free.spec.ts)”](#fluxo-de-template-gratuito-template-install-freespects) Testa o fluxo sem checkout: 1. Preview do template 2. Login/Autenticação 3. Informações de contato 4. Wizard de perfil 5. Wizard de discovery + consentimento 6. Ativação automática (sem pagamento) 7. Sucesso **Eventos de tracking validados:** * `template_viewed` * `template_install_clicked` * `auth_started`, `auth_completed` * `profile_viewed`, `profile_completed` * `checkout_submitted`, `checkout_completed` (sem `checkout_viewed`) * `success_viewed` * `funnel_abandoned` (quando aplicável) ### Fluxo de Template Pago (`template-install-paid.spec.ts`) [Seção intitulada “Fluxo de Template Pago (template-install-paid.spec.ts)”](#fluxo-de-template-pago-template-install-paidspects) Testa o fluxo com checkout: 1-5. Mesmos passos do fluxo gratuito 6. **Checkout** (formulário de pagamento Stripe) 7. Ativação após pagamento 8. Sucesso **Eventos de tracking adicionais:** * `checkout_viewed` (apenas para templates pagos) * `checkout_error` (em caso de falha no pagamento) * Todas as propriedades incluem `is_paid: true`, `plan_id`, `coupon_id` ### Executar apenas testes de template [Seção intitulada “Executar apenas testes de template”](#executar-apenas-testes-de-template) ```bash # Apenas templates gratuitos npx playwright test template-install-free # Apenas templates pagos npx playwright test template-install-paid # Todos os testes de template npx playwright test template-install ``` ## Documentação [Seção intitulada “Documentação”](#documentação) * [Playwright Docs](https://playwright.dev) * [Page Object Model](https://playwright.dev/docs/pom) * [Best Practices](https://playwright.dev/docs/best-practices)
# Visão Geral
> Estrutura e organização (Vitest vs Playwright)
Estrutura organizada para evitar conflitos entre frameworks de teste (Vitest vs Playwright). ## Estrutura [Seção intitulada “Estrutura”](#estrutura) ```plaintext packages/console/ ├── test-shared/ # ✅ Utils compartilhados (SEM dependências de framework) │ ├── fixtures/ # Dados fixos (tokens, users, ids) │ └── factories/ # Criadores de objetos │ ├── test-utils/ # 🧪 Utils específicos do Vitest │ ├── handlers/ # MSW handlers │ ├── middlewares/ # MSW middlewares │ └── server.ts # MSW server setup │ └── e2e/ # 🎭 Testes Playwright ├── pages/ # Page Objects └── specs/ # Testes E2E ``` ## Regras de Importação [Seção intitulada “Regras de Importação”](#regras-de-importação) ### ✅ Permitido [Seção intitulada “✅ Permitido”](#-permitido) ```typescript // Testes Vitest podem importar de: import { TOKENS } from "../../test-shared"; // ✅ Shared import { mswServer } from "../../test-utils"; // ✅ Vitest utils // Testes Playwright podem importar de: import { TOKENS } from "../../test-shared"; // ✅ Shared import { LoginPage } from "../pages/login"; // ✅ Page Objects ``` ### ❌ Proibido (causa conflitos) [Seção intitulada “❌ Proibido (causa conflitos)”](#-proibido-causa-conflitos) ```typescript // ❌ Playwright NÃO deve importar de test-utils (tem dependências Vitest) import { mswServer } from "../../test-utils"; // ❌ ERRO! // ❌ Vitest NÃO deve importar de e2e/pages (são Page Objects do Playwright) import { LoginPage } from "../../e2e/pages"; // ❌ ERRO! ``` ## Framework Agnostic [Seção intitulada “Framework Agnostic”](#framework-agnostic) **Propósito**: Utils compartilhados sem dependências externas de testes. **Características**: * ✅ Zero dependências de frameworks de teste * ✅ Apenas JavaScript/TypeScript puro * ✅ Importável por Vitest E Playwright * ✅ Fixtures simples e factories leves **Conteúdo**: test-shared/fixtures/tokens.ts ```typescript export const TOKENS = { valid: () => generateMockToken(), expired: () => generateMockToken(expiredTime), }; // test-shared/factories/user.ts export const createUser = (overrides = {}) => ({ id: "test-user", name: "Test User", ...overrides, }); ``` ## Vitest Específico [Seção intitulada “Vitest Específico”](#vitest-específico) **Propósito**: Setup e mocks específicos do Vitest (MSW, etc). **Características**: * Apenas para testes Vitest * Pode ter dependências pesadas (MSW, faker, etc) * Inclui server MSW e handlers HTTP * Setup de testes de integração **Conteúdo**: test-utils/server.ts ```typescript import { setupServer } from "msw/node"; export const mswServer = setupServer(...handlers); // test-utils/handlers/auth.handlers.ts export const authHandlers = [ http.post("/api/login", () => { return HttpResponse.json({ token: TOKENS.valid() }); }), ]; ``` ## Playwright Específico [Seção intitulada “Playwright Específico”](#playwright-específico) **Propósito**: Testes End-to-End com Playwright. **Características**: * Testa fluxos completos da aplicação * Page Object Model * Foca em interação do usuário * Usa `test-shared/` para fixtures **Conteúdo**: e2e/pages/login.page.ts ```typescript export class LoginPage extends BasePage { async login(email, password) { ... } } // e2e/specs/login.spec.ts test('should login', async ({ page }) => { const loginPage = new LoginPage(page); await loginPage.login('user@test.com', 'pass'); }); ``` ## Por que essa separação? [Seção intitulada “Por que essa separação?”](#por-que-essa-separação) ### Problema Anterior [Seção intitulada “Problema Anterior”](#problema-anterior) ```plaintext ❌ Testes Playwright importavam de `test-utils/` ↓ `test-utils/` tinha dependências Vitest (MSW com Vitest) ↓ Playwright carregava código do Vitest ↓ CONFLITO: "Cannot redefine Symbol($$jest-matchers-object)" ``` ### Solução Atual [Seção intitulada “Solução Atual”](#solução-atual) ```plaintext ✅ Testes Playwright importam de `test-shared/` ↓ `test-shared/` NÃO tem dependências de framework ↓ Playwright carrega apenas código puro ✅ SEM CONFLITOS ``` ## Checklist ao Criar Utils [Seção intitulada “Checklist ao Criar Utils”](#checklist-ao-criar-utils) Antes de adicionar algo em `test-shared/`: * [ ] É framework-agnostic? (sem `vitest`, `@playwright/test`, `msw`) * [ ] É simples? (sem lógica complexa) * [ ] Será usado por Vitest E Playwright? * [ ] Não tem dependências externas pesadas? Se qualquer resposta for **NÃO**, coloque em: * `test-utils/` se for específico do Vitest * `e2e/` se for específico do Playwright ## Exemplos de Uso [Seção intitulada “Exemplos de Uso”](#exemplos-de-uso) ### Criar novo token fixture [Seção intitulada “Criar novo token fixture”](#criar-novo-token-fixture) test-shared/fixtures/tokens.ts ```typescript export const TOKENS = { admin: () => generateMockToken({ role: "admin" }), }; ``` ### Criar handler MSW [Seção intitulada “Criar handler MSW”](#criar-handler-msw) test-utils/handlers/templates.handlers.ts ```typescript export const templateHandlers = [ http.get("/api/templates", () => { return HttpResponse.json([createTemplate()]); }), ]; ``` ### Criar Page Object [Seção intitulada “Criar Page Object”](#criar-page-object) e2e/pages/template.page.ts ```typescript export class TemplatePage extends BasePage { async installTemplate() { ... } } ``` ## Documentação [Seção intitulada “Documentação”](#documentação) * [Utilitários Compartilhados](/frontend/testing/utils/shared) - Utils compartilhados * [Testes Playwright](/frontend/testing/e2e) - Testes Playwright * [Utils Vitest](/frontend/testing/utils/overview) - Utils Vitest (se existir)
# Vitest / MSW
Utilitários específicos para testes **Vitest** (unitários e de integração) usando **Mock Service Worker (MSW)** para interceptação de requisições HTTP. > **Documentação completa**: [Arquitetura de Testes](/frontend/testing/overview) **Importante**: Estes utils contêm dependências do Vitest e **NÃO devem** ser importados em testes Playwright. Use os [Utilitários Compartilhados](/frontend/testing/utils/shared) para código compartilhado. ## Estrutura [Seção intitulada “Estrutura”](#estrutura) ```plaintext test-utils/ ├── handlers/ # MSW handlers para interceptar APIs │ └── auth.ts # Handlers de autenticação ├── middlewares/ # MSW middlewares customizados ├── helpers/ # Helpers específicos do Vitest ├── fixtures/ # Fixtures específicos do Vitest ├── factories/ # Factories com dependências Vitest ├── server.ts # Setup do MSW server └── index.ts # Exportações centralizadas ``` ## Uso [Seção intitulada “Uso”](#uso) ### Setup MSW no Vitest [Seção intitulada “Setup MSW no Vitest”](#setup-msw-no-vitest) O MSW server é configurado automaticamente no `vitest.setup.tsx`: ```typescript import { beforeAll, afterAll, afterEach } from "vitest"; import { mswServer } from "./test-utils"; // Inicia o server antes de todos os testes beforeAll(() => mswServer.listen({ onUnhandledRequest: "error" })); // Reseta handlers após cada teste afterEach(() => mswServer.resetHandlers()); // Para o server após todos os testes afterAll(() => mswServer.close()); ``` ### Renderizar componente com React Query [Seção intitulada “Renderizar componente com React Query”](#renderizar-componente-com-react-query) ```typescript import { render, screen } from '@/test-utils'; import { MyComponent } from './my-component'; describe('MyComponent', () => { it('should fetch and display data', async () => { render(); // MSW intercepta automaticamente as requests expect(await screen.findByText('Data loaded')).toBeInTheDocument(); }); }); ``` ### Customizar handlers por teste [Seção intitulada “Customizar handlers por teste”](#customizar-handlers-por-teste) ```typescript import { render, screen } from '@/test-utils'; import { mswServer } from '@/test-utils'; import { http, HttpResponse } from 'msw'; describe('MyComponent with errors', () => { it('should handle API errors', async () => { // Override handler apenas para este teste mswServer.use( http.get('/api/data', () => { return HttpResponse.json({ error: 'Failed' }, { status: 500 }); }) ); render(); expect(await screen.findByText('Error occurred')).toBeInTheDocument(); }); }); ``` ## Handlers Disponíveis [Seção intitulada “Handlers Disponíveis”](#handlers-disponíveis) Os handlers estão organizados por domínio em `handlers/`: * **auth.ts**: Login, signup, logout, token refresh * *(Adicione novos handlers conforme necessário)* ## Boas Práticas [Seção intitulada “Boas Práticas”](#boas-práticas) ### ✅ Fazer [Seção intitulada “✅ Fazer”](#-fazer) ```typescript // ✅ Importar de test-utils em testes Vitest import { render, mswServer } from "@/test-utils"; import { TOKENS } from "@/test-shared"; // Shared é OK ``` ### ❌ NÃO Fazer [Seção intitulada “❌ NÃO Fazer”](#-não-fazer) ```typescript // ❌ Importar test-utils em testes Playwright import { mswServer } from "../../test-utils"; // ERRO! // ❌ Importar Page Objects do Playwright aqui import { LoginPage } from "../../e2e/pages/login"; // ERRO! ``` ## Adicionar Novos Handlers [Seção intitulada “Adicionar Novos Handlers”](#adicionar-novos-handlers) 1. Crie um arquivo em `handlers/` (ex: `handlers/user.ts`) 2. Exporte os handlers: handlers/user.ts ```typescript import { http, HttpResponse } from "msw"; export const userHandlers = [ http.get("/api/console/current-user", () => { return HttpResponse.json({ id: "user-123", email: "test@example.com", name: "Test User", }); }), http.patch("/api/console/profile", async ({ request }) => { const body = await request.json(); return HttpResponse.json({ ...body, updated: true }); }), ]; ``` 3. Registre no `server.ts`: server.ts ```typescript import { setupServer } from "msw/node"; import { authHandlers } from "./handlers/auth"; import { userHandlers } from "./handlers/user"; // Novo export const mswServer = setupServer( ...authHandlers, ...userHandlers, // Adiciona aqui ); ``` ## Troubleshooting [Seção intitulada “Troubleshooting”](#troubleshooting) ### Requests não estão sendo interceptadas [Seção intitulada “Requests não estão sendo interceptadas”](#requests-não-estão-sendo-interceptadas) **Problema**: Componente faz request mas MSW não intercepta. **Solução**: 1. Verifique se o handler está registrado no `server.ts` 2. Confirme que o URL do handler corresponde exatamente à request 3. Verifique se `mswServer.listen()` foi chamado no setup ### Erro “Cannot redefine property” [Seção intitulada “Erro “Cannot redefine property””](#erro-cannot-redefine-property) **Problema**: Erro ao rodar testes Playwright. **Solução**: Você está importando `test-utils` em testes Playwright. Use apenas `test-shared` em testes E2E. ### Handler não está sendo aplicado [Seção intitulada “Handler não está sendo aplicado”](#handler-não-está-sendo-aplicado) **Problema**: Override com `mswServer.use()` não funciona. **Solução**: * Use `mswServer.use()` ANTES de renderizar o componente * Se ainda não funcionar, use `mswServer.resetHandlers()` antes ## 📖 Links Úteis [Seção intitulada “📖 Links Úteis”](#-links-úteis) * [MSW Documentation](https://mswjs.io/) * [Vitest](https://vitest.dev/guide/) * [React Testing Library](https://testing-library.com/react) * [Arquitetura de Testes](/frontend/testing/overview)
# Compartilhados
> Utilitários de teste compartilhados entre Vitest e Playwright
Utilitários de teste **compartilhados** e **independentes de framework**, que podem ser usados tanto nos testes do **Vitest** quanto do **Playwright**. > **Documentação completa**: [Arquitetura de Testes](/frontend/testing/overview) ## Objetivo [Seção intitulada “Objetivo”](#objetivo) Evitar conflitos entre frameworks de teste mantendo fixtures e factories sem dependências externas de teste (Vitest, Playwright, MSW, etc). ## Estrutura [Seção intitulada “Estrutura”](#estrutura) ```plaintext test-shared/ ├── fixtures/ # Dados fixos para testes │ ├── ids.ts # IDs de teste consistentes │ ├── tokens.ts # Geração de tokens mock │ └── users.ts # Usuários pré-configurados ├── factories/ # Criadores de objetos de teste │ └── user.ts # Factory de usuários └── index.ts # Exportações centralizadas ``` ## Uso [Seção intitulada “Uso”](#uso) ### Em testes do Playwright (E2E) [Seção intitulada “Em testes do Playwright (E2E)”](#em-testes-do-playwright-e2e) ```typescript import { TOKENS, createUser, TEST_IDS } from "../../test-shared"; test("should authenticate", async ({ page }) => { await page.route("**/api/login", async (route) => { await route.fulfill({ body: JSON.stringify({ access_token: TOKENS.valid(), refresh_token: TOKENS.refresh(), }), }); }); }); ``` ### Em testes do Vitest (Unit/Integration) [Seção intitulada “Em testes do Vitest (Unit/Integration)”](#em-testes-do-vitest-unitintegration) ```typescript import { TOKENS, createUser } from "../../test-shared"; describe("Auth Service", () => { it("should parse token", () => { const token = TOKENS.valid(); expect(parseToken(token)).toBeDefined(); }); }); ``` ## Disponível [Seção intitulada “Disponível”](#disponível) ### Fixtures [Seção intitulada “Fixtures”](#fixtures) * **`TOKENS`**: Gerador de tokens JWT mock * `TOKENS.valid()` - Token válido por 1h * `TOKENS.expired()` - Token expirado * `TOKENS.refresh()` - Refresh token * `TOKENS.impersonate.valid()` - Token de impersonação * **`TEST_IDS`**: IDs consistentes para testes * `TEST_IDS.users.default` - ID do usuário padrão * `TEST_IDS.accounts.default` - ID da conta padrão * **`TEST_USERS`**: Usuários pré-configurados * `TEST_USERS.default` - Usuário padrão * `TEST_USERS.admin` - Usuário admin ### Factories [Seção intitulada “Factories”](#factories) * **`createUser(overrides?)`**: Cria usuário com valores padrão ```typescript const user = createUser({ name: "Custom Name" }); ``` ## 🚫 O que NÃO colocar aqui [Seção intitulada “🚫 O que NÃO colocar aqui”](#-o-que-não-colocar-aqui) * ❌ Setups do MSW (vai em `test-utils/`) * ❌ Configurações do Vitest (vai em `vitest.config.ts`) * ❌ Page Objects do Playwright (vai em `e2e/pages/`) * ❌ Dependências externas pesadas (faker, etc) ## ✅ O que colocar aqui [Seção intitulada “✅ O que colocar aqui”](#-o-que-colocar-aqui) * ✅ Fixtures simples e estáticas * ✅ Factories sem dependências externas * ✅ Constantes de teste * ✅ Geradores de dados mock simples * ✅ Tipos compartilhados para testes ## Diferença entre `test-shared/` e `test-utils/` [Seção intitulada “Diferença entre test-shared/ e test-utils/”](#diferença-entre-test-shared-e-test-utils) | Aspecto | `test-shared/` | `test-utils/` | | ------------------ | ------------------- | ------------------- | | **Framework** | Agnóstico | Específico Vitest | | **Dependências** | Nenhuma | MSW, Vitest, etc | | **Uso** | Vitest + Playwright | Apenas Vitest | | **Conteúdo** | Fixtures, Factories | Handlers MSW, Setup | | **Importável por** | Qualquer teste | Só testes Vitest | ## Boas Práticas [Seção intitulada “Boas Práticas”](#boas-práticas) 1. **Mantenha simples**: Sem lógica complexa ou dependências externas 2. **Seja consistente**: Use sempre os mesmos IDs/valores nos testes 3. **Documente**: Adicione JSDoc explicando o propósito de cada fixture 4. **Exporte tudo**: Use `index.ts` para exportações centralizadas 5. **Evite estado**: Factories devem ser funções puras quando possível