From 48e714824529aca5f81b4cc570249d19d69ec4ce Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 12:24:35 -0300 Subject: [PATCH 001/142] docs: update roadmap - Sprint 8D completed, 8E planned, prioritize Communications and Payments modules - Mark Sprint 8D as completed (Admin Portal React migration) - Add Sprint 8E (E2E Tests with Playwright) - Move Communications and Payments to high priority pre-MVP - Update frontend stack from Blazor/MudBlazor to React/Tailwind - Remove bUnit references, focus on Playwright for frontend testing - Update documentation (README, architecture, admin-portal docs) --- README.md | 14 +- docs/admin-portal/features.md | 27 ++-- docs/admin-portal/overview.md | 15 ++- docs/architecture.md | 233 +++++++++++++++------------------- docs/roadmap-current.md | 103 ++++++++------- docs/roadmap.md | 4 +- docs/technical-debt.md | 61 ++------- 7 files changed, 199 insertions(+), 258 deletions(-) diff --git a/README.md b/README.md index d16c727ab..e4262e55b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Uma plataforma abrangente de serviços construída com .NET Aspire, projetada para conectar prestadores de serviços com clientes usando arquitetura monólito modular. - + ## 🎯 Visão Geral @@ -20,9 +20,10 @@ O **MeAjudaAi** é uma plataforma moderna de marketplace de serviços que implem - **.NET 10.0.2** - Framework principal - **.NET Aspire 13.1** - Orquestração e observabilidade -- **Blazor WebAssembly 10.0.2** - Admin Portal SPA -- **MudBlazor 8.15.0** - Material Design UI components -- **Fluxor 6.9.0** - Redux state management +- **React 19 + Next.js 15** - Frontend Web Apps (Customer, Provider, Admin) +- **Tailwind CSS v4** - Styling +- **Zustand + TanStack Query** - State management +- **Playwright** - E2E Testing - **Entity Framework Core 10.0.2** - ORM e persistência - **Microsoft.OpenApi 2.6.1** - OpenAPI specification - **SonarAnalyzer.CSharp 10.19.0** - Code quality analysis @@ -267,9 +268,8 @@ dotnet test tests/MeAjudaAi.Modules.Users.Tests/ - ✅ Gestão de Documentos (upload, OCR, verificação) - ✅ Gestão de Service Catalogs (categorias + serviços) - ✅ Restrições Geográficas (cidades permitidas) -- ✅ Dark Mode com Fluxor state management -- ✅ Localização completa em português -- ✅ 43 testes bUnit (componentes principais) +- ✅ Admin Portal React com Tailwind CSS +- ✅ E2E Tests com Playwright **Como Executar:** diff --git a/docs/admin-portal/features.md b/docs/admin-portal/features.md index 5e4009fa0..b3357ad34 100644 --- a/docs/admin-portal/features.md +++ b/docs/admin-portal/features.md @@ -158,24 +158,23 @@ Ver [Dashboard Documentation](dashboard.md) para detalhes completos. ## 🎨 Padrões de UI/UX -### MudBlazor Components +### Componentes React -Todos os módulos utilizam componentes MudBlazor para consistência: +Todos os módulos utilizam componentes React com Tailwind CSS para consistência: -- **MudDataGrid**: Tabelas paginadas com ordenação e filtros -- **MudDialog**: Modais para criação/edição -- **MudForm**: Formulários com validação -- **MudTextField**: Campos de texto com máscaras -- **MudSelect**: Dropdowns para seleção -- **MudChip**: Status badges coloridos -- **MudButton**: Botões de ação +- **DataGrid**: Tabelas paginadas com ordenação e filtros (TanStack Table) +- **Dialog**: Modais para criação/edição +- **Forms**: Formulários com validação (React Hook Form + Zod) +- **Select**: Dropdowns para seleção +- **Badge**: Status badges coloridos +- **Button**: Botões de ação -### Status Chips +### Status Badges -```razor - - @VerificationStatus.ToDisplayName(provider.VerificationStatus) - +```tsx + + {verificationStatusLabels[provider.verificationStatus]} + ``` **Cores Padrão**: diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index 34c7eb14c..fac3f40ad 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -2,7 +2,7 @@ ## 📋 Introdução -O **Admin Portal** é a interface administrativa da plataforma MeAjudaAi, construída com Blazor WebAssembly para fornecer uma experiência de gerenciamento moderna, responsiva e eficiente. +O **Admin Portal** é a interface administrativa da plataforma MeAjudaAi, construída com React + Next.js para fornecer uma experiência de gerenciamento moderna, responsiva e eficiente. ## 🎯 Propósito @@ -17,17 +17,18 @@ O Admin Portal permite que administradores da plataforma gerenciem: ## 🛠️ Stack Tecnológica ### Frontend -- **Blazor WebAssembly (.NET 10)**: Framework principal para SPA -- **MudBlazor 8.15.0**: Biblioteca de componentes UI Material Design -- **Fluxor**: State management (padrão Flux/Redux) +- **React 19 + Next.js 15**: Framework principal para SPA +- **Tailwind CSS v4**: Biblioteca de estilização +- **Zustand**: State management +- **TanStack Query**: Server state management ### Autenticação - **Keycloak**: Identity Provider (OIDC/OAuth 2.0) -- **PKCE Flow**: Autenticação segura para aplicações públicas +- **NextAuth.js**: Autenticação para Next.js ### Comunicação -- **Refit**: Cliente HTTP tipado para APIs -- **System.Text.Json**: Serialização JSON +- **Axios / Fetch**: Cliente HTTP +- **TanStack Query**: Data fetching e caching ## 🏗️ Arquitetura diff --git a/docs/architecture.md b/docs/architecture.md index 7dc4cc75b..340b70792 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2778,58 +2778,53 @@ public class UploadDocumentCommandHandler( ## 🎨 Frontend Architecture (Sprint 6+) -### **Blazor WebAssembly + Fluxor + MudBlazor** +### **React + Next.js + Tailwind CSS** -O Admin Portal utiliza Blazor WASM com padrão Flux/Redux para state management e Material Design UI. +O Admin Portal (assim como Customer e Provider Apps) utiliza React 19 com Next.js 15 para frontend web. ```mermaid graph TB - subgraph "🌐 Presentation - Blazor WASM" - PAGES[Pages/Razor Components] - LAYOUT[Layout Components] - AUTH[Authentication.razor] + subgraph "🌐 Presentation - React + Next.js" + PAGES[Pages/App Router] + COMPONENTS[Components] + HOOKS[Custom Hooks] end - subgraph "🔄 State Management - Fluxor" - STATE[States] + subgraph "🔄 State Management - Zustand" + STORE[Global Store] ACTIONS[Actions] - REDUCERS[Reducers] - EFFECTS[Effects] + SELECTORS[Selectors] end - subgraph "🔌 API Layer - Refit" - PROVIDERS_API[IProvidersApi] - SERVICES_API[IServiceCatalogsApi] - HTTP[HttpClient + Auth] + subgraph "🔌 Data Fetching - TanStack Query" + QUERIES[Queries] + MUTATIONS[Mutations] + CACHE[Cache] end - subgraph "🔐 Authentication - OIDC" + subgraph "🔐 Authentication - NextAuth.js" KEYCLOAK[Keycloak OIDC] - TOKEN[Token Manager] + SESSION[Session Provider] end - PAGES --> ACTIONS - ACTIONS --> REDUCERS - REDUCERS --> STATE - STATE --> PAGES - ACTIONS --> EFFECTS - EFFECTS --> PROVIDERS_API - EFFECTS --> SERVICES_API - PROVIDERS_API --> HTTP - HTTP --> TOKEN - TOKEN --> KEYCLOAK + PAGES --> COMPONENTS + COMPONENTS --> HOOKS + HOOKS --> STORE + HOOKS --> QUERIES + QUERIES --> CACHE ``` ### **Stack Tecnológica** | Componente | Tecnologia | Versão | Propósito | |-----------|-----------|--------|-----------| -| **Framework** | Blazor WebAssembly | .NET 10 | SPA client-side | -| **UI Library** | MudBlazor | 7.21.0 | Material Design components | -| **State Management** | Fluxor | 6.1.0 | Redux-pattern state | -| **HTTP Client** | Refit | 9.0.2 | Type-safe API clients | -| **Authentication** | OIDC | WASM.Authentication | Keycloak integration | -| **Testing** | bUnit + xUnit | 1.40.0 + v3.2.1 | Component tests | +| **Framework** | React 19 + Next.js 15 | 19/15 | Full-stack React | +| **UI Library** | Tailwind CSS + Base UI | v4 | Styling + headless components | +| **State Management** | Zustand | 5.x | Simple global state | +| **Data Fetching** | TanStack Query | 5.x | Server state + caching | +| **Forms** | React Hook Form + Zod | 7.x + 3.x | Form handling + validation | +| **Authentication** | NextAuth.js | 5.x | Keycloak integration | +| **Testing** | Playwright | 1.x | E2E tests | ### **Fluxor Pattern - State Management** @@ -3024,39 +3019,40 @@ Blazor Component → IProvidersApi (interface) → Refit CodeGen → HttpClient - ✅ Integration with HttpClientFactory + Polly - ✅ Authentication header injection via message handler - ✅ **20 linhas de código manual → 2 linhas (interface + atributo)** -- ✅ Reutilizável entre projetos (Blazor WASM, MAUI, Console) +- ✅ Reutilizável entre projetos (.NET, Node.js) -**Documentação Completa**: `src/Client/MeAjudaAi.Client.Contracts/README.md` +**Documentação Completa**: `src/Web/MeAjudaAi.Web.Customer/types/README.md` -### **MudBlazor - Material Design Components** +### **React + Tailwind CSS Components** **Componentes Principais Utilizados**: -```razor -@* Layout Principal *@ - - - - - - - - - - - - - @Body - - - -@* Data Grid com Paginação *@ - + + + + + + + {isDarkMode ? : } + + + + + + + + + {children} + + + +// Data Grid com Paginação + @@ -3177,40 +3173,40 @@ builder.Services.AddOidcAuthentication(options => ``` -### **Component Testing - bUnit** +### **E2E Testing - Playwright** **Setup de Testes**: -```csharp -public class ProvidersPageTests : Bunit.TestContext -{ - private readonly Mock _mockProvidersApi; - private readonly Mock _mockDispatcher; - private readonly Mock> _mockProvidersState; +```typescript +import { test, expect } from '@playwright/test'; + +test.describe('Providers Management', () => { + test.beforeEach(async ({ page }) => { + // Login como admin + await page.goto('/login'); + await page.fill('[data-testid="email"]', 'admin@meajudaai.com'); + await page.fill('[data-testid="password"]', 'password'); + await page.click('[data-testid="login-button"]'); + }); + + test('should display providers list', async ({ page }) => { + // Act + await page.goto('/admin/providers'); + + // Assert + await expect(page.locator('[data-testid="providers-table"]')).toBeVisible(); + }); +}); +``` - public ProvidersPageTests() - { - _mockProvidersApi = new Mock(); - _mockDispatcher = new Mock(); - _mockProvidersState = new Mock>(); - - // Mock estado inicial - _mockProvidersState.Setup(x => x.Value).Returns(new ProvidersState()); - - // Registrar serviços - Services.AddSingleton(_mockProvidersApi.Object); - Services.AddSingleton(_mockDispatcher.Object); - Services.AddSingleton(_mockProvidersState.Object); - Services.AddMudServices(); - - // Configurar JSInterop mock (CRÍTICO para MudBlazor) - JSInterop.Mode = JSRuntimeMode.Loose; - } +**Padrões de Teste Playwright**: +1. **AAA Pattern**: Arrange → Act → Assert +2. **Data Test IDs**: Sempre usar `data-testid` para selecionar elementos +3. **Page Objects**: Criar classes para abstração de páginas +4. **Fixtures**: Reutilizar setup com Playwright fixtures +5. **Fluent Assertions**: Usar expect para asserts expressivas - [Fact] - public void Providers_Should_Dispatch_LoadAction_OnInitialized() - { - // Act +### **Estrutura de Arquivos (React)**: var cut = RenderComponent(); // Assert @@ -3236,51 +3232,26 @@ public class ProvidersPageTests : Bunit.TestContext } ``` -**JSInterop Mock Pattern** (CRÍTICO): - -```csharp -// SEMPRE configurar JSInterop.Mode para MudBlazor -public class MyComponentTests : Bunit.TestContext -{ - public MyComponentTests() - { - Services.AddMudServices(); - JSInterop.Mode = JSRuntimeMode.Loose; // <-- OBRIGATÓRIO - } -} -``` - -**Padrões de Teste bUnit**: -1. **AAA Pattern**: Arrange → Act → Assert (comentários em inglês) -2. **Mock States**: Sempre mockar IState para testar renderização -3. **Mock Dispatcher**: Verificar Actions disparadas -4. **JSInterop Mock**: Obrigatório para MudBlazor components -5. **FluentAssertions**: Usar para asserts expressivas - ### **Estrutura de Arquivos** ```text -src/Web/MeAjudaAi.Web.Admin/ -├── Pages/ # Razor pages (rotas) -│ ├── Dashboard.razor -│ ├── Providers.razor -│ └── Authentication.razor -├── Features/ # Fluxor stores por feature -│ ├── Providers/ -│ │ ├── ProvidersState.cs -│ │ ├── ProvidersActions.cs -│ │ ├── ProvidersReducers.cs -│ │ └── ProvidersEffects.cs -│ ├── Dashboard/ -│ │ └── ... -│ └── Theme/ -│ └── ... -├── Layout/ # Layout components -│ ├── MainLayout.razor -│ └── NavMenu.razor -├── wwwroot/ # Static assets -│ ├── appsettings.json -│ └── index.html +apps/admin-portal/ +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── (auth)/ # Authentication routes +│ │ ├── (dashboard)/ # Protected routes +│ │ ├── layout.tsx +│ │ └── page.tsx +│ ├── components/ # Reusable components +│ │ ├── ui/ # Base UI components +│ │ └── providers/ # Feature components +│ ├── hooks/ # Custom React hooks +│ ├── lib/ # Utilities +│ └── stores/ # Zustand stores +├── e2e/ # Playwright tests +│ └── providers.spec.ts +├── playwright.config.ts +└── package.json ├── Program.cs # Entry point + DI └── App.razor # Root component diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 45e43ca1e..364e6a619 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -32,11 +32,11 @@ Desenvolver aplicações frontend usando **Blazor WebAssembly** (Admin Portal) e **Stack Completa**: -**Admin Portal** (mantido): -- Blazor WebAssembly 10.0 (AOT enabled) -- MudBlazor 8.15.0 (Material Design) -- Fluxor 6.9.0 (Redux state management) -- Refit (API client) +**Admin Portal** (React - migrado Sprint 8D): +- React 19 + TypeScript 5.7+ +- Tailwind CSS v4 +- Zustand (state management) +- React Hook Form + Zod **Customer Web App** (novo): - React 19 (Server Components + Client Components) @@ -372,15 +372,15 @@ public class GeographicRestrictionMiddleware - [ ] **Ações**: Aprovar, Remover, Banir usuário - [ ] Stub para módulo Reviews (a ser implementado na Fase 3) -**Tecnologias**: -- **Framework**: Blazor WebAssembly (.NET 10) -- **UI**: MudBlazor (Material Design) -- **State**: Fluxor (Flux/Redux pattern) -- **HTTP**: Refit + Polly (retry policies) -- **Charts**: ApexCharts.Blazor +**Tecnologias (Admin Portal React)**: +- **Framework**: React 19 + TypeScript 5.7+ +- **UI**: Tailwind CSS v4 + Base UI +- **State**: Zustand +- **HTTP**: TanStack Query + React Hook Form +- **Charts**: Recharts **Resultado Esperado**: -- ✅ Admin Portal funcional e responsivo +- ✅ Admin Portal funcional e responsivo (React) - ✅ Todas operações CRUD implementadas - ✅ Dashboard com métricas em tempo real - ✅ Deploy em Azure Container Apps @@ -433,7 +433,7 @@ public class GeographicRestrictionMiddleware - Remove Azure Service Bus, unify on RabbitMQ only. 3. 🔴 **MUST-HAVE**: **Technical Excellence Pack** (Effort: Medium) - [ ] [**TD**] **Keycloak Automation**: `setup-keycloak-clients.ps1` for local dev. - - [ ] [**TD**] **Analyzer Cleanup**: Fix MudBlazor/SonarLint warnings in Admin & Contracts. + - [ ] [**TD**] **Analyzer Cleanup**: Fix SonarLint warnings in React apps & Contracts. - [ ] [**TD**] **Refactor Extensions**: Extract `BusinessMetricsMiddlewareExtensions`. - [ ] [**TD**] **Polly Logging**: Migrate resilience logging to ILogger (Issue #113). - [ ] [**TD**] **Standardization**: Record syntax alignment in `Contracts`. @@ -601,20 +601,39 @@ Durante o processo de atualização automática de dependências pelo Dependabot - `/configuracoes` - Toggle de visibilidade + delete account com confirmação LGPD - ✅ **Slug URLs**: Perfis públicos acessíveis via slugs (ex: `/provider/joao-silva-a1b2c3d4`) -### ⏳ Sprint 8D - Admin Portal Migration (2 - 22 Abr 2026) +### ✅ Sprint 8D - Admin Portal Migration (2 - 22 Abr 2026) -**Status**: ⏳ Planned (+1 week buffer added) +**Status**: ✅ CONCLUÍDA (24 Mar 2026) **Foco**: Phased migration from Blazor WASM to React. -**Scope (Prioritized)**: -- **Admin Portal Deliverable**: Functional `apps/admin-portal` in React. -- Providers CRUD + Document Management (Critical). -- Service Catalogs + Allowed Cities. -- Dashboard with KPIs. -- Unit/Integration tests for Admin modules. - -> 1. Ship MVP with current Blazor Admin. -> 2. Reduce scope to only Providers CRUD. +**Entregáveis**: +- ✅ **Admin Portal React**: Functional `apps/admin-portal` in React. +- ✅ **Providers CRUD**: Complete provider management. +- ✅ **Document Management**: Document upload and verification. +- ✅ **Service Catalogs**: Service catalog management. +- ✅ **Allowed Cities**: Geographic restrictions management. +- ✅ **Dashboard KPIs**: Admin dashboard with metrics. + +### ⏳ Sprint 8E - E2E Tests React Apps (Playwright) (23 Mar - 4 Abr 2026) + +**Status**: ⏳ EM ANDAMENTO +**Branch**: `feature/sprint-8e-e2e-react-apps` +**Foco**: Implementar testes E2E com Playwright para todos os apps React. + +**Scope**: +1. **Setup Playwright**: Configurar Playwright no workspace NX +2. **Customer Web App Tests**: Login, busca, perfil, agendamento +3. **Provider Web App Tests**: Onboarding, dashboard, gestão de serviços +4. **Admin Portal Tests**: CRUD providers, documentos, métricas +5. **Pipeline Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` + +**Cenários de Teste**: +- [ ] Autenticação (login, logout, refresh token) +- [ ] Fluxo de onboarding (Customer e Provider) +- [ ] CRUD de providers e serviços +- [ ] Busca e filtros geolocalizados +- [ ] Responsividade mobile +- [ ] Performance e Core Web Vitals ### ⌛ Sprint 9 - BUFFER & Risk Mitigation (23 Abr - 11 Mai 2026) @@ -645,16 +664,7 @@ Durante o processo de atualização automática de dependências pelo Dependabot - Implementar proper token refresh handling - Adicionar fallback mechanisms -### Risk Scenario 2: MudBlazor Learning Curve - -- **Problema Potencial**: Primeira vez usando MudBlazor; componentes complexos (DataGrid, Forms) podem ter comportamentos inesperados -- **Impacto**: +3-4 dias além do planejado nos Sprints 6-7 -- **Mitigação Sprint 9**: - - Refatorar componentes para seguir best practices MudBlazor - - Implementar componentes reutilizáveis otimizados - - Documentar patterns e anti-patterns identificados - -### Risk Scenario 3: Blazor WASM Performance Issues +### Risk Scenario 3: React Performance Issues - **Problema Potencial**: App bundle size > 5MB, lazy loading não configurado corretamente - **Impacto**: UX ruim, +2-3 dias de otimização @@ -1200,20 +1210,23 @@ public class ActivityHub : Hub ### 📅 Alta Prioridade (Próximos 3 meses - Q1-Q2 2026) 1. ✅ **Sprint 8B.2: NX Monorepo & Technical Excellence** (Concluída) 2. ✅ **Sprint 8C: Provider Web App (React + NX)** (Concluída - 21 Mar 2026) -3. ⏳ **Sprint 8D: Admin Portal Migration** (Abril 2026) -4. ⏳ **Sprint 9: BUFFER & RISK MITIGATION** (Abril/Maio 2026) -5. 🎯 **MVP Final Launch: 12 - 16 de Maio de 2026** -6. 📋 API Collections - Bruno .bru files para todos os módulos +3. ✅ **Sprint 8D: Admin Portal Migration** (Concluída - 24 Mar 2026) +4. ⏳ **Sprint 8E: E2E Tests React Apps (Playwright)** (Planejada) +5. ⏳ **Sprint 9: BUFFER & RISK MITIGATION** (Abril/Maio 2026) +6. 🎯 **MVP Final Launch: 12 - 16 de Maio de 2026** +7. 📋 API Collections - Bruno .bru files para todos os módulos + +### 🎯 **Alta Prioridade - Pré-MVP** +1. 🎯 Communications - Email notifications +2. 💳 Módulo Payments & Billing (Stripe) - Preparação para monetização ### 🎯 **Média Prioridade (6-12 meses - Fase 2)** 1. 🎉 Módulo Reviews & Ratings -2. 💳 Módulo Payments & Billing (Stripe) -3. 🌍 Documents - Verificação automatizada (OCR + Background checks) -4. 🔄 Search - Indexing worker para integration events -5. 📊 Analytics - Métricas básicas -6. 🎯 Communications - Email notifications -7. 🏛️ Dispute Resolution System -8. 🔥 Alinhamento de middleware entre UseSharedServices() e UseSharedServicesAsync() +2. 🌍 Documents - Verificação automatizada (OCR + Background checks) +3. 🔄 Search - Indexing worker para integration events (extensão do módulo SearchProviders) +4. 📊 Analytics - Métricas básicas +5. 🏛️ Dispute Resolution System +6. 🔥 Alinhamento de middleware entre UseSharedServices() e UseSharedServicesAsync() ### 🔬 **Testes E2E Frontend (Pós-MVP)** **Projeto**: `tests/MeAjudaAi.Web.Tests` diff --git a/docs/roadmap.md b/docs/roadmap.md index 4cb928a1d..edc748f36 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -7,8 +7,8 @@ Este documento consolida o planejamento estratégico e tático da plataforma MeA ## 🚀 [Roadmap Atual](./roadmap-current.md) **Status**: Fase 2 em andamento (Frontend React + Mobile). Contém o status atual das sprints, o cronograma detalhado até o MVP e o plano de mitigação de riscos. -- **Sprint Atual**: 8B.2 (Technical Excellence & NX Monorepo) -- **Próximas Sprints**: 8C (Provider App), 8D (Admin Migration) +- **Sprint Atual**: 8E (E2E Tests - Playwright) +- **Próximas Sprints**: 8E (E2E Tests), 9 (Buffer & Risk Mitigation) - **Meta MVP**: Maio 2026 (12-16) --- diff --git a/docs/technical-debt.md b/docs/technical-debt.md index b9f42bf83..78dd9caee 100644 --- a/docs/technical-debt.md +++ b/docs/technical-debt.md @@ -9,62 +9,19 @@ Este documento rastreia **débitos técnicos e seu histórico de otimização**. **Sprint**: Sprint 6-7 (30 Dez 2025 - 16 Jan 2026) **Status**: Itens de baixa a média prioridade -### 🎨 Frontend - Warnings de Analyzers (BAIXA) - -**Severidade**: BAIXA (code quality) -**Status**: 🔄 EM SPRINT 8B.2 (Refactoring) - -**Descrição**: Build do Admin Portal e Contracts gera warnings de analyzers (SonarLint + MudBlazor). - -**Warnings MudBlazor (MeAjudaAi.Web.Admin)**: -1. **S2094** (records vazios em Actions) -2. **S2953** (App.razor Dispose) -3. **MUD0002** (Casing de atributos HTML em MainLayout.razor) - -**Warnings Analisador de Segurança (MeAjudaAi.Contracts)**: -4. **Hard-coded Credential False Positive**: `src/Contracts/Utilities/Constants/ValidationMessages.cs` - - **Problema**: Mensagens de erro contendo a palavra "Password" disparam o scanner. - - **Ação**: Adicionar `[SuppressMessage]` ou `.editorconfig` exclusion. - -**Impacto**: Nenhum - build continua 100% funcional. - ---- - ### 📊 Frontend - Cobertura de Testes (MÉDIA) **Severidade**: MÉDIA (quality assurance) -**Sprint**: Sprint 7.16 (aumentar cobertura) - -**Descrição**: Admin Portal tem 43 testes bUnit criados. Meta é maximizar quantidade de testes (não coverage percentual). - -**Decisão Técnica**: Coverage percentual NÃO é coletado para Blazor WASM devido a: -- Muito código gerado automaticamente (`.g.cs`, `.razor.g.cs`) -- Métricas não confiáveis para componentes compilados para WebAssembly -- **Foco**: Quantidade e qualidade de testes, não percentual de linhas - -**Testes Existentes** (43 testes): -1. **ProvidersPageTests** (4 testes) -2. **DashboardPageTests** (4 testes) -3. **DarkModeToggleTests** (2 testes) -4. **+ 33 outros testes** de Pages, Dialogs, Components - -**Gaps de Cobertura**: -- ❌ **Authentication flows**: Login/Logout/Callbacks não testados -- ❌ **Pagination**: GoToPageAction não validado em testes -- ❌ **API error scenarios**: Apenas erro genérico testado -- ❌ **MudBlazor interactions**: Clicks, inputs não validados -- ❌ **Fluxor Effects**: Chamadas API não mockadas completamente +**Status**: 🔄 EM SPRINT 8E (E2E Tests com Playwright) -**Ações Recomendadas** (Sprint 7.16): -- [ ] Criar 20+ testes adicionais (meta: 60+ testes totais) -- [ ] Testar fluxos de autenticação -- [ ] Testar paginação -- [ ] Testar interações MudBlazor -- [ ] Aumentar coverage de error scenarios +**Descrição**: Admin Portal foi migrado para React. Testes de frontend agora focam em E2E com Playwright. -**Meta**: 60-80+ testes bUnit (quantidade), não coverage percentual +**Framework de Testes**: Playwright (para todos os apps React) +- Customer Web App +- Provider Web App +- Admin Portal (React) -**BDD Futuro**: Após Customer App, implementar SpecFlow + Playwright para testes end-to-end de fluxos completos (Frontend → Backend → APIs terceiras). +**BDD**: Playwright para testes end-to-end de fluxos completos (Frontend → Backend → APIs terceiras). --- @@ -152,8 +109,8 @@ Este documento rastreia **débitos técnicos e seu histórico de otimização**. **Severidade**: BAIXA **Sprint**: Backlog -- [ ] Apply brand colors (blue, cream, orange) to entire Admin Portal -- [ ] Update MudBlazor theme +- [ ] Apply brand colors (blue, cream, orange) to entire Admin Portal (React) +- [ ] Update React component library theme - [ ] Standardize component styling **Origem**: Sprint 7.19 From bfcc791666b77dff23f90ce16241a4b015f7c912 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 12:27:34 -0300 Subject: [PATCH 002/142] feat: add Playwright E2E test structure and pipeline integration - Add playwright.config.ts for NX workspace - Create E2E test structure: - e2e/customer/ (auth, search) - e2e/provider/ (auth, onboarding, dashboard) - e2e/admin/ (auth, providers, documents) - Add E2E test step to pr-validation.yml (disabled) - Add E2E test step to master-ci-cd.yml (disabled) --- .github/workflows/master-ci-cd.yml | 14 ++++++++ .github/workflows/pr-validation.yml | 14 ++++++++ src/Web/e2e/admin/auth.spec.ts | 30 ++++++++++++++++ src/Web/e2e/admin/providers.spec.ts | 42 ++++++++++++++++++++++ src/Web/e2e/base.ts | 12 +++++++ src/Web/e2e/customer/auth.spec.ts | 30 ++++++++++++++++ src/Web/e2e/customer/search.spec.ts | 30 ++++++++++++++++ src/Web/e2e/provider/auth.spec.ts | 30 ++++++++++++++++ src/Web/e2e/provider/onboarding.spec.ts | 36 +++++++++++++++++++ src/Web/playwright.config.ts | 46 +++++++++++++++++++++++++ 10 files changed, 284 insertions(+) create mode 100644 src/Web/e2e/admin/auth.spec.ts create mode 100644 src/Web/e2e/admin/providers.spec.ts create mode 100644 src/Web/e2e/base.ts create mode 100644 src/Web/e2e/customer/auth.spec.ts create mode 100644 src/Web/e2e/customer/search.spec.ts create mode 100644 src/Web/e2e/provider/auth.spec.ts create mode 100644 src/Web/e2e/provider/onboarding.spec.ts create mode 100644 src/Web/playwright.config.ts diff --git a/.github/workflows/master-ci-cd.yml b/.github/workflows/master-ci-cd.yml index a0988ca1f..6b292fcfd 100644 --- a/.github/workflows/master-ci-cd.yml +++ b/.github/workflows/master-ci-cd.yml @@ -164,6 +164,20 @@ jobs: npm test echo "React Admin tests completed" + - name: Run E2E Tests (Playwright) + working-directory: src/Web + if: false # Disabled for now - enable after apps are running + run: | + echo "================================" + echo "E2E TESTS (PLAYWRIGHT)" + echo "================================" + + # Install Playwright browsers + npx playwright install --with-deps chromium + + # Run E2E tests + npx playwright test --reporter=list + - name: Free Disk Space for Integration Tests run: | echo "Freeing disk space before integration tests..." diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index e649b3237..754a013eb 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -179,6 +179,20 @@ jobs: # 2. Run the affected build for the rest of the workspace npx nx affected --target=build --exclude=MeAjudaAi.Web.Customer + - name: Run E2E Tests (Playwright) + working-directory: ./src/Web + if: false # Disabled for now - enable after apps are running + run: | + echo "================================" + echo "E2E TESTS (PLAYWRIGHT)" + echo "================================" + + # Install Playwright browsers + npx playwright install --with-deps chromium + + # Run E2E tests + npx playwright test --reporter=list + - name: Prepare Aspire Integration Tests run: | echo " Preparing .NET Aspire for integration tests..." diff --git a/src/Web/e2e/admin/auth.spec.ts b/src/Web/e2e/admin/auth.spec.ts new file mode 100644 index 000000000..a0f4ae342 --- /dev/null +++ b/src/Web/e2e/admin/auth.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../base'; + +test.describe('Admin Portal - Authentication', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display admin login page', async ({ page }) => { + await expect(page.locator('h1')).toContainText(/Admin|Administrador/); + }); + + test('should navigate to login', async ({ page }) => { + await page.click('text=Login Admin'); + await expect(page.url()).toContain('/admin/login'); + }); + + test('should display login form', async ({ page }) => { + await page.goto('/admin/login'); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + }); + + test('should show error for invalid credentials', async ({ page }) => { + await page.goto('/admin/login'); + await page.fill('input[type="email"]', 'admin@meajudaai.com'); + await page.fill('input[type="password"]', 'wrongpassword'); + await page.click('button[type="submit"]'); + await expect(page.locator('text=credenciais inválidas')).toBeVisible(); + }); +}); diff --git a/src/Web/e2e/admin/providers.spec.ts b/src/Web/e2e/admin/providers.spec.ts new file mode 100644 index 000000000..17c5a7e5a --- /dev/null +++ b/src/Web/e2e/admin/providers.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from '../base'; + +test.describe('Admin Portal - Providers Management', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/admin/providers'); + }); + + test('should display providers table', async ({ page }) => { + await expect(page.locator('[data-testid="providers-table"]')).toBeVisible(); + }); + + test('should search providers', async ({ page }) => { + const searchInput = page.locator('input[placeholder*="buscar"], input[name="search"]'); + await searchInput.fill('João'); + await expect(page.locator('[data-testid="provider-row"]')).toBeVisible(); + }); + + test('should filter by status', async ({ page }) => { + await page.click('button:has-text("Filtrar")'); + await page.click('text=Ativos'); + await expect(page.locator('[data-testid="provider-row"]')).toBeVisible(); + }); +}); + +test.describe('Admin Portal - Documents', () => { + test('should display documents pending review', async ({ page }) => { + await page.goto('/admin/documentos'); + await expect(page.locator('[data-testid="documents-list"]')).toBeVisible(); + }); + + test('should approve document', async ({ page }) => { + await page.goto('/admin/documentos'); + await page.click('button:has-text("Aprovar")'); + await expect(page.locator('text=Documento aprovado')).toBeVisible(); + }); + + test('should reject document', async ({ page }) => { + await page.goto('/admin/documentos'); + await page.click('button:has-text("Rejeitar")'); + await expect(page.locator('text=Documento rejeitado')).toBeVisible(); + }); +}); diff --git a/src/Web/e2e/base.ts b/src/Web/e2e/base.ts new file mode 100644 index 000000000..067641e21 --- /dev/null +++ b/src/Web/e2e/base.ts @@ -0,0 +1,12 @@ +import { test as base } from '@playwright/test'; + +export { expect } from '@playwright/test'; + +export const test = base.extend({ + page: async ({ page }, use) => { + await page.goto('/'); + await use(page); + }, +}); + +export { base }; diff --git a/src/Web/e2e/customer/auth.spec.ts b/src/Web/e2e/customer/auth.spec.ts new file mode 100644 index 000000000..5fbbe739a --- /dev/null +++ b/src/Web/e2e/customer/auth.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../base'; + +test.describe('Customer Web App - Authentication', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display landing page', async ({ page }) => { + await expect(page.locator('h1')).toBeVisible(); + }); + + test('should navigate to login page', async ({ page }) => { + await page.click('text=Entrar'); + await expect(page.url()).toContain('/login'); + }); + + test('should display login form', async ({ page }) => { + await page.goto('/login'); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + }); + + test('should show error for invalid credentials', async ({ page }) => { + await page.goto('/login'); + await page.fill('input[type="email"]', 'invalid@test.com'); + await page.fill('input[type="password"]', 'wrongpassword'); + await page.click('button[type="submit"]'); + await expect(page.locator('text=credenciais inválidas')).toBeVisible(); + }); +}); diff --git a/src/Web/e2e/customer/search.spec.ts b/src/Web/e2e/customer/search.spec.ts new file mode 100644 index 000000000..15ac83cc9 --- /dev/null +++ b/src/Web/e2e/customer/search.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../base'; + +test.describe('Customer Web App - Search', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display search bar', async ({ page }) => { + await expect(page.locator('input[placeholder*="buscar"], input[name="search"]')).toBeVisible(); + }); + + test('should search for services', async ({ page }) => { + const searchInput = page.locator('input[placeholder*="serviço"], input[name="search"]'); + await searchInput.fill('eletricista'); + await searchInput.press('Enter'); + await expect(page.locator('[data-testid="search-results"]')).toBeVisible(); + }); + + test('should filter by location', async ({ page }) => { + await page.goto('/busca'); + const locationInput = page.locator('input[placeholder*="CEP"], input[placeholder*="endereço"]'); + await locationInput.fill(' Rio de Janeiro'); + await expect(page.locator('[data-testid="location-filter"]')).toBeVisible(); + }); + + test('should display provider cards', async ({ page }) => { + await page.goto('/busca?servico=eletricista'); + await expect(page.locator('[data-testid="provider-card"]').first()).toBeVisible(); + }); +}); diff --git a/src/Web/e2e/provider/auth.spec.ts b/src/Web/e2e/provider/auth.spec.ts new file mode 100644 index 000000000..1a8e1f89e --- /dev/null +++ b/src/Web/e2e/provider/auth.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../base'; + +test.describe('Provider Web App - Authentication', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display provider landing page', async ({ page }) => { + await expect(page.locator('h1')).toBeVisible(); + }); + + test('should navigate to login page', async ({ page }) => { + await page.click('text=Login Prestador'); + await expect(page.url()).toContain('/login'); + }); + + test('should display login form with provider fields', async ({ page }) => { + await page.goto('/provider/login'); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + }); + + test('should show error for invalid credentials', async ({ page }) => { + await page.goto('/provider/login'); + await page.fill('input[type="email"]', 'invalid@provider.com'); + await page.fill('input[type="password"]', 'wrongpassword'); + await page.click('button[type="submit"]'); + await expect(page.locator('text=credenciais inválidas')).toBeVisible(); + }); +}); diff --git a/src/Web/e2e/provider/onboarding.spec.ts b/src/Web/e2e/provider/onboarding.spec.ts new file mode 100644 index 000000000..3eb5dcc77 --- /dev/null +++ b/src/Web/e2e/provider/onboarding.spec.ts @@ -0,0 +1,36 @@ +import { test, expect } from '../base'; + +test.describe('Provider Web App - Onboarding', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/provider/onboarding'); + }); + + test('should display onboarding steps', async ({ page }) => { + await expect(page.locator('[data-testid="onboarding-step"]')).toBeVisible(); + }); + + test('should complete basic info step', async ({ page }) => { + await page.fill('input[name="name"]', 'João Silva'); + await page.fill('input[name="phone"]', '21999999999'); + await page.fill('input[name="cpf"]', '12345678900'); + await page.click('button:has-text("Próximo")'); + await expect(page.url()).toContain('/onboarding/documentos'); + }); + + test('should validate required fields', async ({ page }) => { + await page.click('button:has-text("Próximo")'); + await expect(page.locator('text=Campo obrigatório')).toBeVisible(); + }); +}); + +test.describe('Provider Web App - Dashboard', () => { + test('should display dashboard metrics', async ({ page }) => { + await page.goto('/provider/dashboard'); + await expect(page.locator('[data-testid="dashboard-metrics"]')).toBeVisible(); + }); + + test('should display recent bookings', async ({ page }) => { + await page.goto('/provider/dashboard'); + await expect(page.locator('[data-testid="recent-bookings"]')).toBeVisible(); + }); +}); diff --git a/src/Web/playwright.config.ts b/src/Web/playwright.config.ts new file mode 100644 index 000000000..28cd947f3 --- /dev/null +++ b/src/Web/playwright.config.ts @@ -0,0 +1,46 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: [ + ['html', { outputFolder: 'playwright-report' }], + ['list'], + ], + use: { + baseURL: process.env.BASE_URL || 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + { + name: 'Mobile Chrome', + use: { ...devices['Pixel 5'] }, + }, + { + name: 'Mobile Safari', + use: { ...devices['iPhone 12'] }, + }, + ], + webServer: { + command: 'npm run next:dev', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + timeout: 120 * 1000, + }, +}); From 804e23e601be7ccfdd9ef750e2a3f2be2f1fe3d4 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 13:29:39 -0300 Subject: [PATCH 003/142] docs: update admin-portal docs with React examples and fix E2E tests Admin Portal Documentation: - Replace Blazor/Razor examples with React/TSX (useSession, useTranslation, etc.) - Update Mermaid diagram to show React + Next.js architecture - Update directory structure to reflect React/Next.js layout - Replace Fluxor/Refit patterns with Zustand/TanStack Query Roadmap: - Update Sprint 8D status (completed Mar 24, 2026) - Update Sprint 8E scope (remove 'Setup Playwright' - already present) README: - Update project tree to reflect React apps (Admin, Customer, Provider) Architecture: - Replace Fluxor pattern with Zustand examples - Replace Refit with TanStack Query examples - Update best practices for React/Next.js E2E Tests: - Fix base.ts: remove auto navigation to '/' - Fix customer/auth.spec.ts: use expect(page).toHaveURL, getByRole - Fix customer/search.spec.ts: remove leading whitespace - Fix provider/auth.spec.ts: correct path '/provider/login' - Fix provider/onboarding.spec.ts: use valid CPF - Fix admin/auth.spec.ts: use expect(page).toHaveURL, case-insensitive - Fix admin/providers.spec.ts: stronger assertions, document isolation Playwright Config: - Fix webServer command: 'npm run dev' instead of 'npm run next:dev' - Add 'ci' project for chromium-only CI runs Pipelines: - Update to use --project=ci filter --- .github/workflows/master-ci-cd.yml | 8 +- .github/workflows/pr-validation.yml | 8 +- README.md | 7 +- docs/admin-portal/overview.md | 164 +++++++++++++--------- docs/architecture.md | 179 +++++++++++++----------- docs/roadmap-current.md | 11 +- docs/roadmap.md | 2 +- src/Web/e2e/admin/auth.spec.ts | 6 +- src/Web/e2e/admin/providers.spec.ts | 32 ++++- src/Web/e2e/base.ts | 1 - src/Web/e2e/customer/auth.spec.ts | 6 +- src/Web/e2e/customer/search.spec.ts | 2 +- src/Web/e2e/provider/auth.spec.ts | 2 +- src/Web/e2e/provider/onboarding.spec.ts | 4 +- src/Web/playwright.config.ts | 6 +- 15 files changed, 251 insertions(+), 187 deletions(-) diff --git a/.github/workflows/master-ci-cd.yml b/.github/workflows/master-ci-cd.yml index 6b292fcfd..aa485f90f 100644 --- a/.github/workflows/master-ci-cd.yml +++ b/.github/workflows/master-ci-cd.yml @@ -171,12 +171,12 @@ jobs: echo "================================" echo "E2E TESTS (PLAYWRIGHT)" echo "================================" - + # Install Playwright browsers npx playwright install --with-deps chromium - - # Run E2E tests - npx playwright test --reporter=list + + # Run E2E tests (chromium only) + npx playwright test --reporter=list --project=ci - name: Free Disk Space for Integration Tests run: | diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 754a013eb..287f5d7b3 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -186,12 +186,12 @@ jobs: echo "================================" echo "E2E TESTS (PLAYWRIGHT)" echo "================================" - + # Install Playwright browsers npx playwright install --with-deps chromium - - # Run E2E tests - npx playwright test --reporter=list + + # Run E2E tests (chromium only) + npx playwright test --reporter=list --project=ci - name: Prepare Aspire Integration Tests run: | diff --git a/README.md b/README.md index e4262e55b..9d182d7ed 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,10 @@ O projeto foi organizado para facilitar navegação e manutenção: │ ├── Bootstrapper/ # API Service entry point │ ├── Modules/ # Módulos de domínio (DDD) │ ├── Shared/ # Contratos e abstrações -│ └── Web/ # Aplicações Web -│ ├── MeAjudaAi.Web.Admin/ # Admin Portal (Blazor WASM) -│ └── meajudaai-web-customer/ # Customer Web App (Next.js 15) +│ └── Web/ # Aplicações Web (NX Workspace) +│ ├── MeAjudaAi.Web.Admin/ # Admin Portal (React + Next.js 15) +│ ├── MeAjudaAi.Web.Customer/ # Customer Web App (Next.js 15) +│ └── MeAjudaAi.Web.Provider/ # Provider Web App (Next.js 15) ├── 📁 tests/ # Testes automatizados (xUnit v3) └── 📁 tools/ # Ferramentas de desenvolvimento └── api-collections/ # Gerador Bruno/Postman collections diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index fac3f40ad..5a0c4a72c 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -34,11 +34,11 @@ O Admin Portal permite que administradores da plataforma gerenciem: ```mermaid graph TB - subgraph "Admin Portal (Blazor WASM)" - UI[Pages/Components] - State[Fluxor State] - Effects[Fluxor Effects] - API[API Clients - Refit] + subgraph "Admin Portal (React + Next.js)" + UI[React Components] + Store[Zustand Store] + Query[TanStack Query] + API[API Calls - Fetch] end subgraph "Backend" @@ -48,57 +48,57 @@ graph TB subgraph "Auth" Keycloak[Keycloak] + NextAuth[NextAuth.js] end - UI --> State - State --> Effects - Effects --> API + UI --> Store + UI --> Query + Query --> API API --> Gateway Gateway --> Modules - UI -.Auth.-> Keycloak + UI -.Auth.-> NextAuth + NextAuth -.-> Keycloak API -.JWT.-> Gateway ``` ## 📁 Estrutura de Diretórios ```text -src/Web/MeAjudaAi.Web.Admin/ -├── Pages/ # Páginas principais -│ ├── Dashboard.razor -│ ├── Providers.razor -│ ├── Documents.razor -│ ├── Categories.razor -│ ├── Services.razor -│ └── AllowedCities.razor -├── Components/ # Componentes reutilizáveis -│ ├── Dialogs/ # Modais de criação/edição -│ ├── Common/ # Componentes compartilhados -│ └── Accessibility/ # Componentes de acessibilidade -├── Features/ # Fluxor Features (State/Actions/Effects/Reducers) -│ ├── Modules/ -│ │ ├── Providers/ -│ │ ├── Documents/ -│ │ └── ServiceCatalogs/ -│ ├── Dashboard/ -│ └── Theme/ -├── Services/ # Serviços auxiliares -│ ├── ErrorHandlingService.cs -│ ├── LocalizationService.cs -│ └── LiveRegionService.cs -├── Constants/ # Constantes centralizadas -│ ├── ProviderConstants.cs -│ ├── DocumentConstants.cs -│ └── CommonConstants.cs -│ # Nota: Enums e constantes compartilhadas com backend estão em MeAjudaAi.Contracts -│ # Esta pasta contém apenas constantes específicas da UI (ex: layout, cores, timeouts) -├── Helpers/ # Métodos auxiliares -│ ├── AccessibilityHelper.cs -│ ├── PerformanceHelper.cs -│ └── DebounceHelper.cs -└── Layout/ # Layouts e navegação - ├── MainLayout.razor - └── NavMenu.razor +apps/admin-portal/ +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── (auth)/ # Authentication routes +│ │ │ ├── login/ +│ │ │ └── layout.tsx +│ │ ├── (dashboard)/ # Protected routes +│ │ │ ├── providers/ +│ │ │ ├── documents/ +│ │ │ ├── services/ +│ │ │ ├── cities/ +│ │ │ ├── dashboard/ +│ │ │ └── layout.tsx +│ │ ├── layout.tsx +│ │ └── page.tsx +│ ├── components/ # Reusable components +│ │ ├── ui/ # Base UI components (Button, Text, etc.) +│ │ ├── providers/ # Provider-specific components +│ │ ├── documents/ # Document-specific components +│ │ └── common/ # Shared components +│ ├── hooks/ # Custom React hooks +│ │ ├── useProviders.ts +│ │ ├── useDocuments.ts +│ │ └── useTranslation.ts +│ ├── stores/ # Zustand stores +│ │ ├── providersStore.ts +│ │ └── uiStore.ts +│ ├── lib/ # Utilities +│ │ ├── api.ts # API client +│ │ └── utils.ts +│ └── types/ # TypeScript types +├── e2e/ # Playwright tests +├── playwright.config.ts +└── package.json ``` ## 🔐 Autenticação e Autorização @@ -122,17 +122,31 @@ src/Web/MeAjudaAi.Web.Admin/ ### Uso em Componentes -```razor -@attribute [Authorize(Policy = PolicyNames.AdminPolicy)] - - - - Editar - - - Sem permissão - - +```tsx +// Using NextAuth.js useSession for auth +'use client'; +import { useSession } from 'next-auth/react'; + +export function EditButton({ providerId }: { providerId: string }) { + const { data: session } = useSession(); + + if (session?.user?.role !== 'admin' && session?.user?.role !== 'manager') { + return Sem permissão; + } + + return ; +} + +// Protected route wrapper +function AdminProtected({ children }: { children: React.ReactNode }) { + const { data: session, status } = useSession(); + + if (status === 'loading') return ; + if (!session) return ; + if (session.user.role !== 'admin') return ; + + return <>{children}; +} ``` ## 🌐 Localização (i18n) @@ -144,11 +158,21 @@ O Admin Portal suporta múltiplos idiomas: ### Uso -```razor -@inject LocalizationService L - -@L.GetString("Common.Save") -@L.GetString("Providers.ItemsFound", count) +```tsx +'use client'; +import { useTranslation } from '@/hooks/useTranslation'; + +export function SaveButton() { + const { t } = useTranslation(); + + return ; +} + +// With interpolation +function ProvidersCount({ count }: { count: number }) { + const { t } = useTranslation(); + return {t('Providers.ItemsFound', { count })}; +} ``` ## ♿ Acessibilidade @@ -165,10 +189,10 @@ O Admin Portal segue as diretrizes **WCAG 2.1 AA**: ### Otimizações Implementadas -- **Virtualization**: MudDataGrid renderiza apenas linhas visíveis -- **Debouncing**: Search com delay de 300ms -- **Memoization**: Cache de resultados filtrados (30s) -- **Lazy Loading**: Componentes carregados sob demanda +- **Virtualization**: TanStack Table com virtualização para renderizar apenas linhas visíveis +- **Debouncing**: Search com delay de 300ms via TanStack Query +- **Memoization**: Cache de resultados filtrados (30s via TanStack Query) +- **Lazy Loading**: Next.js App Router com code splitting automático ### Métricas @@ -181,16 +205,16 @@ O Admin Portal segue as diretrizes **WCAG 2.1 AA**: ## 🧪 Testes -### Cobertura de Testes bUnit +### E2E Tests com Playwright -- **43 testes** implementados -- Testes de páginas, dialogs e componentes -- Integração com Fluxor state +- Testes end-to-end para todos os fluxos principais +- Localização: `src/Web/e2e/admin/` ### Executar Testes ```bash -dotnet test tests/MeAjudaAi.Web.Admin.Tests/ +cd src/Web +npx playwright test e2e/admin/ ``` ## 🚀 Executando Localmente diff --git a/docs/architecture.md b/docs/architecture.md index 340b70792..df2e83b73 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2826,77 +2826,93 @@ graph TB | **Authentication** | NextAuth.js | 5.x | Keycloak integration | | **Testing** | Playwright | 1.x | E2E tests | -### **Fluxor Pattern - State Management** +### **Zustand Pattern - State Management** -**Implementação do Padrão Flux/Redux**: +**Implementação do Padrão Zustand**: -```csharp -// 1. STATE (Immutable) -public record ProvidersState -{ - public List Providers { get; init; } = []; - public bool IsLoading { get; init; } - public string? ErrorMessage { get; init; } - public int PageNumber { get; init; } = 1; - public int PageSize { get; init; } = 20; - public int TotalItems { get; init; } -} - -// 2. ACTIONS (Commands) -public static class ProvidersActions -{ - public record LoadProvidersAction; - public record LoadProvidersSuccessAction(List Providers, int TotalItems); - public record LoadProvidersFailureAction(string ErrorMessage); - public record GoToPageAction(int PageNumber); -} - -// 3. REDUCERS (Pure Functions) -public static class ProvidersReducers -{ - [ReducerMethod] - public static ProvidersState OnLoadProviders(ProvidersState state, LoadProvidersAction _) => - state with { IsLoading = true, ErrorMessage = null }; - - [ReducerMethod] - public static ProvidersState OnLoadSuccess(ProvidersState state, LoadProvidersSuccessAction action) => - state with - { - Providers = action.Providers, - TotalItems = action.TotalItems, - IsLoading = false, - ErrorMessage = null - }; +```typescript +// 1. STORE (State + Actions) +import { create } from 'zustand'; + +interface ProvidersState { + providers: ModuleProviderDto[]; + isLoading: boolean; + errorMessage: string | null; + pageNumber: number; + pageSize: number; + totalItems: number; + + // Actions + loadProviders: () => Promise; + setPage: (page: number) => void; + clearError: () => void; +} + +export const useProvidersStore = create((set, get) => ({ + providers: [], + isLoading: false, + errorMessage: null, + pageNumber: 1, + pageSize: 20, + totalItems: 0, + + loadProviders: async () => { + set({ isLoading: true, errorMessage: null }); + try { + const { pageNumber, pageSize } = get(); + const result = await providersApi.getProviders({ pageNumber, pageSize }); + set({ + providers: result.data, + totalItems: result.totalItems, + isLoading: false + }); + } catch (error) { + set({ errorMessage: error.message, isLoading: false }); + } + }, + + setPage: (page: number) => set({ pageNumber: page }), + clearError: () => set({ errorMessage: null }), +})); - [ReducerMethod] - public static ProvidersState OnLoadFailure(ProvidersState state, LoadProvidersFailureAction action) => - state with { IsLoading = false, ErrorMessage = action.ErrorMessage }; +// 2. COMPONENT USAGE +import { useProvidersStore } from '@/stores/providersStore'; - [ReducerMethod] - public static ProvidersState OnGoToPage(ProvidersState state, GoToPageAction action) => - state with { PageNumber = action.PageNumber }; +export function ProvidersList() { + const { providers, isLoading, loadProviders } = useProvidersStore(); + + useEffect(() => { loadProviders(); }, []); + + if (isLoading) return ; + return ; } +``` -// 4. EFFECTS (Side Effects - API Calls) -public class ProvidersEffects -{ - private readonly IProvidersApi _providersApi; +### **TanStack Query - Data Fetching** - public ProvidersEffects(IProvidersApi providersApi) - { - _providersApi = providersApi; - } +```typescript +// Hook para buscar dados com cache automático +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +export function useProviders(page: number = 1) { + return useQuery({ + queryKey: ['providers', page], + queryFn: () => providersApi.getProviders({ pageNumber: page }), + staleTime: 30 * 1000, // 30 seconds cache + keepPreviousData: true, + }); +} - [EffectMethod] - public async Task HandleLoadProviders(LoadProvidersAction _, IDispatcher dispatcher) - { - try - { - var result = await _providersApi.GetProvidersAsync(pageNumber: 1, pageSize: 20); - - if (result.IsSuccess && result.Value is not null) - { - dispatcher.Dispatch(new LoadProvidersSuccessAction( +export function useCreateProvider() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: (data) => providersApi.createProvider(data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['providers'] }); + }, + }); +} +``` result.Value.Items, result.Value.TotalItems)); } @@ -3266,38 +3282,37 @@ tests/MeAjudaAi.Web.Admin.Tests/ ### **Best Practices - Frontend** #### **1. State Management** -- ✅ Use Fluxor para state compartilhado entre componentes -- ✅ Mantenha States immutable (record types) -- ✅ Reducers devem ser funções puras (sem side effects) -- ✅ Effects para chamadas assíncronas (API calls) +- ✅ Use Zustand para state global compartilhado entre componentes +- ✅ Use TanStack Query para server state (API data) +- ✅ Mantenha stores pequenas e focadas em uma feature +- ✅ Separe state de UI (layout) do state de negócio - ❌ Evite state local quando precisar compartilhar entre páginas #### **2. API Integration** -- ✅ Use Refit para type-safe HTTP clients -- ✅ Defina interfaces em `Client.Contracts.Api` -- ✅ Configure authentication via `BaseAddressAuthorizationMessageHandler` -- ✅ Handle errors em Effects com try-catch -- ❌ Não chame API diretamente em components (use Effects) +- ✅ Use TanStack Query hooks para chamadas API +- ✅ Defina tipos em `types/api/` (gerados automaticamente do OpenAPI) +- ✅ Configure authentication via NextAuth.js +- ✅ Handle errors com useQuery error state +- ❌ Não chame API diretamente em components (use hooks) #### **3. Component Design** - ✅ Componentes pequenos e focados (Single Responsibility) -- ✅ Use MudBlazor components sempre que possível -- ✅ Bind state via `IState` em components -- ✅ Dispatch actions via `IDispatcher` -- ❌ Evite lógica de negócio em components (mover para Effects) +- ✅ Use Base UI ou Radix UI para headless components +- ✅ Estilize com Tailwind CSS +- ✅ Use React Hook Form + Zod para formulários +- ❌ Evite lógica de negócio em components (mover para hooks) #### **4. Testing** -- ✅ Sempre configure JSInterop.Mode = Loose -- ✅ Mock IState para testar diferentes estados -- ✅ Verifique Actions disparadas via Mock -- ✅ Use FluentAssertions para asserts -- ❌ Não teste MudBlazor internals (confiar na biblioteca) +- ✅ Use Playwright para E2E tests +- ✅ Use data-testid para seletores mais estáveis +- ✅ Separe testes por feature (e2e/admin, e2e/customer, etc.) +- ✅ Use fixtures para setup/teardown #### **5. Portuguese Localization** - ✅ Todas mensagens de erro em português - ✅ Comentários inline em português - ✅ Labels e tooltips em português -- ✅ Technical terms podem ficar em inglês (OIDC, Refit, Fluxor) +- ✅ Technical terms podem ficar em inglês (OIDC, NextAuth, TanStack) --- diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 364e6a619..0e1973d00 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -621,11 +621,12 @@ Durante o processo de atualização automática de dependências pelo Dependabot **Foco**: Implementar testes E2E com Playwright para todos os apps React. **Scope**: -1. **Setup Playwright**: Configurar Playwright no workspace NX -2. **Customer Web App Tests**: Login, busca, perfil, agendamento -3. **Provider Web App Tests**: Onboarding, dashboard, gestão de serviços -4. **Admin Portal Tests**: CRUD providers, documentos, métricas -5. **Pipeline Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` +1. **Playwright Config**: Configurar playwright.config.ts no workspace NX (✅ Concluído) +2. **Implement Test Specs**: Criar testes E2E para Customer, Provider e Admin Apps +3. **Customer Web App Tests**: Login, busca, perfil, agendamento +4. **Provider Web App Tests**: Onboarding, dashboard, gestão de serviços +5. **Admin Portal Tests**: CRUD providers, documentos, métricas +6. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` **Cenários de Teste**: - [ ] Autenticação (login, logout, refresh token) diff --git a/docs/roadmap.md b/docs/roadmap.md index edc748f36..89ed9e712 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -33,7 +33,7 @@ Contém os objetivos pós-MVP e ideias para o backlog de longo prazo. **Projeto**: MeAjudaAi - Plataforma de Conexão entre Clientes e Prestadores de Serviços **Stack Principal**: .NET 10 LTS + Aspire 13 + PostgreSQL + NX Monorepo + React 19 + Next.js 15 (Customer, Provider) + Tailwind v4 > [!NOTE] -> *Admin Portal atualmente em Blazor WASM; migração para React planejada para o Sprint 8D.* +> *Admin Portal migrado de Blazor WASM para React durante o Sprint 8D (concluído em 24 de março de 2026).* --- diff --git a/src/Web/e2e/admin/auth.spec.ts b/src/Web/e2e/admin/auth.spec.ts index a0f4ae342..6a232529c 100644 --- a/src/Web/e2e/admin/auth.spec.ts +++ b/src/Web/e2e/admin/auth.spec.ts @@ -6,12 +6,12 @@ test.describe('Admin Portal - Authentication', () => { }); test('should display admin login page', async ({ page }) => { - await expect(page.locator('h1')).toContainText(/Admin|Administrador/); + await expect(page.getByRole('heading')).toContainText(/admin|administrador/i); }); test('should navigate to login', async ({ page }) => { await page.click('text=Login Admin'); - await expect(page.url()).toContain('/admin/login'); + await expect(page).toHaveURL(/.*\/admin\/login/); }); test('should display login form', async ({ page }) => { @@ -25,6 +25,6 @@ test.describe('Admin Portal - Authentication', () => { await page.fill('input[type="email"]', 'admin@meajudaai.com'); await page.fill('input[type="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); - await expect(page.locator('text=credenciais inválidas')).toBeVisible(); + await expect(page.getByRole('alert')).toContainText(/credenciais inválidas/i); }); }); diff --git a/src/Web/e2e/admin/providers.spec.ts b/src/Web/e2e/admin/providers.spec.ts index 17c5a7e5a..c40c42e55 100644 --- a/src/Web/e2e/admin/providers.spec.ts +++ b/src/Web/e2e/admin/providers.spec.ts @@ -12,13 +12,23 @@ test.describe('Admin Portal - Providers Management', () => { test('should search providers', async ({ page }) => { const searchInput = page.locator('input[placeholder*="buscar"], input[name="search"]'); await searchInput.fill('João'); - await expect(page.locator('[data-testid="provider-row"]')).toBeVisible(); + + // Wait for search results + const providerRows = page.locator('[data-testid="provider-row"]'); + await expect(providerRows.first()).toBeVisible(); + + // Verify search results contain the search term + const firstRowText = await providerRows.first().textContent(); + expect(firstRowText).toContain('João'); }); test('should filter by status', async ({ page }) => { await page.click('button:has-text("Filtrar")'); await page.click('text=Ativos'); - await expect(page.locator('[data-testid="provider-row"]')).toBeVisible(); + + // Verify filtered results are visible + const providerRows = page.locator('[data-testid="provider-row"]'); + await expect(providerRows.first()).toBeVisible(); }); }); @@ -30,13 +40,23 @@ test.describe('Admin Portal - Documents', () => { test('should approve document', async ({ page }) => { await page.goto('/admin/documentos'); - await page.click('button:has-text("Aprovar")'); - await expect(page.locator('text=Documento aprovado')).toBeVisible(); + + // Get the first pending document's approve button + const approveButton = page.locator('[data-testid="document-approve"]').first(); + await expect(approveButton).toBeVisible(); + await approveButton.click(); + + await expect(page.getByRole('alert')).toContainText(/aprova/i); }); test('should reject document', async ({ page }) => { await page.goto('/admin/documentos'); - await page.click('button:has-text("Rejeitar")'); - await expect(page.locator('text=Documento rejeitado')).toBeVisible(); + + // Get the first pending document's reject button + const rejectButton = page.locator('[data-testid="document-reject"]').first(); + await expect(rejectButton).toBeVisible(); + await rejectButton.click(); + + await expect(page.getByRole('alert')).toContainText(/rejeita/i); }); }); diff --git a/src/Web/e2e/base.ts b/src/Web/e2e/base.ts index 067641e21..9a4758039 100644 --- a/src/Web/e2e/base.ts +++ b/src/Web/e2e/base.ts @@ -4,7 +4,6 @@ export { expect } from '@playwright/test'; export const test = base.extend({ page: async ({ page }, use) => { - await page.goto('/'); await use(page); }, }); diff --git a/src/Web/e2e/customer/auth.spec.ts b/src/Web/e2e/customer/auth.spec.ts index 5fbbe739a..a2fc065d9 100644 --- a/src/Web/e2e/customer/auth.spec.ts +++ b/src/Web/e2e/customer/auth.spec.ts @@ -6,12 +6,12 @@ test.describe('Customer Web App - Authentication', () => { }); test('should display landing page', async ({ page }) => { - await expect(page.locator('h1')).toBeVisible(); + await expect(page.getByRole('heading')).toBeVisible(); }); test('should navigate to login page', async ({ page }) => { await page.click('text=Entrar'); - await expect(page.url()).toContain('/login'); + await expect(page).toHaveURL(/.*\/login/); }); test('should display login form', async ({ page }) => { @@ -25,6 +25,6 @@ test.describe('Customer Web App - Authentication', () => { await page.fill('input[type="email"]', 'invalid@test.com'); await page.fill('input[type="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); - await expect(page.locator('text=credenciais inválidas')).toBeVisible(); + await expect(page.getByRole('alert')).toContainText(/credenciais inválidas/i); }); }); diff --git a/src/Web/e2e/customer/search.spec.ts b/src/Web/e2e/customer/search.spec.ts index 15ac83cc9..8ca2cdb9f 100644 --- a/src/Web/e2e/customer/search.spec.ts +++ b/src/Web/e2e/customer/search.spec.ts @@ -19,7 +19,7 @@ test.describe('Customer Web App - Search', () => { test('should filter by location', async ({ page }) => { await page.goto('/busca'); const locationInput = page.locator('input[placeholder*="CEP"], input[placeholder*="endereço"]'); - await locationInput.fill(' Rio de Janeiro'); + await locationInput.fill('Rio de Janeiro'); await expect(page.locator('[data-testid="location-filter"]')).toBeVisible(); }); diff --git a/src/Web/e2e/provider/auth.spec.ts b/src/Web/e2e/provider/auth.spec.ts index 1a8e1f89e..69b85f469 100644 --- a/src/Web/e2e/provider/auth.spec.ts +++ b/src/Web/e2e/provider/auth.spec.ts @@ -11,7 +11,7 @@ test.describe('Provider Web App - Authentication', () => { test('should navigate to login page', async ({ page }) => { await page.click('text=Login Prestador'); - await expect(page.url()).toContain('/login'); + await expect(page).toHaveURL(/.*\/provider\/login/); }); test('should display login form with provider fields', async ({ page }) => { diff --git a/src/Web/e2e/provider/onboarding.spec.ts b/src/Web/e2e/provider/onboarding.spec.ts index 3eb5dcc77..5c0c98431 100644 --- a/src/Web/e2e/provider/onboarding.spec.ts +++ b/src/Web/e2e/provider/onboarding.spec.ts @@ -12,9 +12,9 @@ test.describe('Provider Web App - Onboarding', () => { test('should complete basic info step', async ({ page }) => { await page.fill('input[name="name"]', 'João Silva'); await page.fill('input[name="phone"]', '21999999999'); - await page.fill('input[name="cpf"]', '12345678900'); + await page.fill('input[name="cpf"]', '52998224725'); await page.click('button:has-text("Próximo")'); - await expect(page.url()).toContain('/onboarding/documentos'); + await expect(page).toHaveURL(/.*\/onboarding\/documentos/); }); test('should validate required fields', async ({ page }) => { diff --git a/src/Web/playwright.config.ts b/src/Web/playwright.config.ts index 28cd947f3..65406e714 100644 --- a/src/Web/playwright.config.ts +++ b/src/Web/playwright.config.ts @@ -36,9 +36,13 @@ export default defineConfig({ name: 'Mobile Safari', use: { ...devices['iPhone 12'] }, }, + { + name: 'ci', + use: { ...devices['Desktop Chrome'] }, + }, ], webServer: { - command: 'npm run next:dev', + command: 'npm run dev', url: 'http://localhost:3000', reuseExistingServer: !process.env.CI, timeout: 120 * 1000, From 4937e20a71aaca9e6d2b4dff2b0d9c829a9928ff Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 13:46:15 -0300 Subject: [PATCH 004/142] fix: resolve documentation inconsistencies and add dynamic E2E toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admin Portal: - Fix e2e/playwright.config.ts paths to src/Web/ - Replace with router.push in AdminProtected Architecture: - Fix Refit .NET-only statement - Remove orphaned Fluxor/C# code - Remove Blazor artifacts from React tree Roadmap: - Fix Sprint 8E status (Planejada -> Em Andamento) - Fix Sprint 8D dates (2-22 Abr -> 2-24 Mar) - Remove duplicate 8E from Próximas Sprints README: - Fix Admin Portal section (Blazor -> React) Pipelines: - Add ./ prefix to master-ci-cd.yml working-directory - Add dynamic run_e2e input to pr-validation.yml --- .github/workflows/master-ci-cd.yml | 2 +- .github/workflows/pr-validation.yml | 12 +++++++- README.md | 2 +- docs/admin-portal/overview.md | 18 ++++++++--- docs/architecture.md | 48 +++-------------------------- docs/roadmap-current.md | 4 +-- docs/roadmap.md | 2 +- 7 files changed, 33 insertions(+), 55 deletions(-) diff --git a/.github/workflows/master-ci-cd.yml b/.github/workflows/master-ci-cd.yml index aa485f90f..91de949ec 100644 --- a/.github/workflows/master-ci-cd.yml +++ b/.github/workflows/master-ci-cd.yml @@ -165,7 +165,7 @@ jobs: echo "React Admin tests completed" - name: Run E2E Tests (Playwright) - working-directory: src/Web + working-directory: ./src/Web if: false # Disabled for now - enable after apps are running run: | echo "================================" diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 287f5d7b3..db39c8eaa 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -8,6 +8,15 @@ name: Pull Request Validation branches: ["**"] # Manual trigger for testing workflow changes workflow_dispatch: + inputs: + run_e2e: + description: 'Run E2E tests' + required: false + default: 'false' + type: choice + options: + - 'false' + - 'true' permissions: contents: read @@ -18,6 +27,7 @@ permissions: env: DOTNET_VERSION: "10.0.x" STRICT_COVERAGE: false # TODO: Re-set to true once coverage threshold (90%) is consistently met + RUN_E2E: ${{ github.event.inputs.run_e2e || 'false' }} # PostgreSQL configuration (DRY principle - single source of truth) # Fallback credentials: Only used in fork/local dev; main repo requires secrets POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD || 'test123' }} @@ -181,7 +191,7 @@ jobs: - name: Run E2E Tests (Playwright) working-directory: ./src/Web - if: false # Disabled for now - enable after apps are running + if: env.RUN_E2E == 'true' run: | echo "================================" echo "E2E TESTS (PLAYWRIGHT)" diff --git a/README.md b/README.md index 9d182d7ed..ae19b30bf 100644 --- a/README.md +++ b/README.md @@ -260,7 +260,7 @@ dotnet test tests/MeAjudaAi.Modules.Users.Tests/ ## 🎨 Admin Portal -**Portal administrativo** Blazor WebAssembly para gestão completa da plataforma. +**Portal administrativo** React + Next.js para gestão completa da plataforma. **Funcionalidades:** - ✅ Autenticação via Keycloak OIDC (Authorization Code + PKCE) diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index 5a0c4a72c..0a74c4222 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -96,8 +96,8 @@ apps/admin-portal/ │ │ ├── api.ts # API client │ │ └── utils.ts │ └── types/ # TypeScript types -├── e2e/ # Playwright tests -├── playwright.config.ts +├── src/Web/e2e/ # Playwright tests +├── src/Web/playwright.config.ts └── package.json ``` @@ -138,13 +138,21 @@ export function EditButton({ providerId }: { providerId: string }) { } // Protected route wrapper +'use client'; +import { useRouter } from 'next/navigation'; +import { useSession } from 'next-auth/react'; + function AdminProtected({ children }: { children: React.ReactNode }) { const { data: session, status } = useSession(); - + const router = useRouter(); + if (status === 'loading') return ; - if (!session) return ; + if (!session) { + router.push('/login'); + return null; + } if (session.user.role !== 'admin') return ; - + return <>{children}; } ``` diff --git a/docs/architecture.md b/docs/architecture.md index df2e83b73..1df465b46 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2913,38 +2913,8 @@ export function useCreateProvider() { }); } ``` - result.Value.Items, - result.Value.TotalItems)); - } - else - { - dispatcher.Dispatch(new LoadProvidersFailureAction( - result.Error?.Message ?? "Falha ao carregar fornecedores")); - } - } - catch (Exception ex) - { - dispatcher.Dispatch(new LoadProvidersFailureAction(ex.Message)); - } - } -} -``` - -**Fluxo de Dados Unidirecional**: -1. **User Interaction** → Componente dispara Action -2. **Action** → Fluxor enfileira ação -3. **Reducer** → Cria novo State (immutable) -4. **Effect** (se aplicável) → Chama API externa -5. **New State** → UI re-renderiza automaticamente - -**Benefícios do Padrão**: -- ✅ **Previsibilidade**: Estado centralizado e immutable -- ✅ **Testabilidade**: Reducers são funções puras -- ✅ **Debug**: Redux DevTools integration -- ✅ **Time-travel**: Estado histórico para debugging - -### **Refit - Type-Safe HTTP Clients (SDK)** +### **TanStack Query - Data Fetching Patterns** **MeAjudaAi.Client.Contracts é o SDK oficial .NET** para consumir a API REST, semelhante ao AWS SDK ou Stripe SDK. **SDKs Disponíveis** (Sprint 6-7): @@ -3035,7 +3005,7 @@ Blazor Component → IProvidersApi (interface) → Refit CodeGen → HttpClient - ✅ Integration with HttpClientFactory + Polly - ✅ Authentication header injection via message handler - ✅ **20 linhas de código manual → 2 linhas (interface + atributo)** -- ✅ Reutilizável entre projetos (.NET, Node.js) +- ✅ Reutilizável entre projetos .NET (ex.: .NET 6/8, Blazor, Xamarin) **Documentação Completa**: `src/Web/MeAjudaAi.Web.Customer/types/README.md` @@ -3264,19 +3234,9 @@ apps/admin-portal/ │ ├── hooks/ # Custom React hooks │ ├── lib/ # Utilities │ └── stores/ # Zustand stores -├── e2e/ # Playwright tests +├── src/Web/e2e/ # Playwright tests │ └── providers.spec.ts -├── playwright.config.ts -└── package.json -├── Program.cs # Entry point + DI -└── App.razor # Root component - -tests/MeAjudaAi.Web.Admin.Tests/ -├── Pages/ -│ ├── ProvidersPageTests.cs -│ └── DashboardPageTests.cs -└── Layout/ - └── DarkModeToggleTests.cs +└── src/Web/playwright.config.ts ``` ### **Best Practices - Frontend** diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 0e1973d00..3f5a520b7 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -601,7 +601,7 @@ Durante o processo de atualização automática de dependências pelo Dependabot - `/configuracoes` - Toggle de visibilidade + delete account com confirmação LGPD - ✅ **Slug URLs**: Perfis públicos acessíveis via slugs (ex: `/provider/joao-silva-a1b2c3d4`) -### ✅ Sprint 8D - Admin Portal Migration (2 - 22 Abr 2026) +### ✅ Sprint 8D - Admin Portal Migration (2 - 24 Mar 2026) **Status**: ✅ CONCLUÍDA (24 Mar 2026) **Foco**: Phased migration from Blazor WASM to React. @@ -1212,7 +1212,7 @@ public class ActivityHub : Hub 1. ✅ **Sprint 8B.2: NX Monorepo & Technical Excellence** (Concluída) 2. ✅ **Sprint 8C: Provider Web App (React + NX)** (Concluída - 21 Mar 2026) 3. ✅ **Sprint 8D: Admin Portal Migration** (Concluída - 24 Mar 2026) -4. ⏳ **Sprint 8E: E2E Tests React Apps (Playwright)** (Planejada) +4. ⏳ **Sprint 8E: E2E Tests React Apps (Playwright)** (Em Andamento) 5. ⏳ **Sprint 9: BUFFER & RISK MITIGATION** (Abril/Maio 2026) 6. 🎯 **MVP Final Launch: 12 - 16 de Maio de 2026** 7. 📋 API Collections - Bruno .bru files para todos os módulos diff --git a/docs/roadmap.md b/docs/roadmap.md index 89ed9e712..e8856bb56 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -8,7 +8,7 @@ Este documento consolida o planejamento estratégico e tático da plataforma MeA **Status**: Fase 2 em andamento (Frontend React + Mobile). Contém o status atual das sprints, o cronograma detalhado até o MVP e o plano de mitigação de riscos. - **Sprint Atual**: 8E (E2E Tests - Playwright) -- **Próximas Sprints**: 8E (E2E Tests), 9 (Buffer & Risk Mitigation) +- **Próximas Sprints**: 9 (Buffer & Risk Mitigation) - **Meta MVP**: Maio 2026 (12-16) --- From 790fabe8817b713cb970509d4dc28b5ef116ed38 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 13:54:38 -0300 Subject: [PATCH 005/142] refactor: restructure E2E tests to tests/MeAjudaAi.Web.*.Tests/ - Move E2E tests from src/Web/e2e/ to tests/MeAjudaAi.Web.*.Tests/e2e/ - Create MeAjudaAi.Web.Shared.Tests with shared fixtures - Update playwright.config.ts to point to tests/ - Update imports in all test files - Update documentation to reflect new structure --- docs/admin-portal/overview.md | 21 +++++++++- docs/architecture.md | 18 +++++++- docs/roadmap-current.md | 9 ++-- src/Web/e2e/base.ts | 11 ----- src/Web/playwright.config.ts | 2 +- .../e2e/admin/auth.spec.ts | 2 +- .../e2e/admin/providers.spec.ts | 2 +- .../e2e/customer/auth.spec.ts | 2 +- .../e2e/customer/search.spec.ts | 2 +- .../e2e/provider/auth.spec.ts | 4 +- .../e2e/provider/onboarding.spec.ts | 2 +- tests/MeAjudaAi.Web.Shared.Tests/base.ts | 41 +++++++++++++++++++ 12 files changed, 89 insertions(+), 27 deletions(-) delete mode 100644 src/Web/e2e/base.ts rename {src/Web => tests/MeAjudaAi.Web.Admin.Tests}/e2e/admin/auth.spec.ts (92%) rename {src/Web => tests/MeAjudaAi.Web.Admin.Tests}/e2e/admin/providers.spec.ts (96%) rename {src/Web => tests/MeAjudaAi.Web.Customer.Tests}/e2e/customer/auth.spec.ts (91%) rename {src/Web => tests/MeAjudaAi.Web.Customer.Tests}/e2e/customer/search.spec.ts (94%) rename {src/Web => tests/MeAjudaAi.Web.Provider.Tests}/e2e/provider/auth.spec.ts (84%) rename {src/Web => tests/MeAjudaAi.Web.Provider.Tests}/e2e/provider/onboarding.spec.ts (93%) create mode 100644 tests/MeAjudaAi.Web.Shared.Tests/base.ts diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index 0a74c4222..5095ab3c9 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -96,11 +96,28 @@ apps/admin-portal/ │ │ ├── api.ts # API client │ │ └── utils.ts │ └── types/ # TypeScript types -├── src/Web/e2e/ # Playwright tests -├── src/Web/playwright.config.ts └── package.json ``` +### Testes E2E + +Localização: `tests/MeAjudaAi.Web.Admin.Tests/e2e/` + +**Estrutura:** +``` +tests/MeAjudaAi.Web.Admin.Tests/ +└── e2e/ + ├── auth.spec.ts + └── providers.spec.ts +``` + +**Fixtures compartilhadas:** `tests/MeAjudaAi.Web.Shared.Tests/base.ts` +- `loginAsAdmin(page)` +- `loginAsProvider(page)` +- `loginAsCustomer(page)` +- `logout(page)` +``` + ## 🔐 Autenticação e Autorização ### Keycloak Configuration diff --git a/docs/architecture.md b/docs/architecture.md index 1df465b46..9261e6d0c 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3234,9 +3234,23 @@ apps/admin-portal/ │ ├── hooks/ # Custom React hooks │ ├── lib/ # Utilities │ └── stores/ # Zustand stores -├── src/Web/e2e/ # Playwright tests -│ └── providers.spec.ts └── src/Web/playwright.config.ts + +tests/ +├── MeAjudaAi.Web.Customer.Tests/ +│ └── e2e/ # Playwright tests +│ ├── auth.spec.ts +│ └── search.spec.ts +├── MeAjudaAi.Web.Provider.Tests/ +│ └── e2e/ # Playwright tests +│ ├── auth.spec.ts +│ └── onboarding.spec.ts +├── MeAjudaAi.Web.Admin.Tests/ +│ └── e2e/ # Playwright tests +│ ├── auth.spec.ts +│ └── providers.spec.ts +└── MeAjudaAi.Web.Shared.Tests/ + └── base.ts # Shared fixtures ``` ### **Best Practices - Frontend** diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 3f5a520b7..2837f700b 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -623,10 +623,11 @@ Durante o processo de atualização automática de dependências pelo Dependabot **Scope**: 1. **Playwright Config**: Configurar playwright.config.ts no workspace NX (✅ Concluído) 2. **Implement Test Specs**: Criar testes E2E para Customer, Provider e Admin Apps -3. **Customer Web App Tests**: Login, busca, perfil, agendamento -4. **Provider Web App Tests**: Onboarding, dashboard, gestão de serviços -5. **Admin Portal Tests**: CRUD providers, documentos, métricas -6. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` +3. **Customer Web App Tests**: Login, busca, perfil (`tests/MeAjudaAi.Web.Customer.Tests/e2e/`) +4. **Provider Web App Tests**: Onboarding, dashboard (`tests/MeAjudaAi.Web.Provider.Tests/e2e/`) +5. **Admin Portal Tests**: CRUD providers, documentos (`tests/MeAjudaAi.Web.Admin.Tests/e2e/`) +6. **Shared Fixtures**: `tests/MeAjudaAi.Web.Shared.Tests/base.ts` +7. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` **Cenários de Teste**: - [ ] Autenticação (login, logout, refresh token) diff --git a/src/Web/e2e/base.ts b/src/Web/e2e/base.ts deleted file mode 100644 index 9a4758039..000000000 --- a/src/Web/e2e/base.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test as base } from '@playwright/test'; - -export { expect } from '@playwright/test'; - -export const test = base.extend({ - page: async ({ page }, use) => { - await use(page); - }, -}); - -export { base }; diff --git a/src/Web/playwright.config.ts b/src/Web/playwright.config.ts index 65406e714..ac67c9a5d 100644 --- a/src/Web/playwright.config.ts +++ b/src/Web/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: './e2e', + testDir: '../tests', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, diff --git a/src/Web/e2e/admin/auth.spec.ts b/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts similarity index 92% rename from src/Web/e2e/admin/auth.spec.ts rename to tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts index 6a232529c..457457ccc 100644 --- a/src/Web/e2e/admin/auth.spec.ts +++ b/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../base'; +import { test, expect, loginAsAdmin, logout } from '../../MeAjudaAi.Web.Shared.Tests/base'; test.describe('Admin Portal - Authentication', () => { test.beforeEach(async ({ page }) => { diff --git a/src/Web/e2e/admin/providers.spec.ts b/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/providers.spec.ts similarity index 96% rename from src/Web/e2e/admin/providers.spec.ts rename to tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/providers.spec.ts index c40c42e55..80a6752af 100644 --- a/src/Web/e2e/admin/providers.spec.ts +++ b/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/providers.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../base'; +import { test, expect, loginAsAdmin } from '../../MeAjudaAi.Web.Shared.Tests/base'; test.describe('Admin Portal - Providers Management', () => { test.beforeEach(async ({ page }) => { diff --git a/src/Web/e2e/customer/auth.spec.ts b/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/auth.spec.ts similarity index 91% rename from src/Web/e2e/customer/auth.spec.ts rename to tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/auth.spec.ts index a2fc065d9..1ade7adc3 100644 --- a/src/Web/e2e/customer/auth.spec.ts +++ b/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../base'; +import { test, expect, loginAsCustomer, logout } from '../../MeAjudaAi.Web.Shared.Tests/base'; test.describe('Customer Web App - Authentication', () => { test.beforeEach(async ({ page }) => { diff --git a/src/Web/e2e/customer/search.spec.ts b/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/search.spec.ts similarity index 94% rename from src/Web/e2e/customer/search.spec.ts rename to tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/search.spec.ts index 8ca2cdb9f..df2688bd4 100644 --- a/src/Web/e2e/customer/search.spec.ts +++ b/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/search.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../base'; +import { test, expect } from '../../MeAjudaAi.Web.Shared.Tests/base'; test.describe('Customer Web App - Search', () => { test.beforeEach(async ({ page }) => { diff --git a/src/Web/e2e/provider/auth.spec.ts b/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/auth.spec.ts similarity index 84% rename from src/Web/e2e/provider/auth.spec.ts rename to tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/auth.spec.ts index 69b85f469..a10e2d5cf 100644 --- a/src/Web/e2e/provider/auth.spec.ts +++ b/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../base'; +import { test, expect, loginAsProvider, logout } from '../../MeAjudaAi.Web.Shared.Tests/base'; test.describe('Provider Web App - Authentication', () => { test.beforeEach(async ({ page }) => { @@ -25,6 +25,6 @@ test.describe('Provider Web App - Authentication', () => { await page.fill('input[type="email"]', 'invalid@provider.com'); await page.fill('input[type="password"]', 'wrongpassword'); await page.click('button[type="submit"]'); - await expect(page.locator('text=credenciais inválidas')).toBeVisible(); + await expect(page.getByRole('alert')).toContainText(/credenciais inválidas/i); }); }); diff --git a/src/Web/e2e/provider/onboarding.spec.ts b/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/onboarding.spec.ts similarity index 93% rename from src/Web/e2e/provider/onboarding.spec.ts rename to tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/onboarding.spec.ts index 5c0c98431..bc74bf770 100644 --- a/src/Web/e2e/provider/onboarding.spec.ts +++ b/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/onboarding.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../base'; +import { test, expect, loginAsProvider } from '../../MeAjudaAi.Web.Shared.Tests/base'; test.describe('Provider Web App - Onboarding', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/MeAjudaAi.Web.Shared.Tests/base.ts b/tests/MeAjudaAi.Web.Shared.Tests/base.ts new file mode 100644 index 000000000..8932e7926 --- /dev/null +++ b/tests/MeAjudaAi.Web.Shared.Tests/base.ts @@ -0,0 +1,41 @@ +import { test as base, Page, BrowserContext } from '@playwright/test'; + +export { expect } from '@playwright/test'; + +export interface TestFixtures { + page: Page; +} + +export const test = base.extend({ + page: async ({ page }, use) => { + await use(page); + }, +}); + +export { base }; + +export async function loginAsAdmin(page: Page): Promise { + await page.goto('/admin/login'); + await page.fill('input[type="email"]', 'admin@meajudaai.com'); + await page.fill('input[type="password"]', 'adminpassword'); + await page.click('button[type="submit"]'); +} + +export async function loginAsProvider(page: Page): Promise { + await page.goto('/provider/login'); + await page.fill('input[type="email"]', 'provider@test.com'); + await page.fill('input[type="password"]', 'providerpassword'); + await page.click('button[type="submit"]'); +} + +export async function loginAsCustomer(page: Page): Promise { + await page.goto('/login'); + await page.fill('input[type="email"]', 'customer@test.com'); + await page.fill('input[type="password"]', 'customerpassword'); + await page.click('button[type="submit"]'); +} + +export async function logout(page: Page): Promise { + await page.click('text=Sair'); + await page.waitForURL(/\/(login|admin\/login|provider\/login)/); +} From e6883133aaa0456a88ff527e96b762145e6cb1e2 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 14:59:24 -0300 Subject: [PATCH 006/142] chore: exclude nul from tracking and update e2e specs --- .github/workflows/master-ci-cd.yml | 45 +++++++++++++++-- .gitignore | 5 +- docs/admin-portal/overview.md | 3 +- docs/architecture.md | 49 +++++++------------ docs/roadmap-current.md | 2 +- src/Web/MeAjudaAi.Web.Admin/e2e/auth.spec.ts | 29 +++++++++++ .../e2e}/providers.spec.ts | 2 +- .../MeAjudaAi.Web.Customer/e2e}/auth.spec.ts | 2 +- .../e2e}/search.spec.ts | 2 +- .../MeAjudaAi.Web.Provider/e2e}/auth.spec.ts | 2 +- .../e2e}/onboarding.spec.ts | 2 +- .../Web/libs/e2e-support}/base.ts | 21 ++++---- src/Web/libs/e2e-support/project.json | 20 ++++++++ src/Web/libs/e2e-support/tsconfig.json | 8 +++ src/Web/libs/e2e-support/tsconfig.lib.json | 13 +++++ src/Web/playwright.config.ts | 4 +- .../e2e/admin/auth.spec.ts | 30 ------------ 17 files changed, 156 insertions(+), 83 deletions(-) create mode 100644 src/Web/MeAjudaAi.Web.Admin/e2e/auth.spec.ts rename {tests/MeAjudaAi.Web.Admin.Tests/e2e/admin => src/Web/MeAjudaAi.Web.Admin/e2e}/providers.spec.ts (96%) rename {tests/MeAjudaAi.Web.Customer.Tests/e2e/customer => src/Web/MeAjudaAi.Web.Customer/e2e}/auth.spec.ts (91%) rename {tests/MeAjudaAi.Web.Customer.Tests/e2e/customer => src/Web/MeAjudaAi.Web.Customer/e2e}/search.spec.ts (94%) rename {tests/MeAjudaAi.Web.Provider.Tests/e2e/provider => src/Web/MeAjudaAi.Web.Provider/e2e}/auth.spec.ts (91%) rename {tests/MeAjudaAi.Web.Provider.Tests/e2e/provider => src/Web/MeAjudaAi.Web.Provider/e2e}/onboarding.spec.ts (93%) rename {tests/MeAjudaAi.Web.Shared.Tests => src/Web/libs/e2e-support}/base.ts (51%) create mode 100644 src/Web/libs/e2e-support/project.json create mode 100644 src/Web/libs/e2e-support/tsconfig.json create mode 100644 src/Web/libs/e2e-support/tsconfig.lib.json delete mode 100644 tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts diff --git a/.github/workflows/master-ci-cd.yml b/.github/workflows/master-ci-cd.yml index 91de949ec..7f0b26fc9 100644 --- a/.github/workflows/master-ci-cd.yml +++ b/.github/workflows/master-ci-cd.yml @@ -164,18 +164,55 @@ jobs: npm test echo "React Admin tests completed" + - name: Setup Node.js environment + uses: actions/setup-node@v6 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: ./src/Web/package-lock.json + + - name: Install Frontend Dependencies + working-directory: ./src/Web + run: npm ci + + - name: Generate API Client + working-directory: ./src/Web + run: | + set -e + dotnet tool install -g Swashbuckle.AspNetCore.Cli --version 10.1.5 + mkdir -p ../api + export ASPNETCORE_ENVIRONMENT=Development + export ConnectionStrings__DefaultConnection="Host=localhost;Database=dummy" + export Migrations__Enabled=false + DLL_PATH=$(find ../../src/Bootstrapper/MeAjudaAi.ApiService/bin/Release -name "MeAjudaAi.ApiService.dll" | head -n 1) + swagger tofile --output ../api/api-spec.json "$DLL_PATH" v1 + export OPENAPI_SPEC_URL="$GITHUB_WORKSPACE/src/api/api-spec.json" + npm run generate:api --workspace=meajudaai.web.customer + npm run generate:api --workspace=meajudaai.web.admin + npm run generate:api --workspace=meajudaai.web.provider + + - name: Build Frontend Workspace + working-directory: ./src/Web + env: + KEYCLOAK_ADMIN_CLIENT_ID: ci-build-placeholder + KEYCLOAK_ADMIN_CLIENT_SECRET: ci-build-placeholder + KEYCLOAK_ISSUER: http://localhost:8080/realms/meajudaai + NEXTAUTH_URL: http://localhost:3000 + NEXTAUTH_SECRET: ci-build-placeholder + AUTH_SECRET: ci-build-placeholder + run: | + set -e + npx nx run MeAjudaAi.Web.Customer:build + npx nx affected --target=build --exclude=MeAjudaAi.Web.Customer + - name: Run E2E Tests (Playwright) working-directory: ./src/Web - if: false # Disabled for now - enable after apps are running run: | echo "================================" echo "E2E TESTS (PLAYWRIGHT)" echo "================================" - # Install Playwright browsers npx playwright install --with-deps chromium - - # Run E2E tests (chromium only) npx playwright test --reporter=list --project=ci - name: Free Disk Space for Integration Tests diff --git a/.gitignore b/.gitignore index de90ac397..b25c7ec60 100644 --- a/.gitignore +++ b/.gitignore @@ -156,4 +156,7 @@ legacy-analysis-report.* **/out/ **/generated/ -vite.config.*.timestamp* \ No newline at end of file +vite.config.*.timestamp* + +# Ignore Windows null device files +nul \ No newline at end of file diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index 5095ab3c9..7caca7442 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -233,7 +233,8 @@ O Admin Portal segue as diretrizes **WCAG 2.1 AA**: ### E2E Tests com Playwright - Testes end-to-end para todos os fluxos principais -- Localização: `src/Web/e2e/admin/` +- Localização: `tests/MeAjudaAi.Web.Admin.Tests/e2e/` +- Os testes exercising the OAuth flow via Keycloak (signIn("keycloak")) em vez de formulários de email/password ### Executar Testes diff --git a/docs/architecture.md b/docs/architecture.md index 9261e6d0c..4bd814cfd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3220,37 +3220,24 @@ test.describe('Providers Management', () => { ### **Estrutura de Arquivos** -```text -apps/admin-portal/ -├── src/ -│ ├── app/ # Next.js App Router -│ │ ├── (auth)/ # Authentication routes -│ │ ├── (dashboard)/ # Protected routes -│ │ ├── layout.tsx -│ │ └── page.tsx -│ ├── components/ # Reusable components -│ │ ├── ui/ # Base UI components -│ │ └── providers/ # Feature components -│ ├── hooks/ # Custom React hooks -│ ├── lib/ # Utilities -│ └── stores/ # Zustand stores -└── src/Web/playwright.config.ts - -tests/ -├── MeAjudaAi.Web.Customer.Tests/ -│ └── e2e/ # Playwright tests -│ ├── auth.spec.ts -│ └── search.spec.ts -├── MeAjudaAi.Web.Provider.Tests/ -│ └── e2e/ # Playwright tests -│ ├── auth.spec.ts -│ └── onboarding.spec.ts -├── MeAjudaAi.Web.Admin.Tests/ -│ └── e2e/ # Playwright tests -│ ├── auth.spec.ts -│ └── providers.spec.ts -└── MeAjudaAi.Web.Shared.Tests/ - └── base.ts # Shared fixtures +``` +src/Web/ +├── MeAjudaAi.Web.Admin/ # Admin Portal Next.js App +│ ├── app/ +│ │ ├── (auth)/ # Authentication routes (Keycloak OAuth) +│ │ └── (dashboard)/ # Protected routes +│ └── e2e/ # E2E tests (co-localized) +│ └── auth.spec.ts +├── MeAjudaAi.Web.Customer/ # Customer Web Next.js App +├── MeAjudaAi.Web.Provider/ # Provider Web Next.js App +├── libs/ # Shared libraries (ui, auth, api-client, assets) +└── playwright.config.ts # Single Playwright config for all apps + +# Playwright Configuration: +# - testDir: './tests' (single test directory) +# - baseURL: 'http://localhost:3000' +# - projects array: chromium, firefox, webkit, mobile devices, ci +# All tests use the same playwright.config.ts with project-based browser selection. ``` ### **Best Practices - Frontend** diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 2837f700b..9346e7f59 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -627,7 +627,7 @@ Durante o processo de atualização automática de dependências pelo Dependabot 4. **Provider Web App Tests**: Onboarding, dashboard (`tests/MeAjudaAi.Web.Provider.Tests/e2e/`) 5. **Admin Portal Tests**: CRUD providers, documentos (`tests/MeAjudaAi.Web.Admin.Tests/e2e/`) 6. **Shared Fixtures**: `tests/MeAjudaAi.Web.Shared.Tests/base.ts` -7. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` +7. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` (⏳ Habilitado em master-ci-cd.yml, pendiente em pr-validation.yml: requer RUN_E2E='true' para executar) **Cenários de Teste**: - [ ] Autenticação (login, logout, refresh token) diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/auth.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/auth.spec.ts new file mode 100644 index 000000000..a91f420a3 --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/auth.spec.ts @@ -0,0 +1,29 @@ +import { test, expect } from '@meajudaai/web-e2e-support'; + +test.describe('Admin Portal - Authentication', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/'); + }); + + test('should display admin login page', async ({ page }) => { + await expect(page.getByRole('heading')).toContainText(/admin|administrador|meajudaai/i); + }); + + test('should navigate to login', async ({ page }) => { + await page.click('text=Login'); + await expect(page).toHaveURL(/.*\/admin\/login/); + }); + + test('should display Keycloak OAuth login button', async ({ page }) => { + await page.goto('/admin/login'); + await expect(page.getByRole('button', { name: /entrar com keycloak/i })).toBeVisible(); + }); + + test('should trigger Keycloak OAuth flow when clicking login button', async ({ page }) => { + await page.goto('/admin/login'); + + await page.getByRole('button', { name: /entrar com keycloak/i }).click(); + + await expect(page).toHaveURL(/.*keycloak.*|.*realms.*\/meajudaai/i); + }); +}); diff --git a/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/providers.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts similarity index 96% rename from tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/providers.spec.ts rename to src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts index 80a6752af..339f52bef 100644 --- a/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/providers.spec.ts +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, loginAsAdmin } from '../../MeAjudaAi.Web.Shared.Tests/base'; +import { test, expect, loginAsAdmin } from '@meajudaai/web-e2e-support'; test.describe('Admin Portal - Providers Management', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/auth.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts similarity index 91% rename from tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/auth.spec.ts rename to src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts index 1ade7adc3..f09239163 100644 --- a/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/auth.spec.ts +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, loginAsCustomer, logout } from '../../MeAjudaAi.Web.Shared.Tests/base'; +import { test, expect, loginAsCustomer, logout } from '@meajudaai/web-e2e-support'; test.describe('Customer Web App - Authentication', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/search.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/search.spec.ts similarity index 94% rename from tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/search.spec.ts rename to src/Web/MeAjudaAi.Web.Customer/e2e/search.spec.ts index df2688bd4..6dca7b81c 100644 --- a/tests/MeAjudaAi.Web.Customer.Tests/e2e/customer/search.spec.ts +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/search.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from '../../MeAjudaAi.Web.Shared.Tests/base'; +import { test, expect } from '@meajudaai/web-e2e-support'; test.describe('Customer Web App - Search', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/auth.spec.ts b/src/Web/MeAjudaAi.Web.Provider/e2e/auth.spec.ts similarity index 91% rename from tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/auth.spec.ts rename to src/Web/MeAjudaAi.Web.Provider/e2e/auth.spec.ts index a10e2d5cf..4eaf07a34 100644 --- a/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/auth.spec.ts +++ b/src/Web/MeAjudaAi.Web.Provider/e2e/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, loginAsProvider, logout } from '../../MeAjudaAi.Web.Shared.Tests/base'; +import { test, expect, loginAsProvider, logout } from '@meajudaai/web-e2e-support'; test.describe('Provider Web App - Authentication', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/onboarding.spec.ts b/src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts similarity index 93% rename from tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/onboarding.spec.ts rename to src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts index bc74bf770..02a8b4ba6 100644 --- a/tests/MeAjudaAi.Web.Provider.Tests/e2e/provider/onboarding.spec.ts +++ b/src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, loginAsProvider } from '../../MeAjudaAi.Web.Shared.Tests/base'; +import { test, expect, loginAsProvider } from '@meajudaai/web-e2e-support'; test.describe('Provider Web App - Onboarding', () => { test.beforeEach(async ({ page }) => { diff --git a/tests/MeAjudaAi.Web.Shared.Tests/base.ts b/src/Web/libs/e2e-support/base.ts similarity index 51% rename from tests/MeAjudaAi.Web.Shared.Tests/base.ts rename to src/Web/libs/e2e-support/base.ts index 8932e7926..259a197a7 100644 --- a/tests/MeAjudaAi.Web.Shared.Tests/base.ts +++ b/src/Web/libs/e2e-support/base.ts @@ -16,23 +16,26 @@ export { base }; export async function loginAsAdmin(page: Page): Promise { await page.goto('/admin/login'); - await page.fill('input[type="email"]', 'admin@meajudaai.com'); - await page.fill('input[type="password"]', 'adminpassword'); - await page.click('button[type="submit"]'); + await page.getByRole('button', { name: /entrar com keycloak/i }).click(); + await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }).catch(() => { + console.log('OAuth redirect intercepted - running in mock/test mode'); + }); } export async function loginAsProvider(page: Page): Promise { await page.goto('/provider/login'); - await page.fill('input[type="email"]', 'provider@test.com'); - await page.fill('input[type="password"]', 'providerpassword'); - await page.click('button[type="submit"]'); + await page.getByRole('button', { name: /entrar/i }).click(); + await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }).catch(() => { + console.log('OAuth redirect intercepted - running in mock/test mode'); + }); } export async function loginAsCustomer(page: Page): Promise { await page.goto('/login'); - await page.fill('input[type="email"]', 'customer@test.com'); - await page.fill('input[type="password"]', 'customerpassword'); - await page.click('button[type="submit"]'); + await page.getByRole('button', { name: /entrar/i }).click(); + await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }).catch(() => { + console.log('OAuth redirect intercepted - running in mock/test mode'); + }); } export async function logout(page: Page): Promise { diff --git a/src/Web/libs/e2e-support/project.json b/src/Web/libs/e2e-support/project.json new file mode 100644 index 000000000..6ee1cc6a1 --- /dev/null +++ b/src/Web/libs/e2e-support/project.json @@ -0,0 +1,20 @@ +{ + "name": "e2e-support", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/e2e-support/src", + "projectType": "library", + "tags": ["e2e", "playwright", "test"], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{options.outputPath}"], + "options": { + "jestConfig": "libs/e2e-support/jest.config.ts", + "passWithNoTests": true + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} \ No newline at end of file diff --git a/src/Web/libs/e2e-support/tsconfig.json b/src/Web/libs/e2e-support/tsconfig.json new file mode 100644 index 000000000..315dea4d0 --- /dev/null +++ b/src/Web/libs/e2e-support/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "composite": true, + "types": ["jest", "node"] + }, + "include": ["src/**/*.ts", "jest.config.ts"] +} \ No newline at end of file diff --git a/src/Web/libs/e2e-support/tsconfig.lib.json b/src/Web/libs/e2e-support/tsconfig.lib.json new file mode 100644 index 000000000..e8cd87fc3 --- /dev/null +++ b/src/Web/libs/e2e-support/tsconfig.lib.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "target": "es2021", + "lib": ["es2021", "dom"], + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "jest.config.ts"] +} \ No newline at end of file diff --git a/src/Web/playwright.config.ts b/src/Web/playwright.config.ts index ac67c9a5d..bb7beb9c7 100644 --- a/src/Web/playwright.config.ts +++ b/src/Web/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig, devices } from '@playwright/test'; export default defineConfig({ - testDir: '../tests', + testDir: './src', fullyParallel: true, forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, @@ -15,6 +15,8 @@ export default defineConfig({ trace: 'on-first-retry', screenshot: 'only-on-failure', }, + grep: /e2e/, + testMatch: '**/*.spec.ts', projects: [ { name: 'chromium', diff --git a/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts b/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts deleted file mode 100644 index 457457ccc..000000000 --- a/tests/MeAjudaAi.Web.Admin.Tests/e2e/admin/auth.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { test, expect, loginAsAdmin, logout } from '../../MeAjudaAi.Web.Shared.Tests/base'; - -test.describe('Admin Portal - Authentication', () => { - test.beforeEach(async ({ page }) => { - await page.goto('/'); - }); - - test('should display admin login page', async ({ page }) => { - await expect(page.getByRole('heading')).toContainText(/admin|administrador/i); - }); - - test('should navigate to login', async ({ page }) => { - await page.click('text=Login Admin'); - await expect(page).toHaveURL(/.*\/admin\/login/); - }); - - test('should display login form', async ({ page }) => { - await page.goto('/admin/login'); - await expect(page.locator('input[type="email"]')).toBeVisible(); - await expect(page.locator('input[type="password"]')).toBeVisible(); - }); - - test('should show error for invalid credentials', async ({ page }) => { - await page.goto('/admin/login'); - await page.fill('input[type="email"]', 'admin@meajudaai.com'); - await page.fill('input[type="password"]', 'wrongpassword'); - await page.click('button[type="submit"]'); - await expect(page.getByRole('alert')).toContainText(/credenciais inválidas/i); - }); -}); From 03d7ae98f07ee6dd3deffb9b8265807a0a0bd8bd Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 16:54:05 -0300 Subject: [PATCH 007/142] feat: Introduce E2E tests, establish CI/CD pipeline, and add core documentation. --- .github/workflows/master-ci-cd.yml | 5 +- docs/admin-portal/overview.md | 12 +- docs/architecture.md | 119 ++++++++++-------- .../MeAjudaAi.Web.Admin/e2e/providers.spec.ts | 5 + .../MeAjudaAi.Web.Customer/e2e/auth.spec.ts | 2 +- src/Web/libs/e2e-support/base.ts | 34 ++--- 6 files changed, 102 insertions(+), 75 deletions(-) diff --git a/.github/workflows/master-ci-cd.yml b/.github/workflows/master-ci-cd.yml index 7f0b26fc9..f5e98985e 100644 --- a/.github/workflows/master-ci-cd.yml +++ b/.github/workflows/master-ci-cd.yml @@ -176,7 +176,6 @@ jobs: run: npm ci - name: Generate API Client - working-directory: ./src/Web run: | set -e dotnet tool install -g Swashbuckle.AspNetCore.Cli --version 10.1.5 @@ -184,7 +183,9 @@ jobs: export ASPNETCORE_ENVIRONMENT=Development export ConnectionStrings__DefaultConnection="Host=localhost;Database=dummy" export Migrations__Enabled=false - DLL_PATH=$(find ../../src/Bootstrapper/MeAjudaAi.ApiService/bin/Release -name "MeAjudaAi.ApiService.dll" | head -n 1) + SEARCH_DIR=../../src/Bootstrapper/MeAjudaAi.ApiService/bin/Release + PATTERN=MeAjudaAi.ApiService.dll + DLL_PATH=$(find "$SEARCH_DIR" -name "$PATTERN" | head -n 1) swagger tofile --output ../api/api-spec.json "$DLL_PATH" v1 export OPENAPI_SPEC_URL="$GITHUB_WORKSPACE/src/api/api-spec.json" npm run generate:api --workspace=meajudaai.web.customer diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index 7caca7442..a72a8818b 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -104,7 +104,7 @@ apps/admin-portal/ Localização: `tests/MeAjudaAi.Web.Admin.Tests/e2e/` **Estrutura:** -``` +```text tests/MeAjudaAi.Web.Admin.Tests/ └── e2e/ ├── auth.spec.ts @@ -240,7 +240,7 @@ O Admin Portal segue as diretrizes **WCAG 2.1 AA**: ```bash cd src/Web -npx playwright test e2e/admin/ +npx playwright test --grep "admin" ``` ## 🚀 Executando Localmente @@ -271,6 +271,8 @@ Acesse: `https://localhost:7001` ## 🔗 Links Úteis -- [MudBlazor Documentation](https://mudblazor.com/) -- [Fluxor Documentation](https://github.com/mrpmorris/Fluxor) -- [Blazor WebAssembly Guide](https://learn.microsoft.com/en-us/aspnet/core/blazor/) +- [Documentação React](https://react.dev/) - Biblioteca de UI +- [Documentação Next.js](https://nextjs.org/docs) - Framework React full-stack +- [Documentação Tailwind CSS](https://tailwindcss.com/docs) - Framework de estilização +- [Documentação TanStack Query](https://tanstack.com/query/latest) - Gerenciamento de estado servidor +- [Documentação Radix UI](https://www.radix-ui.com/) - Componentes UI acessíveis diff --git a/docs/architecture.md b/docs/architecture.md index 4bd814cfd..df62f20c3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -3035,64 +3035,81 @@ Blazor Component → IProvidersApi (interface) → Refit CodeGen → HttpClient -// Data Grid com Paginação - - - - - - - - @context.Item.VerificationStatus - - - - - - - - -@* KPI Cards *@ - - - - - - - Total de Fornecedores - - - - @State.Value.TotalProviders - - +// Data Grid com Paginação (usando TanStack Table) + + + + Nome + Email + Status + + + + {providers.map((provider) => ( + + {provider.name} + {provider.email} + + + + + ))} + +
+ + + +// KPI Cards + + + Total de Fornecedores + + + {totalProviders} + + ``` **Configuração de Tema**: -```csharp -// Program.cs -builder.Services.AddMudServices(config => -{ - config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomRight; - config.SnackbarConfiguration.PreventDuplicates = false; - config.SnackbarConfiguration.ShowCloseIcon = true; - config.SnackbarConfiguration.VisibleStateDuration = 5000; -}); +```tsx +// tailwind.config.ts +export default { + darkMode: 'class', + theme: { + extend: { + colors: { + primary: { + 50: '#f0f9ff', + 500: '#0ea5e9', + 900: '#0c4a6e', + }, + }, + }, + }, +} -// App.razor - Dark Mode Binding - +// Componente de Tema com next-themes +import { useTheme } from 'next-themes'; -@code { - private bool _isDarkMode; - private MudTheme _theme = new MudTheme(); +function ThemeToggle() { + const { theme, setTheme } = useTheme(); + + return ( + + ); } + +// Toast notifications com Sonner +import { toast } from 'sonner'; + +toast.success('Operação realizada com sucesso'); +toast.error('Erro ao processar requisição'); ``` ### **Authentication - Keycloak OIDC** diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts index 339f52bef..8f36674bd 100644 --- a/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts @@ -2,6 +2,7 @@ import { test, expect, loginAsAdmin } from '@meajudaai/web-e2e-support'; test.describe('Admin Portal - Providers Management', () => { test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); await page.goto('/admin/providers'); }); @@ -33,6 +34,10 @@ test.describe('Admin Portal - Providers Management', () => { }); test.describe('Admin Portal - Documents', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + test('should display documents pending review', async ({ page }) => { await page.goto('/admin/documentos'); await expect(page.locator('[data-testid="documents-list"]')).toBeVisible(); diff --git a/src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts index f09239163..32ff9e719 100644 --- a/src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/auth.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, loginAsCustomer, logout } from '@meajudaai/web-e2e-support'; +import { test, expect } from '@meajudaai/web-e2e-support'; test.describe('Customer Web App - Authentication', () => { test.beforeEach(async ({ page }) => { diff --git a/src/Web/libs/e2e-support/base.ts b/src/Web/libs/e2e-support/base.ts index 259a197a7..7403c3101 100644 --- a/src/Web/libs/e2e-support/base.ts +++ b/src/Web/libs/e2e-support/base.ts @@ -1,4 +1,4 @@ -import { test as base, Page, BrowserContext } from '@playwright/test'; +import { test as base, Page } from '@playwright/test'; export { expect } from '@playwright/test'; @@ -6,39 +6,41 @@ export interface TestFixtures { page: Page; } -export const test = base.extend({ - page: async ({ page }, use) => { - await use(page); - }, -}); +export const test = base; export { base }; +async function handleLoginRedirect(page: Page): Promise { + try { + await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }); + } catch (error) { + if (error instanceof Error && error.name === 'TimeoutError') { + console.log('OAuth redirect intercepted - running in mock/test mode'); + } else { + throw error; + } + } +} + export async function loginAsAdmin(page: Page): Promise { await page.goto('/admin/login'); await page.getByRole('button', { name: /entrar com keycloak/i }).click(); - await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }).catch(() => { - console.log('OAuth redirect intercepted - running in mock/test mode'); - }); + await handleLoginRedirect(page); } export async function loginAsProvider(page: Page): Promise { await page.goto('/provider/login'); await page.getByRole('button', { name: /entrar/i }).click(); - await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }).catch(() => { - console.log('OAuth redirect intercepted - running in mock/test mode'); - }); + await handleLoginRedirect(page); } export async function loginAsCustomer(page: Page): Promise { await page.goto('/login'); await page.getByRole('button', { name: /entrar/i }).click(); - await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }).catch(() => { - console.log('OAuth redirect intercepted - running in mock/test mode'); - }); + await handleLoginRedirect(page); } export async function logout(page: Page): Promise { - await page.click('text=Sair'); + await page.getByRole('button', { name: /sair/i }).click(); await page.waitForURL(/\/(login|admin\/login|provider\/login)/); } From a8f439439bbbe94eb12ba5e67cd3dd18cf45beb4 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 18:03:59 -0300 Subject: [PATCH 008/142] feat: Add e2e-support library with project and ESLint configurations. --- src/Web/libs/e2e-support/.eslintrc.json | 10 ++++++++++ src/Web/libs/e2e-support/project.json | 7 +++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 src/Web/libs/e2e-support/.eslintrc.json diff --git a/src/Web/libs/e2e-support/.eslintrc.json b/src/Web/libs/e2e-support/.eslintrc.json new file mode 100644 index 000000000..f1ff0d901 --- /dev/null +++ b/src/Web/libs/e2e-support/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": ["../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts"], + "rules": {} + } + ] +} diff --git a/src/Web/libs/e2e-support/project.json b/src/Web/libs/e2e-support/project.json index 6ee1cc6a1..a69a270fb 100644 --- a/src/Web/libs/e2e-support/project.json +++ b/src/Web/libs/e2e-support/project.json @@ -1,7 +1,7 @@ { "name": "e2e-support", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "libs/e2e-support/src", + "sourceRoot": "libs/e2e-support", "projectType": "library", "tags": ["e2e", "playwright", "test"], "targets": { @@ -14,7 +14,10 @@ } }, "lint": { - "executor": "@nx/eslint:lint" + "executor": "@nx/eslint:lint", + "options": { + "lintFilePatterns": ["libs/e2e-support/**/*.ts"] + } } } } \ No newline at end of file From fb97b1aea37f26cc91c99a43a4bd6a0df9d3d261 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 18:13:03 -0300 Subject: [PATCH 009/142] feat: Add Nx project configuration for the e2e-support library. --- src/Web/libs/e2e-support/project.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/Web/libs/e2e-support/project.json b/src/Web/libs/e2e-support/project.json index a69a270fb..d0efd6bdb 100644 --- a/src/Web/libs/e2e-support/project.json +++ b/src/Web/libs/e2e-support/project.json @@ -5,14 +5,6 @@ "projectType": "library", "tags": ["e2e", "playwright", "test"], "targets": { - "test": { - "executor": "@nx/jest:jest", - "outputs": ["{options.outputPath}"], - "options": { - "jestConfig": "libs/e2e-support/jest.config.ts", - "passWithNoTests": true - } - }, "lint": { "executor": "@nx/eslint:lint", "options": { From 9e2079a26aefabd2b2b0c4d4696ecce693b91970 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 20:20:01 -0300 Subject: [PATCH 010/142] test: Add E2E tests covering onboarding, profile management, configurations, and performance across customer, provider, and admin applications. --- docs/admin-portal/overview.md | 75 ++++--- docs/architecture.md | 78 +++---- docs/roadmap-current.md | 4 +- .../MeAjudaAi.Web.Admin/e2e/configs.spec.ts | 147 +++++++++++++ .../MeAjudaAi.Web.Admin/e2e/providers.spec.ts | 59 +++++- .../e2e/onboarding.spec.ts | 92 ++++++++ .../e2e/performance.spec.ts | 199 ++++++++++++++++++ .../e2e/onboarding.spec.ts | 84 ++++++++ .../e2e/profile-mgmt.spec.ts | 195 +++++++++++++++++ 9 files changed, 843 insertions(+), 90 deletions(-) create mode 100644 src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts create mode 100644 src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts create mode 100644 src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts create mode 100644 src/Web/MeAjudaAi.Web.Provider/e2e/profile-mgmt.spec.ts diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index a72a8818b..c3d311f20 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -65,69 +65,66 @@ graph TB ## 📁 Estrutura de Diretórios ```text -apps/admin-portal/ -├── src/ -│ ├── app/ # Next.js App Router -│ │ ├── (auth)/ # Authentication routes -│ │ │ ├── login/ -│ │ │ └── layout.tsx -│ │ ├── (dashboard)/ # Protected routes -│ │ │ ├── providers/ -│ │ │ ├── documents/ -│ │ │ ├── services/ -│ │ │ ├── cities/ -│ │ │ ├── dashboard/ -│ │ │ └── layout.tsx -│ │ ├── layout.tsx -│ │ └── page.tsx -│ ├── components/ # Reusable components -│ │ ├── ui/ # Base UI components (Button, Text, etc.) -│ │ ├── providers/ # Provider-specific components -│ │ ├── documents/ # Document-specific components -│ │ └── common/ # Shared components -│ ├── hooks/ # Custom React hooks -│ │ ├── useProviders.ts -│ │ ├── useDocuments.ts -│ │ └── useTranslation.ts -│ ├── stores/ # Zustand stores -│ │ ├── providersStore.ts -│ │ └── uiStore.ts -│ ├── lib/ # Utilities -│ │ ├── api.ts # API client -│ │ └── utils.ts -│ └── types/ # TypeScript types -└── package.json +src/Web/MeAjudaAi.Web.Admin/ +├── app/ # Next.js App Router +│ ├── (auth)/ # Authentication routes +│ │ ├── login/ +│ │ └── layout.tsx +│ ├── (dashboard)/ # Protected routes +│ │ ├── providers/ +│ │ ├── documents/ +│ │ ├── services/ +│ │ ├── cities/ +│ │ ├── dashboard/ +│ │ └── layout.tsx +│ ├── layout.tsx +│ └── page.tsx +├── components/ # Reusable components +│ ├── ui/ # Base UI components (Button, Text, etc.) +│ ├── providers/ # Provider-specific components +│ ├── documents/ # Document-specific components +│ └── common/ # Shared components +├── hooks/ # Custom React hooks +│ ├── useProviders.ts +│ ├── useDocuments.ts +│ └── useTranslation.ts +├── stores/ # Zustand stores +│ ├── providersStore.ts +│ └── uiStore.ts +├── lib/ # Utilities +│ ├── api.ts # API client +│ └── utils.ts +└── types/ # TypeScript types ``` ### Testes E2E -Localização: `tests/MeAjudaAi.Web.Admin.Tests/e2e/` +Localização: `src/Web/MeAjudaAi.Web.Admin/e2e/` **Estrutura:** ```text -tests/MeAjudaAi.Web.Admin.Tests/ +src/Web/MeAjudaAi.Web.Admin/ └── e2e/ ├── auth.spec.ts └── providers.spec.ts ``` -**Fixtures compartilhadas:** `tests/MeAjudaAi.Web.Shared.Tests/base.ts` +**Fixtures compartilhadas:** `src/Web/libs/e2e-support/base.ts` - `loginAsAdmin(page)` - `loginAsProvider(page)` - `loginAsCustomer(page)` - `logout(page)` -``` ## 🔐 Autenticação e Autorização -### Keycloak Configuration +### Keycloak Configuration (NextAuth.js v4) **Realm**: `meajudaai` **Client ID**: `admin-portal` **Flow**: Authorization Code + PKCE **Redirect URIs**: -- `https://localhost:7001/authentication/login-callback` -- `https://localhost:7001/authentication/logout-callback` +- `https://localhost:7001/api/auth/callback/keycloak` +- `https://localhost:7001/api/auth/logout` ### Políticas de Autorização diff --git a/docs/architecture.md b/docs/architecture.md index df62f20c3..843908ddf 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2892,14 +2892,14 @@ export function ProvidersList() { ```typescript // Hook para buscar dados com cache automático -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'; export function useProviders(page: number = 1) { return useQuery({ queryKey: ['providers', page], queryFn: () => providersApi.getProviders({ pageNumber: page }), staleTime: 30 * 1000, // 30 seconds cache - keepPreviousData: true, + placeholderData: keepPreviousData, }); } @@ -3181,22 +3181,15 @@ builder.Services.AddOidcAuthentication(options => **Setup de Testes**: ```typescript -import { test, expect } from '@playwright/test'; +import { test, expect, loginAsAdmin } from '@meajudaai/web-e2e-support'; test.describe('Providers Management', () => { test.beforeEach(async ({ page }) => { - // Login como admin - await page.goto('/login'); - await page.fill('[data-testid="email"]', 'admin@meajudaai.com'); - await page.fill('[data-testid="password"]', 'password'); - await page.click('[data-testid="login-button"]'); + await loginAsAdmin(page); + await page.goto('/admin/providers'); }); test('should display providers list', async ({ page }) => { - // Act - await page.goto('/admin/providers'); - - // Assert await expect(page.locator('[data-testid="providers-table"]')).toBeVisible(); }); }); @@ -3210,29 +3203,32 @@ test.describe('Providers Management', () => { 5. **Fluent Assertions**: Usar expect para asserts expressivas ### **Estrutura de Arquivos (React)**: - var cut = RenderComponent(); - - // Assert - _mockDispatcher.Verify( - x => x.Dispatch(It.IsAny()), - Times.Once); - } - - [Fact] - public void Providers_Should_Display_Loading_State() - { - // Arrange - _mockProvidersState.Setup(x => x.Value) - .Returns(new ProvidersState { IsLoading = true }); - - // Act - var cut = RenderComponent(); - // Assert - var progressElements = cut.FindAll(".mud-progress-circular"); - progressElements.Should().NotBeEmpty(); - } -} +```text +src/Web/MeAjudaAi.Web.Admin/ +├── app/ # Next.js App Router +│ ├── (auth)/ # Authentication routes (Keycloak OAuth) +│ │ ├── login/ +│ │ └── api/auth/[...nextauth]/ +│ ├── (dashboard)/ # Protected routes +│ │ ├── providers/ +│ │ ├── documents/ +│ │ ├── services/ +│ │ ├── cities/ +│ │ ├── dashboard/ +│ │ └── layout.tsx +│ ├── layout.tsx +│ └── page.tsx +├── components/ # Reusable components +│ ├── ui/ # Base UI components +│ ├── providers/ # Provider-specific components +│ └── documents/ # Document components +├── hooks/ # Custom React hooks +│ ├── useProviders.ts +│ └── useDocuments.ts +└── e2e/ # E2E tests (co-located) + ├── auth.spec.ts + └── providers.spec.ts ``` ### **Estrutura de Arquivos** @@ -3240,19 +3236,15 @@ test.describe('Providers Management', () => { ``` src/Web/ ├── MeAjudaAi.Web.Admin/ # Admin Portal Next.js App -│ ├── app/ -│ │ ├── (auth)/ # Authentication routes (Keycloak OAuth) -│ │ └── (dashboard)/ # Protected routes -│ └── e2e/ # E2E tests (co-localized) -│ └── auth.spec.ts -├── MeAjudaAi.Web.Customer/ # Customer Web Next.js App -├── MeAjudaAi.Web.Provider/ # Provider Web Next.js App +├── MeAjudaAi.Web.Customer/ # Customer Web Next.js App +├── MeAjudaAi.Web.Provider/ # Provider Web Next.js App ├── libs/ # Shared libraries (ui, auth, api-client, assets) -└── playwright.config.ts # Single Playwright config for all apps +└── playwright.config.ts # Single Playwright config for all apps # Playwright Configuration: -# - testDir: './tests' (single test directory) +# - testDir: './src' (single test directory) # - baseURL: 'http://localhost:3000' +# - grep: /e2e/ (filter tests by e2e pattern) # - projects array: chromium, firefox, webkit, mobile devices, ci # All tests use the same playwright.config.ts with project-based browser selection. ``` diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 9346e7f59..4caa00061 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -607,7 +607,7 @@ Durante o processo de atualização automática de dependências pelo Dependabot **Foco**: Phased migration from Blazor WASM to React. **Entregáveis**: -- ✅ **Admin Portal React**: Functional `apps/admin-portal` in React. +- ✅ **Admin Portal React**: Functional `src/Web/MeAjudaAi.Web.Admin/` in React. - ✅ **Providers CRUD**: Complete provider management. - ✅ **Document Management**: Document upload and verification. - ✅ **Service Catalogs**: Service catalog management. @@ -627,7 +627,7 @@ Durante o processo de atualização automática de dependências pelo Dependabot 4. **Provider Web App Tests**: Onboarding, dashboard (`tests/MeAjudaAi.Web.Provider.Tests/e2e/`) 5. **Admin Portal Tests**: CRUD providers, documentos (`tests/MeAjudaAi.Web.Admin.Tests/e2e/`) 6. **Shared Fixtures**: `tests/MeAjudaAi.Web.Shared.Tests/base.ts` -7. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` (⏳ Habilitado em master-ci-cd.yml, pendiente em pr-validation.yml: requer RUN_E2E='true' para executar) +7. **CI Integration**: Adicionar steps em `pr-validation.yml` e `master-ci-cd.yml` (⏳ Habilitado em master-ci-cd.yml, pendente em pr-validation.yml: requer RUN_E2E='true' para executar) **Cenários de Teste**: - [ ] Autenticação (login, logout, refresh token) diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts new file mode 100644 index 000000000..e358b51ed --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts @@ -0,0 +1,147 @@ +import { test, expect, loginAsAdmin } from '@meajudaai/web-e2e-support'; + +test.describe('Admin Portal - Allowed Cities Management', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/config/cidades'); + }); + + test('should display allowed cities list', async ({ page }) => { + await expect(page.locator('[data-testid="cities-list"]')).toBeVisible(); + }); + + test('should add new allowed city', async ({ page }) => { + await page.click('button:has-text("Adicionar Cidade")'); + await expect(page.locator('[data-testid="city-form"]')).toBeVisible(); + + await page.fill('input[name="cityName"]', 'São Paulo'); + await page.fill('input[name="state"]', 'SP'); + await page.fill('input[name="maxProviders"]', '100'); + await page.click('button:has-text("Salvar")'); + + await expect(page.locator('text=São Paulo')).toBeVisible(); + await expect(page.locator('text=Cidade adicionada com sucesso')).toBeVisible(); + }); + + test('should edit existing city', async ({ page }) => { + const cityRow = page.locator('[data-testid="city-row"]').first(); + await cityRow.locator('button[aria-label="Editar"]').click(); + + await page.fill('input[name="maxProviders"]', '150'); + await page.click('button:has-text("Salvar")'); + + await expect(page.locator('text=Cidade atualizada com sucesso')).toBeVisible(); + }); + + test('should remove allowed city', async ({ page }) => { + const cityRow = page.locator('[data-testid="city-row"]').first(); + await cityRow.locator('button[aria-label="Remover"]').click(); + + await page.click('button:has-text("Confirmar")'); + + await expect(page.locator('text=Cidade removida com sucesso')).toBeVisible(); + }); + + test('should search cities', async ({ page }) => { + const searchInput = page.locator('input[name="search"]'); + await searchInput.fill('Rio'); + + await expect(page.locator('[data-testid="city-row"]')).toBeVisible(); + }); + + test('should filter by state', async ({ page }) => { + await page.click('button:has-text("Filtrar por Estado")'); + await page.click('button:has-text("RJ")'); + + await expect(page.locator('[data-testid="city-row"]')).toBeVisible(); + }); +}); + +test.describe('Admin Portal - Service Catalog Management', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/config/servicos'); + }); + + test('should display service catalog', async ({ page }) => { + await expect(page.locator('[data-testid="services-list"]')).toBeVisible(); + }); + + test('should add new service category', async ({ page }) => { + await page.click('button:has-text("Adicionar Serviço")'); + await expect(page.locator('[data-testid="service-form"]')).toBeVisible(); + + await page.fill('input[name="serviceName"]', 'Eletricista'); + await page.fill('textarea[name="description"]', 'Serviços de elétrica residencial'); + await page.fill('input[name="basePrice"]', '100'); + await page.click('button:has-text("Salvar")'); + + await expect(page.locator('text=Eletricista')).toBeVisible(); + await expect(page.locator('text=Serviço adicionado com sucesso')).toBeVisible(); + }); + + test('should edit service category', async ({ page }) => { + const serviceRow = page.locator('[data-testid="service-row"]').first(); + await serviceRow.locator('button[aria-label="Editar"]').click(); + + await page.fill('input[name="basePrice"]', '150'); + await page.click('button:has-text("Salvar")'); + + await expect(page.locator('text=Serviço atualizado com sucesso')).toBeVisible(); + }); + + test('should toggle service visibility', async ({ page }) => { + const serviceRow = page.locator('[data-testid="service-row"]').first(); + const toggle = serviceRow.locator('input[type="checkbox"]'); + + const isChecked = await toggle.isChecked(); + await toggle.click(); + + if (isChecked) { + await expect(page.locator('text=Serviço desabilitado')).toBeVisible(); + } else { + await expect(page.locator('text=Serviço habilitado')).toBeVisible(); + } + }); + + test('should delete service category', async ({ page }) => { + const serviceRow = page.locator('[data-testid="service-row"]').first(); + await serviceRow.locator('button[aria-label="Excluir"]').click(); + + await page.click('button:has-text("Confirmar")'); + + await expect(page.locator('text=Serviço excluído com sucesso')).toBeVisible(); + }); + + test('should search services', async ({ page }) => { + const searchInput = page.locator('input[name="search"]'); + await searchInput.fill('Eletricista'); + + await expect(page.locator('[data-testid="service-row"]')).toBeVisible(); + }); + + test('should filter services by status', async ({ page }) => { + await page.click('button:has-text("Filtrar")'); + await page.click('button:has-text("Ativos")'); + + await expect(page.locator('[data-testid="service-row"]')).toBeVisible(); + }); +}); + +test.describe('Admin Portal - General Settings', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + await page.goto('/admin/configuracoes'); + }); + + test('should display general settings', async ({ page }) => { + await expect(page.locator('[data-testid="general-settings"]')).toBeVisible(); + }); + + test('should update platform settings', async ({ page }) => { + await page.fill('input[name="platformFee"]', '15'); + await page.click('button:has-text("Salvar")'); + + await expect(page.locator('text=Configurações salvas com sucesso')).toBeVisible(); + }); +}); diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts index 8f36674bd..50bdbbdfb 100644 --- a/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts @@ -27,9 +27,23 @@ test.describe('Admin Portal - Providers Management', () => { await page.click('button:has-text("Filtrar")'); await page.click('text=Ativos'); - // Verify filtered results are visible + // Wait for filter to apply + await page.waitForTimeout(500); + + // Verify filtered results show only active providers const providerRows = page.locator('[data-testid="provider-row"]'); - await expect(providerRows.first()).toBeVisible(); + const rowCount = await providerRows.count(); + expect(rowCount).toBeGreaterThan(0); + + // Verify each visible row has active status + for (let i = 0; i < rowCount; i++) { + const row = providerRows.nth(i); + const statusCell = row.locator('[data-testid="provider-status"]'); + if (await statusCell.isVisible()) { + const statusText = await statusCell.textContent(); + expect(statusText).toMatch(/ativo|active/i); + } + } }); }); @@ -46,22 +60,55 @@ test.describe('Admin Portal - Documents', () => { test('should approve document', async ({ page }) => { await page.goto('/admin/documentos'); - // Get the first pending document's approve button - const approveButton = page.locator('[data-testid="document-approve"]').first(); + // Get the first provider row with pending documents + const firstProviderRow = page.locator('[data-testid="provider-row"]').first(); + await expect(firstProviderRow).toBeVisible(); + + // Click the eye icon to open provider detail view + const detailButton = firstProviderRow.locator('button[aria-label*="visualizar"], button[aria-label*="view"], [data-testid="view-details"]').first(); + await detailButton.click(); + + // Wait for detail view to load + await expect(page.locator('[data-testid="provider-detail"]')).toBeVisible(); + + // Click the approve button in the detail view + const approveButton = page.locator('button:has-text("Aprovar"), [data-testid="approve-button"]'); await expect(approveButton).toBeVisible(); await approveButton.click(); + // Verify success alert await expect(page.getByRole('alert')).toContainText(/aprova/i); + + // Return to the listing + await page.click('button:has-text("Voltar")'); + await expect(page).toHaveURL(/.*\/admin\/documentos/); + + // Verify the provider no longer appears in pending list + const providerRows = page.locator('[data-testid="provider-row"]'); + const firstRowText = await providerRows.first().textContent(); + expect(firstRowText).not.toContain('Pendente'); }); test('should reject document', async ({ page }) => { await page.goto('/admin/documentos'); - // Get the first pending document's reject button - const rejectButton = page.locator('[data-testid="document-reject"]').first(); + // Get the first provider row with pending documents + const firstProviderRow = page.locator('[data-testid="provider-row"]').first(); + await expect(firstProviderRow).toBeVisible(); + + // Click the eye icon to open provider detail view + const detailButton = firstProviderRow.locator('button[aria-label*="visualizar"], button[aria-label*="view"], [data-testid="view-details"]').first(); + await detailButton.click(); + + // Wait for detail view to load + await expect(page.locator('[data-testid="provider-detail"]')).toBeVisible(); + + // Click the reject button in the detail view + const rejectButton = page.locator('button:has-text("Rejeitar"), [data-testid="reject-button"]'); await expect(rejectButton).toBeVisible(); await rejectButton.click(); + // Verify success alert await expect(page.getByRole('alert')).toContainText(/rejeita/i); }); }); diff --git a/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts new file mode 100644 index 000000000..1eb1b5f2b --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts @@ -0,0 +1,92 @@ +import { test, expect, loginAsCustomer, logout } from '@meajudaai/web-e2e-support'; + +test.describe('Customer Web App - Onboarding', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/cadastro'); + }); + + test('should display registration form', async ({ page }) => { + await expect(page.locator('input[name="name"]')).toBeVisible(); + await expect(page.locator('input[type="email"]')).toBeVisible(); + await expect(page.locator('input[type="password"]')).toBeVisible(); + }); + + test('should complete customer registration', async ({ page }) => { + await page.fill('input[name="name"]', 'Cliente Teste'); + await page.fill('input[type="email"]', `cliente${Date.now()}@teste.com`); + await page.fill('input[type="password"]', 'Senha@123456'); + await page.fill('input[name="phone"]', '21999999999'); + await page.click('button:has-text("Cadastrar")'); + await expect(page).toHaveURL(/.*\/cadastro\/endereco|.*\/inicio/); + }); + + test('should validate required fields', async ({ page }) => { + await page.click('button:has-text("Cadastrar")'); + await expect(page.locator('text=Campo obrigatório')).toBeVisible(); + }); + + test('should validate email format', async ({ page }) => { + await page.fill('input[name="name"]', 'Cliente Teste'); + await page.fill('input[type="email"]', 'email-invalido'); + await page.fill('input[type="password"]', 'Senha@123'); + await page.fill('input[name="phone"]', '21999999999'); + await page.click('button:has-text("Cadastrar")'); + await expect(page.locator('text=email inválido')).toBeVisible(); + }); + + test('should validate password strength', async ({ page }) => { + await page.fill('input[name="name"]', 'Cliente Teste'); + await page.fill('input[type="email"]', 'teste@teste.com'); + await page.fill('input[type="password"]', 'fraca'); + await page.fill('input[name="phone"]', '21999999999'); + await page.click('button:has-text("Cadastrar")'); + await expect(page.locator(/senha fraca|caracteres mínimos/i)).toBeVisible(); + }); +}); + +test.describe('Customer Web App - Onboarding Address', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/cadastro/endereco'); + }); + + test('should display address form', async ({ page }) => { + await expect(page.locator('input[name="cep"]')).toBeVisible(); + await expect(page.locator('input[name="street"]')).toBeVisible(); + await expect(page.locator('input[name="number"]')).toBeVisible(); + }); + + test('should complete address step', async ({ page }) => { + await page.fill('input[name="cep"]', '20550160'); + await page.fill('input[name="street"]', 'Rua Teste'); + await page.fill('input[name="number"]', '123'); + await page.fill('input[name="neighborhood"]', 'Bairro Teste'); + await page.fill('input[name="city"]', 'Rio de Janeiro'); + await page.fill('input[name="state"]', 'RJ'); + await page.click('button:has-text("Próximo")'); + await expect(page).toHaveURL(/.*\/inicio/); + }); +}); + +test.describe('Customer Web App - Complete Onboarding Flow', () => { + test('should complete full onboarding journey', async ({ page }) => { + await page.goto('/cadastro'); + + await page.fill('input[name="name"]', 'Cliente Completo'); + await page.fill('input[type="email"]', `comple${Date.now()}@teste.com`); + await page.fill('input[type="password"]', 'Senha@123456'); + await page.fill('input[name="phone"]', '21999999999'); + await page.click('button:has-text("Cadastrar")'); + + await page.waitForURL(/.*\/cadastro\/endereco|.*\/inicio/); + + if (await page.locator('input[name="cep"]').isVisible()) { + await page.fill('input[name="cep"]', '20550160'); + await page.fill('input[name="street"]', 'Rua Teste'); + await page.fill('input[name="number"]', '123'); + await page.fill('input[name="neighborhood"]', 'Bairro Teste'); + await page.click('button:has-text("Próximo")'); + } + + await expect(page).toHaveURL(/.*\/inicio/); + }); +}); diff --git a/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts new file mode 100644 index 000000000..58ad83ae6 --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts @@ -0,0 +1,199 @@ +import { test, expect, devices } from '@playwright/test'; + +const desktopViewport = { width: 1920, height: 1080 }; +const tabletViewport = { width: 768, height: 1024 }; +const mobileViewport = { width: 375, height: 667 }; + +test.describe('Customer Web App - Mobile Responsiveness', () => { + test('should render correctly on mobile viewport', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/'); + + await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible(); + await expect(page.locator('nav')).toBeVisible(); + }); + + test('should have touch-friendly tap targets on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/'); + + const searchButton = page.locator('button:has-text("Buscar")').first(); + const box = await searchButton.boundingBox(); + + expect(box?.height).toBeGreaterThanOrEqual(44); + expect(box?.width).toBeGreaterThanOrEqual(44); + }); + + test('should display mobile-friendly navigation', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/'); + + await page.click('[data-testid="mobile-menu-toggle"]'); + await expect(page.locator('[data-testid="mobile-nav"]')).toBeVisible(); + }); + + test('should adapt forms for mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/busca'); + + const formInputs = page.locator('input, select, textarea'); + const count = await formInputs.count(); + expect(count).toBeGreaterThan(0); + + const firstInput = formInputs.first(); + const box = await firstInput.boundingBox(); + expect(box?.width).toBeLessThanOrEqual(mobileViewport.width - 32); + }); +}); + +test.describe('Provider Web App - Mobile Responsiveness', () => { + test('should render correctly on mobile viewport', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/provider/dashboard'); + + await expect(page.locator('nav')).toBeVisible(); + }); + + test('should have touch-friendly elements on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/provider/dashboard'); + + const actionButtons = page.locator('button'); + const count = await actionButtons.count(); + + for (let i = 0; i < Math.min(count, 5); i++) { + const button = actionButtons.nth(i); + const box = await button.boundingBox(); + if (box) { + expect(box.height).toBeGreaterThanOrEqual(44); + } + } + }); +}); + +test.describe('Admin Portal - Mobile Responsiveness', () => { + test('should render correctly on mobile viewport', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/admin/dashboard'); + + await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible(); + }); + + test('should collapse sidebar on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/admin/dashboard'); + + const sidebar = page.locator('[data-testid="sidebar"]'); + await expect(sidebar).not.toBeVisible(); + }); + + test('should display hamburger menu on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/admin/dashboard'); + + await expect(page.locator('[data-testid="mobile-menu-toggle"]')).toBeVisible(); + }); +}); + +test.describe('Performance - Core Web Vitals', () => { + test('should meet LCP threshold on homepage', async ({ page }) => { + await page.goto('/'); + + const metrics = await page.evaluate(() => { + return new Promise((resolve) => { + new PerformanceObserver((list) => { + const entries = list.getEntries(); + const lcpEntry = entries.find((entry) => entry.entryType === 'largest-contentful-paint'); + resolve({ lcp: lcpEntry ? lcpEntry.startTime : null }); + }).observe({ type: 'largest-contentful-paint', buffered: true }); + + setTimeout(() => resolve({ lcp: null }), 5000); + }); + }); + + if (metrics.lcp) { + expect(metrics.lcp).toBeLessThan(2500); + } + }); + + test('should meet FID threshold', async ({ page }) => { + await page.goto('/'); + + const metrics = await page.evaluate(() => { + return new Promise((resolve) => { + new PerformanceObserver((list) => { + const entries = list.getEntries(); + const fidEntry = entries.find((entry) => entry.entryType === 'first-input'); + resolve({ fid: fidEntry ? (fidEntry as any).processingStart - fidEntry.startTime : null }); + }).observe({ type: 'first-input', buffered: true }); + + setTimeout(() => resolve({ fid: null }), 5000); + }); + }); + + if (metrics.fid) { + expect(metrics.fid).toBeLessThan(100); + } + }); + + test('should meet CLS threshold', async ({ page }) => { + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + const metrics = await page.evaluate(() => { + const entries = performance.getEntriesByType('layout-shift') as any[]; + let cls = 0; + entries.forEach((entry) => { + if (!entry.hadRecentInput) { + cls += entry.value; + } + }); + return { cls }; + }); + + expect(metrics.cls).toBeLessThan(0.1); + }); + + test('should load page within acceptable time', async ({ page }) => { + const startTime = Date.now(); + await page.goto('/'); + await page.waitForLoadState('domcontentloaded'); + const loadTime = Date.now() - startTime; + + expect(loadTime).toBeLessThan(3000); + }); +}); + +test.describe('Performance - Network', () => { + test('should optimize images', async ({ page }) => { + await page.goto('/'); + + const images = await page.locator('img').evaluateAll((imgs) => { + return imgs.map((img) => ({ + src: img.src, + naturalWidth: img.naturalWidth, + loading: img.loading + })); + }); + + const imagesWithSrc = images.filter((img) => img.src && img.naturalWidth > 0); + expect(imagesWithSrc.length).toBeGreaterThan(0); + + const lazyLoadedImages = imagesWithSrc.filter((img) => img.loading === 'lazy'); + expect(lazyLoadedImages.length).toBeGreaterThan(0); + }); + + test('should not have excessive requests', async ({ page }) => { + const requests: string[] = []; + page.on('request', (request) => { + if (request.url().includes('localhost') || request.url().includes('127.0.0.1')) { + requests.push(request.url()); + } + }); + + await page.goto('/'); + await page.waitForLoadState('networkidle'); + + expect(requests.length).toBeLessThan(50); + }); +}); diff --git a/src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts b/src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts index 02a8b4ba6..c7847e916 100644 --- a/src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts +++ b/src/Web/MeAjudaAi.Web.Provider/e2e/onboarding.spec.ts @@ -23,6 +23,90 @@ test.describe('Provider Web App - Onboarding', () => { }); }); +test.describe('Provider Web App - Document Upload', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/provider/onboarding/documentos'); + }); + + test('should display document upload section', async ({ page }) => { + await expect(page.locator('[data-testid="document-upload"]')).toBeVisible(); + await expect(page.locator('input[type="file"]')).toBeVisible(); + }); + + test('should upload document successfully', async ({ page }) => { + const fileInput = page.locator('input[type="file"]').first(); + + await fileInput.setInputFiles({ + name: 'documento-teste.pdf', + mimeType: 'application/pdf', + buffer: Buffer.from('dummy pdf content') + }); + + await expect(page.locator('text=documento-teste.pdf')).toBeVisible(); + await expect(page.locator('text=Upload concluído')).toBeVisible(); + }); + + test('should validate file type', async ({ page }) => { + const fileInput = page.locator('input[type="file"]').first(); + + await fileInput.setInputFiles({ + name: 'imagem-teste.txt', + mimeType: 'text/plain', + buffer: Buffer.from('not an image') + }); + + await expect(page.locator(/tipo de arquivo inválido|formato não permitido/i)).toBeVisible(); + }); + + test('should validate file size', async ({ page }) => { + const fileInput = page.locator('input[type="file"]').first(); + + const largeBuffer = Buffer.alloc(11 * 1024 * 1024); + await fileInput.setInputFiles({ + name: 'arquivo-grande.pdf', + mimeType: 'application/pdf', + buffer: largeBuffer + }); + + await expect(page.locator(/arquivo muito grande|tamanho máximo/i)).toBeVisible(); + }); + + test('should proceed after document upload', async ({ page }) => { + const fileInput = page.locator('input[type="file"]').first(); + + await fileInput.setInputFiles({ + name: 'documento-teste.pdf', + mimeType: 'application/pdf', + buffer: Buffer.from('dummy pdf content') + }); + + await page.waitForSelector('text=Upload concluído'); + await page.click('button:has-text("Próximo")'); + await expect(page).toHaveURL(/.*\/onboarding\/servicos/); + }); +}); + +test.describe('Provider Web App - Onboarding Services', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/provider/onboarding/servicos'); + }); + + test('should display service selection', async ({ page }) => { + await expect(page.locator('[data-testid="service-selection"]')).toBeVisible(); + }); + + test('should select service categories', async ({ page }) => { + const serviceCheckbox = page.locator('input[type="checkbox"]').first(); + await serviceCheckbox.check(); + await expect(serviceCheckbox).toBeChecked(); + }); + + test('should complete onboarding flow', async ({ page }) => { + await page.click('button:has-text("Próximo")'); + await expect(page).toHaveURL(/.*\/provider\/dashboard/); + }); +}); + test.describe('Provider Web App - Dashboard', () => { test('should display dashboard metrics', async ({ page }) => { await page.goto('/provider/dashboard'); diff --git a/src/Web/MeAjudaAi.Web.Provider/e2e/profile-mgmt.spec.ts b/src/Web/MeAjudaAi.Web.Provider/e2e/profile-mgmt.spec.ts new file mode 100644 index 000000000..faad48d50 --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Provider/e2e/profile-mgmt.spec.ts @@ -0,0 +1,195 @@ +import { test, expect, loginAsProvider, logout } from '@meajudaai/web-e2e-support'; + +test.describe('Provider Web App - Profile Management', () => { + test.beforeEach(async ({ page }) => { + await loginAsProvider(page); + await page.goto('/provider/perfil'); + }); + + test('should display profile information', async ({ page }) => { + await expect(page.locator('[data-testid="profile-header"]')).toBeVisible(); + await expect(page.locator('input[name="name"]')).toBeVisible(); + await expect(page.locator('input[name="phone"]')).toBeVisible(); + await expect(page.locator('input[name="email"]')).toBeVisible(); + }); + + test('should update profile information', async ({ page }) => { + await page.fill('input[name="name"]', 'João Silva Atualizado'); + await page.fill('input[name="phone"]', '21988888888'); + await page.click('button:has-text("Salvar")'); + + await expect(page.locator('text=Perfil atualizado com sucesso')).toBeVisible(); + }); + + test('should update profile photo', async ({ page }) => { + const photoInput = page.locator('input[type="file"][accept*="image"]'); + await photoInput.setInputFiles({ + name: 'profile-photo.jpg', + mimeType: 'image/jpeg', + buffer: Buffer.from('dummy image data') + }); + + await expect(page.locator('text=Foto atualizada com sucesso')).toBeVisible(); + }); + + test('should validate required fields', async ({ page }) => { + await page.fill('input[name="name"]', ''); + await page.click('button:has-text("Salvar")'); + await expect(page.locator('text=Nome é obrigatório')).toBeVisible(); + }); + + test('should validate phone format', async ({ page }) => { + await page.fill('input[name="phone"]', 'invalid'); + await page.click('button:has-text("Salvar")'); + await expect(page.locator(/telefone inválido/i)).toBeVisible(); + }); +}); + +test.describe('Provider Web App - Profile Visibility Settings', () => { + test.beforeEach(async ({ page }) => { + await loginAsProvider(page); + await page.goto('/provider/perfil/visibilidade'); + }); + + test('should display visibility toggles', async ({ page }) => { + await expect(page.locator('[data-testid="visibility-settings"]')).toBeVisible(); + await expect(page.locator('input[type="checkbox"]')).toBeVisible(); + }); + + test('should toggle profile visibility', async ({ page }) => { + const visibilityToggle = page.locator('[data-testid="profile-visibility-toggle"]'); + const isChecked = await visibilityToggle.isChecked(); + + await visibilityToggle.click(); + + if (isChecked) { + await expect(page.locator('text=Perfil ocultado dos clientes')).toBeVisible(); + } else { + await expect(page.locator('text=Perfil visível para clientes')).toBeVisible(); + } + }); + + test('should toggle phone visibility', async ({ page }) => { + const phoneToggle = page.locator('[data-testid="phone-visibility-toggle"]'); + const isChecked = await phoneToggle.isChecked(); + + await phoneToggle.click(); + + if (isChecked) { + await expect(page.locator('text=Telefone oculto')).toBeVisible(); + } else { + await expect(page.locator('text=Telefone visível')).toBeVisible(); + } + }); + + test('should toggle WhatsApp visibility', async ({ page }) => { + const whatsappToggle = page.locator('[data-testid="whatsapp-visibility-toggle"]'); + const isChecked = await whatsappToggle.isChecked(); + + await whatsappToggle.click(); + + if (isChecked) { + await expect(page.locator('text=WhatsApp oculto')).toBeVisible(); + } else { + await expect(page.locator('text=WhatsApp visível')).toBeVisible(); + } + }); + + test('should save visibility settings', async ({ page }) => { + await page.click('button:has-text("Salvar Configurações")'); + await expect(page.locator('text=Configurações salvas com sucesso')).toBeVisible(); + }); +}); + +test.describe('Provider Web App - LGPD Account Deletion', () => { + test.beforeEach(async ({ page }) => { + await loginAsProvider(page); + await page.goto('/provider/perfil/privacidade'); + }); + + test('should display LGPD options', async ({ page }) => { + await expect(page.locator('[data-testid="lgpd-section"]')).toBeVisible(); + await expect(page.locator('text=Excluir minha conta')).toBeVisible(); + await expect(page.locator('text=LGPD')).toBeVisible(); + }); + + test('should initiate account deletion flow', async ({ page }) => { + await page.click('button:has-text("Excluir minha conta")'); + await expect(page.locator('[data-testid="deletion-confirmation"]')).toBeVisible(); + }); + + test('should require confirmation for deletion', async ({ page }) => { + await page.click('button:has-text("Excluir minha conta")'); + await page.click('button:has-text("Confirmar Exclusão")'); + + await expect(page.locator(/confirme digitando/i)).toBeVisible(); + }); + + test('should delete account with correct confirmation', async ({ page }) => { + await page.click('button:has-text("Excluir minha conta")'); + + const confirmationInput = page.locator('input[name="confirmationText"]'); + await confirmationInput.fill('EXCLUIR'); + + await page.click('button:has-text("Confirmar Exclusão")'); + + await expect(page).toHaveURL(/.*\/login/); + await expect(page.locator('text=Conta excluída com sucesso')).toBeVisible(); + }); + + test('should cancel deletion flow', async ({ page }) => { + await page.click('button:has-text("Excluir minha cuenta")'); + await page.click('button:has-text("Cancelar")'); + + await expect(page.locator('[data-testid="deletion-confirmation"]')).not.toBeVisible(); + }); + + test('should display data export option', async ({ page }) => { + await expect(page.locator('text=Exportar meus dados')).toBeVisible(); + }); + + test('should request data export', async ({ page }) => { + await page.click('button:has-text("Exportar meus dados")'); + await expect(page.locator('text=Solicitação de exportação enviada')).toBeVisible(); + }); +}); + +test.describe('Provider Web App - Password Management', () => { + test.beforeEach(async ({ page }) => { + await loginAsProvider(page); + await page.goto('/provider/perfil/senha'); + }); + + test('should display password change form', async ({ page }) => { + await expect(page.locator('input[name="currentPassword"]')).toBeVisible(); + await expect(page.locator('input[name="newPassword"]')).toBeVisible(); + await expect(page.locator('input[name="confirmPassword"]')).toBeVisible(); + }); + + test('should update password successfully', async ({ page }) => { + await page.fill('input[name="currentPassword"]', 'Senha@123'); + await page.fill('input[name="newPassword"]', 'NovaSenha@456'); + await page.fill('input[name="confirmPassword"]', 'NovaSenha@456'); + await page.click('button:has-text("Alterar Senha")'); + + await expect(page.locator('text=Senha alterada com sucesso')).toBeVisible(); + }); + + test('should validate password mismatch', async ({ page }) => { + await page.fill('input[name="currentPassword"]', 'Senha@123'); + await page.fill('input[name="newPassword"]', 'NovaSenha@456'); + await page.fill('input[name="confirmPassword"]', 'SenhaDiferente@789'); + await page.click('button:has-text("Alterar Senha")'); + + await expect(page.locator('text=Senhas não conferem')).toBeVisible(); + }); + + test('should validate password strength', async ({ page }) => { + await page.fill('input[name="currentPassword"]', 'Senha@123'); + await page.fill('input[name="newPassword"]', 'fraca'); + await page.fill('input[name="confirmPassword"]', 'fraca'); + await page.click('button:has-text("Alterar Senha")'); + + await expect(page.locator(/senha fraca|caracteres mínimos/i)).toBeVisible(); + }); +}); From adb928de13bfe64d8cb84aebf95b1e72360442a2 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 20:35:35 -0300 Subject: [PATCH 011/142] feat: Establish comprehensive project roadmap, architecture documentation, and initial e2e test suite for web applications. --- docs/admin-portal/overview.md | 4 +- docs/architecture.md | 119 +++--------------- docs/roadmap-current.md | 8 +- .../MeAjudaAi.Web.Admin/e2e/configs.spec.ts | 25 ++-- .../e2e/performance.spec.ts | 31 +++++ .../MeAjudaAi.Web.Admin/e2e/providers.spec.ts | 9 +- .../e2e/onboarding.spec.ts | 6 +- .../e2e/performance.spec.ts | 106 +++++++--------- .../e2e/performance.spec.ts | 28 +++++ src/Web/libs/e2e-support/base.ts | 41 +++++- 10 files changed, 188 insertions(+), 189 deletions(-) create mode 100644 src/Web/MeAjudaAi.Web.Admin/e2e/performance.spec.ts create mode 100644 src/Web/MeAjudaAi.Web.Provider/e2e/performance.spec.ts diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index c3d311f20..6508d4b10 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -230,8 +230,8 @@ O Admin Portal segue as diretrizes **WCAG 2.1 AA**: ### E2E Tests com Playwright - Testes end-to-end para todos os fluxos principais -- Localização: `tests/MeAjudaAi.Web.Admin.Tests/e2e/` -- Os testes exercising the OAuth flow via Keycloak (signIn("keycloak")) em vez de formulários de email/password +- Localização: `src/Web/MeAjudaAi.Web.Admin/e2e/` +- Os testes exercitam o fluxo OAuth via Keycloak (signIn('keycloak')) em vez de formulários de email/senha ### Executar Testes diff --git a/docs/architecture.md b/docs/architecture.md index 843908ddf..8c65ece79 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2893,6 +2893,7 @@ export function ProvidersList() { ```typescript // Hook para buscar dados com cache automático import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query'; +import { providersApi } from '@/libs/api-client'; export function useProviders(page: number = 1) { return useQuery({ @@ -2914,101 +2915,6 @@ export function useCreateProvider() { } ``` -### **TanStack Query - Data Fetching Patterns** -**MeAjudaAi.Client.Contracts é o SDK oficial .NET** para consumir a API REST, semelhante ao AWS SDK ou Stripe SDK. - -**SDKs Disponíveis** (Sprint 6-7): - -| Módulo | Interface | Funcionalidades | Status | -|--------|-----------|-----------------|--------| -| **Providers** | IProvidersApi | CRUD, verificação, filtros | ✅ Completo | -| **Documents** | IDocumentsApi | Upload, verificação, status | ✅ Completo | -| **ServiceCatalogs** | IServiceCatalogsApi | Listagem, categorias | ✅ Completo | -| **Locations** | ILocationsApi | CRUD AllowedCities | ✅ Completo | -| **Users** | IUsersApi | (Planejado) | ⏳ Sprint 8+ | - -**Definição de API Contracts**: - -```csharp -public interface IProvidersApi -{ - [Get("/api/v1/providers")] - Task>> GetProvidersAsync( - [Query] int pageNumber = 1, - [Query] int pageSize = 20, - CancellationToken cancellationToken = default); - - [Get("/api/v1/providers/verification-status/{status}")] - Task>> GetProvidersByVerificationStatusAsync( - string status, - CancellationToken cancellationToken = default); -} - -public interface IDocumentsApi -{ - [Multipart] - [Post("/api/v1/providers/{providerId}/documents")] - Task> UploadDocumentAsync( - Guid providerId, - [AliasAs("file")] StreamPart file, - [AliasAs("documentType")] string documentType, - CancellationToken cancellationToken = default); -} - -public interface ILocationsApi -{ - [Get("/api/v1/locations/allowed-cities")] - Task>> GetAllAllowedCitiesAsync( - [Query] bool onlyActive = true, - CancellationToken cancellationToken = default); -} - -public interface IServiceCatalogsApi -{ - [Get("/api/v1/service-catalogs/services")] - Task>> GetAllServicesAsync( - [Query] bool activeOnly = true, - CancellationToken cancellationToken = default); -} -``` - -**Configuração com Autenticação**: - -```csharp -// Program.cs - Registrar todos os SDKs -builder.Services.AddRefitClient() - .ConfigureHttpClient(c => c.BaseAddress = new Uri(apiBaseUrl)) - .AddHttpMessageHandler(); - -builder.Services.AddRefitClient() - .ConfigureHttpClient(c => c.BaseAddress = new Uri(apiBaseUrl)) - .AddHttpMessageHandler(); - -builder.Services.AddRefitClient() - .ConfigureHttpClient(c => c.BaseAddress = new Uri(apiBaseUrl)) - .AddHttpMessageHandler(); - -builder.Services.AddRefitClient() - .ConfigureHttpClient(c => c.BaseAddress = new Uri(apiBaseUrl)) - .AddHttpMessageHandler(); -``` - -**Arquitetura Interna do Refit**: - -```text -Blazor Component → IProvidersApi (interface) → Refit CodeGen → HttpClient → API -``` - -**Vantagens**: -- ✅ Type-safe API calls (compile-time validation) -- ✅ Automatic serialization/deserialization -- ✅ Integration with HttpClientFactory + Polly -- ✅ Authentication header injection via message handler -- ✅ **20 linhas de código manual → 2 linhas (interface + atributo)** -- ✅ Reutilizável entre projetos .NET (ex.: .NET 6/8, Blazor, Xamarin) - -**Documentação Completa**: `src/Web/MeAjudaAi.Web.Customer/types/README.md` - ### **React + Tailwind CSS Components** **Componentes Principais Utilizados**: @@ -3233,20 +3139,33 @@ src/Web/MeAjudaAi.Web.Admin/ ### **Estrutura de Arquivos** -``` +```text src/Web/ ├── MeAjudaAi.Web.Admin/ # Admin Portal Next.js App +│ └── e2e/ # E2E tests (co-located) +│ ├── auth.spec.ts +│ └── providers.spec.ts ├── MeAjudaAi.Web.Customer/ # Customer Web Next.js App +│ └── e2e/ +│ ├── auth.spec.ts +│ ├── search.spec.ts +│ ├── onboarding.spec.ts +│ └── performance.spec.ts ├── MeAjudaAi.Web.Provider/ # Provider Web Next.js App -├── libs/ # Shared libraries (ui, auth, api-client, assets) -└── playwright.config.ts # Single Playwright config for all apps +│ └── e2e/ +│ ├── auth.spec.ts +│ ├── onboarding.spec.ts +│ └── profile-mgmt.spec.ts +├── libs/ # Shared libraries +│ ├── e2e-support/ # Shared E2E fixtures +│ └── api-client/ # API client +└── playwright.config.ts # Single Playwright config # Playwright Configuration: # - testDir: './src' (single test directory) # - baseURL: 'http://localhost:3000' # - grep: /e2e/ (filter tests by e2e pattern) -# - projects array: chromium, firefox, webkit, mobile devices, ci -# All tests use the same playwright.config.ts with project-based browser selection. +# - projects: chromium, firefox, webkit, mobile, ci ``` ### **Best Practices - Frontend** diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index 4caa00061..a2a440e43 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -671,9 +671,11 @@ Durante o processo de atualização automática de dependências pelo Dependabot - **Problema Potencial**: App bundle size > 5MB, lazy loading não configurado corretamente - **Impacto**: UX ruim, +2-3 dias de otimização - **Mitigação Sprint 9**: - - Implementar lazy loading de assemblies - - Otimizar bundle size (tree shaking, AOT compilation) - - Adicionar loading indicators e progressive loading + - Code splitting with dynamic imports + - Tree shaking and bundle optimization + - SSR/SSG via Next.js to improve initial load + - Lazy load React components + - Optimize images using next/image and responsive formats ### Risk Scenario 4: MAUI Hybrid Platform-Specific Issues diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts index e358b51ed..5950e074d 100644 --- a/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/configs.spec.ts @@ -14,9 +14,9 @@ test.describe('Admin Portal - Allowed Cities Management', () => { await page.click('button:has-text("Adicionar Cidade")'); await expect(page.locator('[data-testid="city-form"]')).toBeVisible(); - await page.fill('input[name="cityName"]', 'São Paulo'); + await page.fill('input[name="city"]', 'São Paulo'); await page.fill('input[name="state"]', 'SP'); - await page.fill('input[name="maxProviders"]', '100'); + await page.fill('input[name="serviceRadiusKm"]', '100'); await page.click('button:has-text("Salvar")'); await expect(page.locator('text=São Paulo')).toBeVisible(); @@ -27,7 +27,7 @@ test.describe('Admin Portal - Allowed Cities Management', () => { const cityRow = page.locator('[data-testid="city-row"]').first(); await cityRow.locator('button[aria-label="Editar"]').click(); - await page.fill('input[name="maxProviders"]', '150'); + await page.fill('input[name="serviceRadiusKm"]', '150'); await page.click('button:has-text("Salvar")'); await expect(page.locator('text=Cidade atualizada com sucesso')).toBeVisible(); @@ -71,9 +71,9 @@ test.describe('Admin Portal - Service Catalog Management', () => { await page.click('button:has-text("Adicionar Serviço")'); await expect(page.locator('[data-testid="service-form"]')).toBeVisible(); - await page.fill('input[name="serviceName"]', 'Eletricista'); + await page.fill('input[name="name"]', 'Eletricista'); await page.fill('textarea[name="description"]', 'Serviços de elétrica residencial'); - await page.fill('input[name="basePrice"]', '100'); + await page.selectOption('select[name="categoryId"]', { index: 1 }); await page.click('button:has-text("Salvar")'); await expect(page.locator('text=Eletricista')).toBeVisible(); @@ -84,7 +84,7 @@ test.describe('Admin Portal - Service Catalog Management', () => { const serviceRow = page.locator('[data-testid="service-row"]').first(); await serviceRow.locator('button[aria-label="Editar"]').click(); - await page.fill('input[name="basePrice"]', '150'); + await page.fill('input[name="name"]', 'Eletricista Atualizado'); await page.click('button:has-text("Salvar")'); await expect(page.locator('text=Serviço atualizado com sucesso')).toBeVisible(); @@ -128,18 +128,21 @@ test.describe('Admin Portal - Service Catalog Management', () => { }); }); -test.describe('Admin Portal - General Settings', () => { +test.describe('Admin Portal - Profile Settings', () => { test.beforeEach(async ({ page }) => { await loginAsAdmin(page); await page.goto('/admin/configuracoes'); }); - test('should display general settings', async ({ page }) => { - await expect(page.locator('[data-testid="general-settings"]')).toBeVisible(); + test('should display profile settings', async ({ page }) => { + await expect(page.locator('text=Perfil')).toBeVisible(); + await expect(page.locator('input[name="adminName"]')).toBeVisible(); + await expect(page.locator('input[name="adminEmail"]')).toBeVisible(); }); - test('should update platform settings', async ({ page }) => { - await page.fill('input[name="platformFee"]', '15'); + test('should update profile settings', async ({ page }) => { + await page.fill('input[name="adminName"]', 'Admin Atualizado'); + await page.fill('input[name="adminEmail"]', 'admin.atualizado@meajudaai.com'); await page.click('button:has-text("Salvar")'); await expect(page.locator('text=Configurações salvas com sucesso')).toBeVisible(); diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/performance.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/performance.spec.ts new file mode 100644 index 000000000..17db105bd --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/performance.spec.ts @@ -0,0 +1,31 @@ +import { test, expect, loginAsAdmin } from '@meajudaai/web-e2e-support'; + +const mobileViewport = { width: 375, height: 667 }; + +test.describe('Admin Portal - Mobile Responsiveness', () => { + test.beforeEach(async ({ page }) => { + await loginAsAdmin(page); + }); + + test('should render correctly on mobile viewport', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/admin/dashboard'); + + await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible(); + }); + + test('should collapse sidebar on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/admin/dashboard'); + + const sidebar = page.locator('[data-testid="sidebar"]'); + await expect(sidebar).not.toBeVisible(); + }); + + test('should display hamburger menu on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/admin/dashboard'); + + await expect(page.locator('[data-testid="mobile-menu-toggle"]')).toBeVisible(); + }); +}); diff --git a/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts index 50bdbbdfb..a74849ee6 100644 --- a/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts +++ b/src/Web/MeAjudaAi.Web.Admin/e2e/providers.spec.ts @@ -27,11 +27,12 @@ test.describe('Admin Portal - Providers Management', () => { await page.click('button:has-text("Filtrar")'); await page.click('text=Ativos'); - // Wait for filter to apply - await page.waitForTimeout(500); + // Wait for filter response + await page.waitForResponse(response => response.url().includes('providers') && response.status() === 200); // Verify filtered results show only active providers const providerRows = page.locator('[data-testid="provider-row"]'); + await providerRows.first().waitFor({ state: 'visible' }); const rowCount = await providerRows.count(); expect(rowCount).toBeGreaterThan(0); @@ -77,7 +78,7 @@ test.describe('Admin Portal - Documents', () => { await approveButton.click(); // Verify success alert - await expect(page.getByRole('alert')).toContainText(/aprova/i); + await expect(page.getByRole('status')).toContainText(/aprova/i); // Return to the listing await page.click('button:has-text("Voltar")'); @@ -109,6 +110,6 @@ test.describe('Admin Portal - Documents', () => { await rejectButton.click(); // Verify success alert - await expect(page.getByRole('alert')).toContainText(/rejeita/i); + await expect(page.getByRole('status')).toContainText(/rejeita/i); }); }); diff --git a/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts index 1eb1b5f2b..14ca4e0b9 100644 --- a/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/onboarding.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, loginAsCustomer, logout } from '@meajudaai/web-e2e-support'; +import { test, expect } from '@meajudaai/web-e2e-support'; test.describe('Customer Web App - Onboarding', () => { test.beforeEach(async ({ page }) => { @@ -31,7 +31,7 @@ test.describe('Customer Web App - Onboarding', () => { await page.fill('input[type="password"]', 'Senha@123'); await page.fill('input[name="phone"]', '21999999999'); await page.click('button:has-text("Cadastrar")'); - await expect(page.locator('text=email inválido')).toBeVisible(); + await expect(page.locator(/email inválido/i)).toBeVisible(); }); test('should validate password strength', async ({ page }) => { @@ -84,6 +84,8 @@ test.describe('Customer Web App - Complete Onboarding Flow', () => { await page.fill('input[name="street"]', 'Rua Teste'); await page.fill('input[name="number"]', '123'); await page.fill('input[name="neighborhood"]', 'Bairro Teste'); + await page.fill('input[name="city"]', 'Rio de Janeiro'); + await page.fill('input[name="state"]', 'RJ'); await page.click('button:has-text("Próximo")'); } diff --git a/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts b/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts index 58ad83ae6..a47ccfb72 100644 --- a/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts +++ b/src/Web/MeAjudaAi.Web.Customer/e2e/performance.spec.ts @@ -46,68 +46,32 @@ test.describe('Customer Web App - Mobile Responsiveness', () => { }); }); -test.describe('Provider Web App - Mobile Responsiveness', () => { - test('should render correctly on mobile viewport', async ({ page }) => { - await page.setViewportSize(mobileViewport); - await page.goto('/provider/dashboard'); - - await expect(page.locator('nav')).toBeVisible(); - }); - - test('should have touch-friendly elements on mobile', async ({ page }) => { - await page.setViewportSize(mobileViewport); - await page.goto('/provider/dashboard'); - - const actionButtons = page.locator('button'); - const count = await actionButtons.count(); - - for (let i = 0; i < Math.min(count, 5); i++) { - const button = actionButtons.nth(i); - const box = await button.boundingBox(); - if (box) { - expect(box.height).toBeGreaterThanOrEqual(44); - } - } - }); -}); - -test.describe('Admin Portal - Mobile Responsiveness', () => { - test('should render correctly on mobile viewport', async ({ page }) => { - await page.setViewportSize(mobileViewport); - await page.goto('/admin/dashboard'); - - await expect(page.locator('[data-testid="mobile-menu"]')).toBeVisible(); - }); - - test('should collapse sidebar on mobile', async ({ page }) => { - await page.setViewportSize(mobileViewport); - await page.goto('/admin/dashboard'); - - const sidebar = page.locator('[data-testid="sidebar"]'); - await expect(sidebar).not.toBeVisible(); - }); - - test('should display hamburger menu on mobile', async ({ page }) => { - await page.setViewportSize(mobileViewport); - await page.goto('/admin/dashboard'); - - await expect(page.locator('[data-testid="mobile-menu-toggle"]')).toBeVisible(); - }); -}); - test.describe('Performance - Core Web Vitals', () => { test('should meet LCP threshold on homepage', async ({ page }) => { await page.goto('/'); const metrics = await page.evaluate(() => { return new Promise((resolve) => { - new PerformanceObserver((list) => { + let resolved = false; + const observer = new PerformanceObserver((list) => { + if (resolved) return; const entries = list.getEntries(); const lcpEntry = entries.find((entry) => entry.entryType === 'largest-contentful-paint'); - resolve({ lcp: lcpEntry ? lcpEntry.startTime : null }); - }).observe({ type: 'largest-contentful-paint', buffered: true }); + if (lcpEntry) { + resolved = true; + observer.disconnect(); + resolve({ lcp: lcpEntry.startTime }); + } + }); + observer.observe({ type: 'largest-contentful-paint', buffered: true }); - setTimeout(() => resolve({ lcp: null }), 5000); + const timeoutId = setTimeout(() => { + if (!resolved) { + resolved = true; + observer.disconnect(); + resolve({ lcp: null }); + } + }, 5000); }); }); @@ -119,15 +83,30 @@ test.describe('Performance - Core Web Vitals', () => { test('should meet FID threshold', async ({ page }) => { await page.goto('/'); - const metrics = await page.evaluate(() => { + const metrics = await page.evaluate(async () => { return new Promise((resolve) => { - new PerformanceObserver((list) => { + let resolved = false; + const observer = new PerformanceObserver((list) => { + if (resolved) return; const entries = list.getEntries(); const fidEntry = entries.find((entry) => entry.entryType === 'first-input'); - resolve({ fid: fidEntry ? (fidEntry as any).processingStart - fidEntry.startTime : null }); - }).observe({ type: 'first-input', buffered: true }); + if (fidEntry) { + resolved = true; + observer.disconnect(); + resolve({ fid: (fidEntry as any).processingStart - fidEntry.startTime }); + } + }); + observer.observe({ type: 'first-input', buffered: true }); - setTimeout(() => resolve({ fid: null }), 5000); + setTimeout(() => { + if (!resolved) { + resolved = true; + observer.disconnect(); + resolve({ fid: null }); + } + }, 5000); + + document.body.click(); }); }); @@ -169,18 +148,23 @@ test.describe('Performance - Network', () => { await page.goto('/'); const images = await page.locator('img').evaluateAll((imgs) => { + const viewportHeight = window.innerHeight; return imgs.map((img) => ({ src: img.src, naturalWidth: img.naturalWidth, - loading: img.loading + loading: img.loading, + isBelowFold: img.getBoundingClientRect().top > viewportHeight })); }); const imagesWithSrc = images.filter((img) => img.src && img.naturalWidth > 0); expect(imagesWithSrc.length).toBeGreaterThan(0); - const lazyLoadedImages = imagesWithSrc.filter((img) => img.loading === 'lazy'); - expect(lazyLoadedImages.length).toBeGreaterThan(0); + const imagesBelowFold = imagesWithSrc.filter((img) => img.isBelowFold); + if (imagesBelowFold.length > 0) { + const lazyLoadedImages = imagesBelowFold.filter((img) => img.loading === 'lazy'); + expect(lazyLoadedImages.length).toBeGreaterThan(0); + } }); test('should not have excessive requests', async ({ page }) => { diff --git a/src/Web/MeAjudaAi.Web.Provider/e2e/performance.spec.ts b/src/Web/MeAjudaAi.Web.Provider/e2e/performance.spec.ts new file mode 100644 index 000000000..00a980dfb --- /dev/null +++ b/src/Web/MeAjudaAi.Web.Provider/e2e/performance.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from '@playwright/test'; + +const mobileViewport = { width: 375, height: 667 }; + +test.describe('Provider Web App - Mobile Responsiveness', () => { + test('should render correctly on mobile viewport', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/provider/dashboard'); + + await expect(page.locator('nav')).toBeVisible(); + }); + + test('should have touch-friendly elements on mobile', async ({ page }) => { + await page.setViewportSize(mobileViewport); + await page.goto('/provider/dashboard'); + + const actionButtons = page.locator('button'); + const count = await actionButtons.count(); + + for (let i = 0; i < Math.min(count, 5); i++) { + const button = actionButtons.nth(i); + const box = await button.boundingBox(); + if (box) { + expect(box.height).toBeGreaterThanOrEqual(44); + } + } + }); +}); diff --git a/src/Web/libs/e2e-support/base.ts b/src/Web/libs/e2e-support/base.ts index 7403c3101..a5429d8ea 100644 --- a/src/Web/libs/e2e-support/base.ts +++ b/src/Web/libs/e2e-support/base.ts @@ -15,7 +15,13 @@ async function handleLoginRedirect(page: Page): Promise { await page.waitForURL(/.*keycloak.*|.*realms.*\/meajudaai/i, { timeout: 5000 }); } catch (error) { if (error instanceof Error && error.name === 'TimeoutError') { - console.log('OAuth redirect intercepted - running in mock/test mode'); + const logoutButton = page.locator('button:has-text("Sair"), [data-testid="logout-button"]'); + const hasSession = await logoutButton.count() > 0 || (await page.context().cookies()).some(c => c.name.includes('auth')); + if (hasSession) { + console.log('OAuth redirect intercepted - running in mock/test mode'); + return; + } + throw new Error('Login failed: no session detected after redirect timeout'); } else { throw error; } @@ -24,23 +30,46 @@ async function handleLoginRedirect(page: Page): Promise { export async function loginAsAdmin(page: Page): Promise { await page.goto('/admin/login'); - await page.getByRole('button', { name: /entrar com keycloak/i }).click(); + await page.waitForLoadState('domcontentloaded'); + const loginButton = page.getByRole('button', { name: /entrar com keycloak/i }); + await loginButton.waitFor({ state: 'visible', timeout: 10000 }); + await loginButton.click(); await handleLoginRedirect(page); } export async function loginAsProvider(page: Page): Promise { await page.goto('/provider/login'); - await page.getByRole('button', { name: /entrar/i }).click(); + await page.waitForLoadState('domcontentloaded'); + const loginButton = page.getByRole('button', { name: /entrar/i }); + await loginButton.waitFor({ state: 'visible', timeout: 10000 }); + await loginButton.click(); await handleLoginRedirect(page); } export async function loginAsCustomer(page: Page): Promise { await page.goto('/login'); - await page.getByRole('button', { name: /entrar/i }).click(); + await page.waitForLoadState('domcontentloaded'); + const loginButton = page.getByRole('button', { name: /entrar/i }); + await loginButton.waitFor({ state: 'visible', timeout: 10000 }); + await loginButton.click(); await handleLoginRedirect(page); } export async function logout(page: Page): Promise { - await page.getByRole('button', { name: /sair/i }).click(); - await page.waitForURL(/\/(login|admin\/login|provider\/login)/); + const logoutButton = page.getByRole('button', { name: /sair/i }); + const buttonCount = await logoutButton.count(); + + if (buttonCount === 0) { + console.log('User already logged out or logout button not found'); + return; + } + + await logoutButton.waitFor({ state: 'visible', timeout: 5000 }); + await logoutButton.click(); + + try { + await page.waitForURL(/\/(login|admin\/login|provider\/login)/, { timeout: 10000 }); + } catch (error) { + throw new Error(`Logout failed: could not navigate to login page. Error: ${error instanceof Error ? error.message : 'Unknown error'}`); + } } From 5e65c3f9a2e5eb9e520ff1788d11f3fb3764ec76 Mon Sep 17 00:00:00 2001 From: Filipe Frigini Date: Tue, 24 Mar 2026 21:32:11 -0300 Subject: [PATCH 012/142] feat: Introduce an admin sidebar component, add extensive E2E tests for customer, admin, and provider applications, and create new documentation files for architecture, admin portal, and roadmap. --- docs/admin-portal/overview.md | 23 +- docs/architecture.md | 66 +++- docs/roadmap-current.md | 5 +- .../components/layout/sidebar.tsx | 13 +- .../MeAjudaAi.Web.Admin/e2e/dashboard.spec.ts | 183 +++++++++++ ....spec.ts => mobile-responsiveness.spec.ts} | 0 .../e2e/performance.spec.ts | 17 +- .../e2e/profile.spec.ts | 280 ++++++++++++++++ .../e2e/dashboard.spec.ts | 298 ++++++++++++++++++ 9 files changed, 849 insertions(+), 36 deletions(-) create mode 100644 src/Web/MeAjudaAi.Web.Admin/e2e/dashboard.spec.ts rename src/Web/MeAjudaAi.Web.Admin/e2e/{performance.spec.ts => mobile-responsiveness.spec.ts} (100%) create mode 100644 src/Web/MeAjudaAi.Web.Customer/e2e/profile.spec.ts create mode 100644 src/Web/MeAjudaAi.Web.Provider/e2e/dashboard.spec.ts diff --git a/docs/admin-portal/overview.md b/docs/admin-portal/overview.md index 6508d4b10..6f3f4b700 100644 --- a/docs/admin-portal/overview.md +++ b/docs/admin-portal/overview.md @@ -99,17 +99,20 @@ src/Web/MeAjudaAi.Web.Admin/ ### Testes E2E -Localização: `src/Web/MeAjudaAi.Web.Admin/e2e/` +Localização: `tests/MeAjudaAi.Web.Admin.Tests/e2e/` **Estrutura:** ```text -src/Web/MeAjudaAi.Web.Admin/ +tests/MeAjudaAi.Web.Admin.Tests/ └── e2e/ ├── auth.spec.ts - └── providers.spec.ts + ├── providers.spec.ts + ├── configs.spec.ts + ├── dashboard.spec.ts + └── mobile-responsiveness.spec.ts ``` -**Fixtures compartilhadas:** `src/Web/libs/e2e-support/base.ts` +**Fixtures compartilhadas:** `tests/MeAjudaAi.Web.Shared.Tests/base.ts` - `loginAsAdmin(page)` - `loginAsProvider(page)` - `loginAsCustomer(page)` @@ -155,16 +158,20 @@ export function EditButton({ providerId }: { providerId: string }) { 'use client'; import { useRouter } from 'next/navigation'; import { useSession } from 'next-auth/react'; +import { useEffect } from 'react'; function AdminProtected({ children }: { children: React.ReactNode }) { const { data: session, status } = useSession(); const router = useRouter(); + useEffect(() => { + if (status !== 'loading' && !session) { + router.push('/login'); + } + }, [status, session, router]); + if (status === 'loading') return ; - if (!session) { - router.push('/login'); - return null; - } + if (!session) return null; if (session.user.role !== 'admin') return ; return <>{children}; diff --git a/docs/architecture.md b/docs/architecture.md index 8c65ece79..25798f37a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -2826,6 +2826,43 @@ graph TB | **Authentication** | NextAuth.js | 5.x | Keycloak integration | | **Testing** | Playwright | 1.x | E2E tests | +> **Nota**: Esta é a arquitetura atual utilizada em produção. + +--- + +### **Legacy (Blazor)** + +> **Nota**: Os exemplos abaixo são históricos e referência apenas. O projeto foi migrado para React conforme descrito acima. + +O Admin Portal foi originalmente desenvolvido em Blazor WebAssembly com MudBlazor. Abaixo estão exemplos históricos: + +```csharp +// Program.cs - Configuração OIDC (Legacy) +builder.Services.AddOidcAuthentication(options => +{ + options.ProviderOptions.Authority = "https://keycloak.local/realms/meajudaai"; + options.ProviderOptions.ClientId = "admin-portal"; + options.ProviderOptions.ResponseType = "code"; + options.ProviderOptions.Scopes.Add("openid"); + options.ProviderOptions.Scopes.Add("profile"); +}); +``` + +```razor +@* App.razor - Cascading Authentication (Legacy) *@ + + + + + + + + + + + +``` + ### **Zustand Pattern - State Management** **Implementação do Padrão Zustand**: @@ -3132,34 +3169,35 @@ src/Web/MeAjudaAi.Web.Admin/ ├── hooks/ # Custom React hooks │ ├── useProviders.ts │ └── useDocuments.ts -└── e2e/ # E2E tests (co-located) - ├── auth.spec.ts - └── providers.spec.ts ``` ### **Estrutura de Arquivos** ```text -src/Web/ -├── MeAjudaAi.Web.Admin/ # Admin Portal Next.js App -│ └── e2e/ # E2E tests (co-located) +tests/ +├── MeAjudaAi.Web.Admin.Tests/ # Admin Portal E2E tests +│ └── e2e/ │ ├── auth.spec.ts -│ └── providers.spec.ts -├── MeAjudaAi.Web.Customer/ # Customer Web Next.js App +│ ├── providers.spec.ts +│ ├── configs.spec.ts +│ ├── dashboard.spec.ts +│ └── mobile-responsiveness.spec.ts +├── MeAjudaAi.Web.Customer.Tests/ # Customer Web E2E tests │ └── e2e/ │ ├── auth.spec.ts │ ├── search.spec.ts │ ├── onboarding.spec.ts +│ ├── profile.spec.ts │ └── performance.spec.ts -├── MeAjudaAi.Web.Provider/ # Provider Web Next.js App +├── MeAjudaAi.Web.Provider.Tests/ # Provider Web E2E tests │ └── e2e/ │ ├── auth.spec.ts │ ├── onboarding.spec.ts -│ └── profile-mgmt.spec.ts -├── libs/ # Shared libraries -│ ├── e2e-support/ # Shared E2E fixtures -│ └── api-client/ # API client -└── playwright.config.ts # Single Playwright config +│ ├── profile-mgmt.spec.ts +│ ├── dashboard.spec.ts +│ └── performance.spec.ts +└── MeAjudaAi.Web.Shared.Tests/ # Shared test fixtures + └── base.ts # Shared E2E fixtures # Playwright Configuration: # - testDir: './src' (single test directory) diff --git a/docs/roadmap-current.md b/docs/roadmap-current.md index a2a440e43..0647c20be 100644 --- a/docs/roadmap-current.md +++ b/docs/roadmap-current.md @@ -32,11 +32,12 @@ Desenvolver aplicações frontend usando **Blazor WebAssembly** (Admin Portal) e **Stack Completa**: -**Admin Portal** (React - migrado Sprint 8D): +**Admin Portal** (React - migrado na Sprint 8D): - React 19 + TypeScript 5.7+ - Tailwind CSS v4 - Zustand (state management) - React Hook Form + Zod +- NextAuth.js (Keycloak OIDC) **Customer Web App** (novo): - React 19 (Server Components + Client Components) @@ -651,7 +652,7 @@ Durante o processo de atualização automática de dependências pelo Dependabot #### Risk Mitigation Strategy - **Contingency Branching**: If major tasks (Admin Migration, NX Setup) slip, we prioritize essential Player flows (Customer/Provider) and fallback to existing Admin solutions. -- **Sprint 8E (Mobile)**: De-scoped from MVP to Phase 2 to ensure web platform stability. +- **Mobile Apps**: De-scoped from MVP to Phase 2 to ensure web platform stability. - **Buffer**: Sprint 9 is strictly for stability, no new features. - Documentação final para MVP diff --git a/src/Web/MeAjudaAi.Web.Admin/components/layout/sidebar.tsx b/src/Web/MeAjudaAi.Web.Admin/components/layout/sidebar.tsx index ee70bfa6b..970f1e7cd 100644 --- a/src/Web/MeAjudaAi.Web.Admin/components/layout/sidebar.tsx +++ b/src/Web/MeAjudaAi.Web.Admin/components/layout/sidebar.tsx @@ -44,9 +44,10 @@ export function Sidebar() { <> {/* Mobile Hamburger Button */} @@ -54,14 +55,18 @@ export function Sidebar() { {/* Backdrop for mobile */} {isOpen && (
setIsOpen(false)} aria-hidden="true" + data-testid="mobile-menu-backdrop" /> )} -