) {
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