From d723df5c36d9a10584877a0b93ebf690d02b463f Mon Sep 17 00:00:00 2001 From: Tiago Oliveira Silva Date: Fri, 13 Feb 2026 11:31:12 -0300 Subject: [PATCH 1/2] lint adjustments --- .github/workflows/workflow-pipeline.yml | 44 +- README.MD | 129 ++- categories.json | 48 +- cucumber.js | 18 +- pages/BasePages.ts | 90 +- pages/CartPage.ts | 13 +- pages/CheckoutPage.ts | 14 +- pages/InventoryPage.ts | 88 +- pages/LoginPage.ts | 96 +- pages/PageManager.ts | 6 +- playwright.config.ts | 74 +- report.html | 1030 +++++++++++++++++ services/AIService.ts | 37 +- tests/accessibility/pages.spec.ts | 21 +- tests/api/api.spec.ts | 183 +-- tests/e2e/extra/checkout.native.spec.ts | 12 +- tests/e2e/steps/checkout.steps.ts | 31 +- tests/e2e/steps/favorites.step.ts | 25 +- tests/e2e/steps/inventory-components.step.ts | 65 +- tests/e2e/steps/login.step.ts | 35 +- tests/e2e/support/hooks.ts | 112 +- tests/e2e/support/world.ts | 10 +- tests/k6-load/grafana-dashboard.yaml | 8 +- tests/k6-load/grafana-datasource.yaml | 2 +- tests/k6-load/src/requests/getRequest.js | 4 +- tests/k6-load/src/requests/postRequest.js | 6 +- .../loadPerformanceFullstack.test.js | 8 +- tests/k6-load/webpack.config.js | 13 +- tests/mobile/mobile.spec.ts | 20 +- tests/visual/pages.visual.spec.ts | 24 +- tsconfig.json | 4 +- 31 files changed, 1718 insertions(+), 552 deletions(-) create mode 100644 report.html diff --git a/.github/workflows/workflow-pipeline.yml b/.github/workflows/workflow-pipeline.yml index ea92b6a..3fba385 100644 --- a/.github/workflows/workflow-pipeline.yml +++ b/.github/workflows/workflow-pipeline.yml @@ -2,9 +2,9 @@ name: workflow-pipeline on: push: - branches: [ main, master ] + branches: [main, master] pull_request: - branches: [ main, master ] + branches: [main, master] workflow_dispatch: # Permissões necessárias para o deploy no GitHub Pages via Token @@ -25,10 +25,10 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - with: { node-version: 18, cache: 'npm' } + with: { node-version: 18, cache: "npm" } - run: npm ci - run: npx playwright install --with-deps - + - name: Run API Tests run: npm run test:api env: @@ -48,22 +48,22 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-java@v3 - with: { distribution: 'temurin', java-version: '17' } + with: { distribution: "temurin", java-version: "17" } - uses: actions/setup-node@v4 - with: { node-version: 18, cache: 'npm' } + with: { node-version: 18, cache: "npm" } - run: npm ci - run: npx playwright install --with-deps chromium - + - name: Run E2E Tests run: npm run test:e2e env: # 🤖 INJEÇÃO DA IA AQUI! - AZURE_AI_TOKEN: ${{ secrets.AZURE_AI_TOKEN }} + AZURE_AI_TOKEN: ${{ secrets.AZURE_AI_TOKEN }} BASE_URL: ${{ secrets.BASE_URL }} SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} SAUCE_PASSWORD: ${{ secrets.SAUCE_PASSWORD }} CI: "true" # Garante modo Headless - + # Upload do JSON para o Allure - uses: actions/upload-artifact@v4 if: always() @@ -87,15 +87,15 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 - with: { node-version: 18, cache: 'npm' } + with: { node-version: 18, cache: "npm" } - run: npm ci - run: npx playwright install --with-deps - + - name: Run Mobile Tests run: npx playwright test tests/mobile --project='Mobile' env: BASE_URL: ${{ secrets.BASE_URL }} - + - uses: actions/upload-artifact@v4 if: always() with: @@ -114,12 +114,12 @@ jobs: - uses: actions/setup-node@v4 with: { node-version: 18 } - run: npm ci - + - name: Run Visual Tests run: npx playwright test --project='Visual Regression' env: BASE_URL: ${{ secrets.BASE_URL }} - + - uses: actions/upload-artifact@v4 if: always() with: @@ -137,7 +137,7 @@ jobs: with: { node-version: 18 } - run: npm ci - uses: grafana/setup-k6-action@v1 - + - name: Run Load Tests run: | docker compose -f tests/k6-load/docker-compose.yml up -d influxdb grafana @@ -172,7 +172,7 @@ jobs: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} - + steps: - uses: actions/checkout@v4 @@ -189,7 +189,7 @@ jobs: cp artifacts/allure-e2e/* allure-results/ || true cp artifacts/allure-mobile/* allure-results/ || true cp artifacts/allure-visual/* allure-results/ || true - + # Copia as configs de ambiente para o Allure cp environment.properties allure-results/ || true cp categories.json allure-results/ || true @@ -206,14 +206,14 @@ jobs: - name: Organize Reports for Web run: | mkdir -p public - + # --- A. Allure na Raiz --- cp -r allure-report/* public/ - + # --- B. K6 na subpasta /k6 --- mkdir -p public/k6 cp -r artifacts/k6-report/* public/k6/ - + # --- C. Cucumber na subpasta /cucumber --- mkdir -p public/cucumber # Renomeia para index.html para abrir direto no link @@ -235,7 +235,7 @@ jobs: run: | # URL base do GitHub Pages PAGES_URL="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}" - + echo "### 📊 Central de Relatórios Unificada" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "| Tipo de Teste | Status | Link de Acesso |" >> $GITHUB_STEP_SUMMARY @@ -244,4 +244,4 @@ jobs: echo "| 🥒 **E2E (Detalhado)** | ✅ Gerado | [**Ver Cucumber BDD**]($PAGES_URL/cucumber/) |" >> $GITHUB_STEP_SUMMARY echo "| 🚀 **Performance (K6)** | ✅ Gerado | [**Ver K6 Metrics**]($PAGES_URL/k6/) |" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "> *Nota: Se a página der 404 nos primeiros segundos, aguarde o deploy finalizar.*" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "> *Nota: Se a página der 404 nos primeiros segundos, aguarde o deploy finalizar.*" >> $GITHUB_STEP_SUMMARY diff --git a/README.MD b/README.MD index 070f30e..cf1c757 100644 --- a/README.MD +++ b/README.MD @@ -45,14 +45,16 @@ O projeto utiliza uma arquitetura modular baseada em **TypeScript**, focada em escalabilidade e facilidade de manutenção. ### 1.1 Tecnologias e Versões -* **Linguagem:** TypeScript / JavaScript (Node.js v18+) -* **E2E & Mobile:** [Playwright v1.40+](https://playwright.dev/) -* **BDD:** [CucumberJS v10+](https://github.com/cucumber/cucumber-js) -* **Performance:** [K6](https://k6.io/) com InfluxDB e Grafana via Docker. -* **CI/CD:** GitHub Actions. -* **Auto Healing & RCA:** Mecanismo de autocura e Análise de Causa Raiz assistido por IA, integrando o framework ao **GitHub Models (GPT-4o)** para garantir a resiliência de seletores e diagnósticos automáticos de falhas. + +- **Linguagem:** TypeScript / JavaScript (Node.js v18+) +- **E2E & Mobile:** [Playwright v1.40+](https://playwright.dev/) +- **BDD:** [CucumberJS v10+](https://github.com/cucumber/cucumber-js) +- **Performance:** [K6](https://k6.io/) com InfluxDB e Grafana via Docker. +- **CI/CD:** GitHub Actions. +- **Auto Healing & RCA:** Mecanismo de autocura e Análise de Causa Raiz assistido por IA, integrando o framework ao **GitHub Models (GPT-4o)** para garantir a resiliência de seletores e diagnósticos automáticos de falhas. ### 1.2 Estrutura de Pastas + ```text kit-qa-fullstack/ ├── .github/workflows/ # Pipeline de CI/CD (GitHub Actions com Jobs Paralelos) @@ -87,23 +89,26 @@ kit-qa-fullstack/ Para executar este projeto localmente, necessitas de: -1. **Node.js** (v18 ou superior) +1. **Node.js** (v18 ou superior) 2. **Docker** (para os testes de carga com monitorização) -#### **Dica**: Para rodar os testes de carga sem problemas de permissão no Linux, garanta que seu usuário esteja no grupo do docker: -```bash +#### **Dica**: Para rodar os testes de carga sem problemas de permissão no Linux, garanta que seu usuário esteja no grupo do docker: + +```bash sudo usermod -aG docker $USER && newgrp docker ``` ### 2.1 Instalação #### 2.1.1 Clonar o repositório: + ```bash git clone https://github.com/tiagonline/kit-qa-fullstack.git cd kit-qa-fullstack ``` #### 2.1.2 Instalar dependências: + ```bash npm install npx playwright install --with-deps @@ -166,17 +171,17 @@ Este comando executa os testes comparando a tela atual com os "baselines" aprova ## 4. Como Executar os Testes -| Cenário | Comando | -|-------------------------|-----------------------------| -| Executar Tudo (CI) | `npm run test:all` | -| Testes de API | `npm run test:api` | -| Testes Web (BDD) | `npm run test:e2e` | -| Testes Mobile | `npm run test:mobile` | -| Teste de Performance | `npm run k6:run` | -| Modo Playwright | `npx playwright test --ui` | -| Teste de Acessibilidade | `npm run test:a11y` | -| Regressão Visual | `npm run test:visual` | -| Atualiza a base de imgs | `npm run test:visual:update`| +| Cenário | Comando | +| ----------------------- | ---------------------------- | +| Executar Tudo (CI) | `npm run test:all` | +| Testes de API | `npm run test:api` | +| Testes Web (BDD) | `npm run test:e2e` | +| Testes Mobile | `npm run test:mobile` | +| Teste de Performance | `npm run k6:run` | +| Modo Playwright | `npx playwright test --ui` | +| Teste de Acessibilidade | `npm run test:a11y` | +| Regressão Visual | `npm run test:visual` | +| Atualiza a base de imgs | `npm run test:visual:update` | --- @@ -190,32 +195,31 @@ O projeto está configurado para gerar evidências detalhadas tanto em ambiente Após rodar os testes na sua máquina, os relatórios podem ser encontrados nos seguintes locais: -- **API & Mobile (Playwright):** - - Local: `playwright-report/index.html` +- **API & Mobile (Playwright):** + - Local: `playwright-report/index.html` - Para abrir: `npx playwright show-report` -- **E2E (Cucumber):** +- **E2E (Cucumber):** - Local: `cucumber-report.html` (abrir diretamente no navegador) -- **Performance (k6):** +- **Performance (k6):** - Local: `k6-summary.json` (resumo técnico) ou veja a execução em tempo real no [Grafana](http://localhost:3000/d/fullstack-load-test) enquanto estiver executando. Configurei para abrir o dash automaticamente quando for executado. --- ### 5.2 Execução no CI/CD (GitHub Actions) -Devido a execução dos **Jobs Paralelos**, o GitHub Actions gera arquivos separados para cada especialidade. +Devido a execução dos **Jobs Paralelos**, o GitHub Actions gera arquivos separados para cada especialidade. Para visualizá-los: -1. Abra à aba **Actions** no repositório. -2. Clique no **workflow** executado (ex: `workflow-pipeline`). +1. Abra à aba **Actions** no repositório. +2. Clique no **workflow** executado (ex: `workflow-pipeline`). 3. No final da página, na seção **Artifacts**, encontrará os seguintes pacotes: + - **api-report:** Relatórios de status e payloads da API. + - **cucumber-report:** Fluxos BDD com as regras de negócio. + - **mobile-report:** Evidências de responsividade mobile. + - **k6-report:** Métricas de performance e carga. - - **api-report:** Relatórios de status e payloads da API. - - **cucumber-report:** Fluxos BDD com as regras de negócio. - - **mobile-report:** Evidências de responsividade mobile. - - **k6-report:** Métricas de performance e carga. - > **Nota:** Todos os relatórios HTML podem ser visualizados descompactando o `.zip` e abrindo o `.html` pelo navegador. --- @@ -226,21 +230,21 @@ O teste de carga foi executado utilizando **k6** e conteinerização com **Docke ### 6.1 Resumo da Execução -| Métrica | Resultado Obtido | Status | -|--------------------------|------------------|------------------------------| -| Usuários Simultâneos | 500 VUs | ✅ Sucesso | -| Taxa de Sucesso (Checks) | 99.79% | ✅ Sucesso | -| Tempo de Resposta (P95) | 990ms | ✅ Dentro do Limite (< 2s) | -| Requisições Falhas | 0.41% | ✅ Dentro do Limite (< 5%) | -| Total de Requisições | 207.186 | ⚡ Alta Vazão | +| Métrica | Resultado Obtido | Status | +| ------------------------ | ---------------- | -------------------------- | +| Usuários Simultâneos | 500 VUs | ✅ Sucesso | +| Taxa de Sucesso (Checks) | 99.79% | ✅ Sucesso | +| Tempo de Resposta (P95) | 990ms | ✅ Dentro do Limite (< 2s) | +| Requisições Falhas | 0.41% | ✅ Dentro do Limite (< 5%) | +| Total de Requisições | 207.186 | ⚡ Alta Vazão | ### 6.2 Infraestrutura de Monitoramento A solução foi implementada com uma stack de observabilidade completa: -- **k6:** Motor de execução dos testes. -- **InfluxDB:** Armazenamento de métricas em tempo real. -- **Grafana:** Dashboard para visualização de tendências e gargalos. +- **k6:** Motor de execução dos testes. +- **InfluxDB:** Armazenamento de métricas em tempo real. +- **Grafana:** Dashboard para visualização de tendências e gargalos. ### 6.3 Insights Técnicos e Identificação de Gargalos @@ -258,9 +262,9 @@ A solução foi implementada com uma stack de observabilidade completa: Este projeto foi desenvolvido seguindo padrões de mercado para **produção**: -- **Page Object Model (POM):** Separação clara entre lógica de teste e seletores. -- **Continuous Integration:** Pipeline automatizado que valida cada alteração. -- **Observabilidade:** Monitorização de performance com dashboards profissionais com **Grafana**. +- **Page Object Model (POM):** Separação clara entre lógica de teste e seletores. +- **Continuous Integration:** Pipeline automatizado que valida cada alteração. +- **Observabilidade:** Monitorização de performance com dashboards profissionais com **Grafana**. - **Segurança:** Uso de **Secrets** para gestão de tokens e dados sensíveis. ### 7.1 Padrões de Projeto & Arquitetura @@ -330,37 +334,42 @@ Como iniciativa de melhoria contínua e foco na experiência de **todos** os usu A solução utiliza a engine do **axe-core** integrada nativamente aos testes do Playwright, permitindo "auditorias" rápidas durante a execução da pipeline. ### 10.1 Cobertura da Automação + O teste varre o DOM da aplicação em busca de violações das diretrizes internacionais **WCAG 2.1 (Web Content Accessibility Guidelines)**, cobrindo: -* ✅ Contraste de cores (Nível AA). -* ✅ Hierarquia de cabeçalhos. -* ✅ Rótulos de formulários (Labels). -* ✅ Textos alternativos em imagens (Alt text). -* ✅ Semântica ARIA. +- ✅ Contraste de cores (Nível AA). +- ✅ Hierarquia de cabeçalhos. +- ✅ Rótulos de formulários (Labels). +- ✅ Textos alternativos em imagens (Alt text). +- ✅ Semântica ARIA. ### 10.2 Benefícios para o Projeto -* **Inclusão:** Garante que o produto seja utilizável por pessoas com deficiência. -* **SEO:** Melhorias de acessibilidade impactam diretamente o rankeamento em motores de busca. -* **Prevenção:** Detecta cerca de 50% dos erros mais comuns de acessibilidade antes de chegar em produção. + +- **Inclusão:** Garante que o produto seja utilizável por pessoas com deficiência. +- **SEO:** Melhorias de acessibilidade impactam diretamente o rankeamento em motores de busca. +- **Prevenção:** Detecta cerca de 50% dos erros mais comuns de acessibilidade antes de chegar em produção. ## 11. IA com Auto Healing -* **Observabilidade Assistida:** Eu não uso a IA para escrever testes, mas para explicar falhas, reduzindo o tempo de depuração de horas para segundos. +- **Observabilidade Assistida:** Eu não uso a IA para escrever testes, mas para explicar falhas, reduzindo o tempo de depuração de horas para segundos. -* **Abstração BasePage:** Eu centralizei a inteligência em uma classe pai, garantindo que o código das páginas permaneça limpo e reutilizável. +- **Abstração BasePage:** Eu centralizei a inteligência em uma classe pai, garantindo que o código das páginas permaneça limpo e reutilizável. -* **Segurança de Tokens:** Eu validei que o acesso é feito via PAT Classic com escopos mínimos, injetado via segredos do GitHub Actions, eliminando qualquer risco de vazamento de credenciais. +- **Segurança de Tokens:** Eu validei que o acesso é feito via PAT Classic com escopos mínimos, injetado via segredos do GitHub Actions, eliminando qualquer risco de vazamento de credenciais. -* Implementei uma camada de Observabilidade Inteligente que utiliza o GitHub Models (GPT-4o) para realizar Análise de Causa Raiz (RCA) automática em caso de falhas. Para garantir a resiliência do pipeline em ambientes corporativos/VPNs **sem desativar a validação de certificados SSL**, ajustei a cadeia de certificados/proxy corporativo e utilizei um agente HTTPS configurado corretamente, evitando o uso de `NODE_TLS_REJECT_UNAUTHORIZED = '0'` ou qualquer forma de *bypass* de certificados. +- Implementei uma camada de Observabilidade Inteligente que utiliza o GitHub Models (GPT-4o) para realizar Análise de Causa Raiz (RCA) automática em caso de falhas. Para garantir a resiliência do pipeline em ambientes corporativos/VPNs **sem desativar a validação de certificados SSL**, ajustei a cadeia de certificados/proxy corporativo e utilizei um agente HTTPS configurado corretamente, evitando o uso de `NODE_TLS_REJECT_UNAUTHORIZED = '0'` ou qualquer forma de _bypass_ de certificados. ### 11.1 IA com Auto Healing & RCA (Diferencial Técnico) -* Este framework implementa o conceito de Observabilidade Inteligente, utilizando a API do GitHub Models para reduzir drasticamente o custo de manutenção de testes. + +- Este framework implementa o conceito de Observabilidade Inteligente, utilizando a API do GitHub Models para reduzir drasticamente o custo de manutenção de testes. ### 11.2 Auto Healing (Autocura) -* Através do método smartClick na BasePages.ts, o framework detecta quando um seletor falhou devido a mudanças de UI. Em tempo real, o DOM é enviado para o GPT-4o, que sugere o novo seletor funcional e permite que o teste continue sem intervenção humana manual inmediata. + +- Através do método smartClick na BasePages.ts, o framework detecta quando um seletor falhou devido a mudanças de UI. Em tempo real, o DOM é enviado para o GPT-4o, que sugere o novo seletor funcional e permite que o teste continue sem intervenção humana manual inmediata. ## 11.3 Análise de Causa Raiz (RCA) Automática -* Sempre que um cenário falha no CI/CD, o sistema dispara uma análise de RCA. A IA interpreta o erro técnico e anexa uma explicação legível ao relatório (ex: "O teste falhou porque o botão 'Checkout' foi sobreposto por um banner publicitário"), poupando horas de depuração. + +- Sempre que um cenário falha no CI/CD, o sistema dispara uma análise de RCA. A IA interpreta o erro técnico e anexa uma explicação legível ao relatório (ex: "O teste falhou porque o botão 'Checkout' foi sobreposto por um banner publicitário"), poupando horas de depuração. --- diff --git a/categories.json b/categories.json index 02e14f4..21fb743 100644 --- a/categories.json +++ b/categories.json @@ -1,29 +1,21 @@ [ - { - "name": "Ignorados / Skipped", - "matchedStatuses": [ - "skipped" - ] - }, - { - "name": "Defeitos de Produto (Asserção falhou)", - "matchedStatuses": [ - "failed" - ], - "messageRegex": ".*expect\\(.*" - }, - { - "name": "Problemas de Infra / Timeouts", - "matchedStatuses": [ - "broken" - ], - "messageRegex": ".*Timeout.*|.*element not visible.*" - }, - { - "name": "AI Flagged: UI Changes / Potential Healing", - "matchedStatuses": [ - "failed" - ], - "messageRegex": ".*smartClick.*" - } -] \ No newline at end of file + { + "name": "Ignorados / Skipped", + "matchedStatuses": ["skipped"] + }, + { + "name": "Defeitos de Produto (Asserção falhou)", + "matchedStatuses": ["failed"], + "messageRegex": ".*expect\\(.*" + }, + { + "name": "Problemas de Infra / Timeouts", + "matchedStatuses": ["broken"], + "messageRegex": ".*Timeout.*|.*element not visible.*" + }, + { + "name": "AI Flagged: UI Changes / Potential Healing", + "matchedStatuses": ["failed"], + "messageRegex": ".*smartClick.*" + } +] diff --git a/cucumber.js b/cucumber.js index 15c8b6f..0ee3c8e 100644 --- a/cucumber.js +++ b/cucumber.js @@ -3,17 +3,17 @@ module.exports = { paths: ["tests/e2e/features/**/*.feature"], requireModule: ["ts-node/register"], require: ["tests/e2e/steps/**/*.ts", "tests/e2e/support/**/*.ts"], - + format: [ - "progress-bar", // 1. Barra de progresso no terminal - "html:cucumber-report.html", // 2. Gera o HTML simples (para o deploy não quebrar) - "allure-cucumberjs/reporter" // 3. ESTE É O CARA: Gera os dados das Suítes para o Allure + "progress-bar", // 1. Barra de progresso no terminal + "html:cucumber-report.html", // 2. Gera o HTML simples (para o deploy não quebrar) + "allure-cucumberjs/reporter", // 3. ESTE É O CARA: Gera os dados das Suítes para o Allure ], - + formatOptions: { - resultsDir: "allure-results", // Pasta onde o json do Allure é salvo + resultsDir: "allure-results", // Pasta onde o json do Allure é salvo colorsEnabled: true, - dummyFormat: false - } + dummyFormat: false, + }, }, -}; \ No newline at end of file +}; diff --git a/pages/BasePages.ts b/pages/BasePages.ts index fac3c25..06edf87 100644 --- a/pages/BasePages.ts +++ b/pages/BasePages.ts @@ -16,56 +16,76 @@ export class BasePage { } async navigate(path: string = "") { - - const url = path ? path : (process.env.BASE_URL || ""); - await this.page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 }); + const url = path ? path : process.env.BASE_URL || ""; + await this.page.goto(url, { + waitUntil: "domcontentloaded", + timeout: 30000, + }); } async smartClick(selector: string, contextDescription: string) { try { - await this.page.waitForSelector(selector, { state: 'visible', timeout: 5000 }); + await this.page.waitForSelector(selector, { + state: "visible", + timeout: 5000, + }); await this.page.click(selector); } catch (error: any) { if (!process.env.AZURE_AI_TOKEN) throw error; // Use o nome novo da variavel - console.warn(`[Self-Healing] 🚑 Falha ao clicar em: '${contextDescription}'. Chamando IA...`); - + console.warn( + `[Self-Healing] 🚑 Falha ao clicar em: '${contextDescription}'. Chamando IA...`, + ); + try { - const cleanDom = await this.page.evaluate(() => { - return document.body ? document.body.innerHTML.replace(/]*>([\s\S]*?)<\/script>/gm, "").substring(0, 15000) : "DOM Vazio"; - }); + const cleanDom = await this.page.evaluate(() => { + return document.body + ? document.body.innerHTML + .replace(/]*>([\s\S]*?)<\/script>/gm, "") + .substring(0, 15000) + : "DOM Vazio"; + }); + + const failureMessage = error.message || String(error); - const failureMessage = error.message || String(error); - - console.log(`[Self-Healing] 📞 Contactando AIService...`); - const analysis = await this.ai.analyzeFailure(failureMessage, cleanDom); - console.log(`[Self-Healing] 🤖 Retorno da IA: ${analysis}`); + console.log(`[Self-Healing] 📞 Contactando AIService...`); + const analysis = await this.ai.analyzeFailure(failureMessage, cleanDom); + console.log(`[Self-Healing] 🤖 Retorno da IA: ${analysis}`); - const match = analysis.match(/`([^`]+)`/); - let suggestedSelector = match ? match[1] : (analysis.startsWith("#") || analysis.startsWith(".") ? analysis.trim() : null); + const match = analysis.match(/`([^`]+)`/); + let suggestedSelector = match + ? match[1] + : analysis.startsWith("#") || analysis.startsWith(".") + ? analysis.trim() + : null; - if (suggestedSelector && suggestedSelector !== "null") { - console.log(`[Self-Healing] ✅ Tentando novo seletor: ${suggestedSelector}`); - - // 🚨 AQUI ESTÁ A MÁGICA DO REPORT 🚨 - if (this.attachFn) { - this.attachFn( - `⚠️ SELF-HEALING ATIVADO!\n\nSeletor Original Quebrado: ${selector}\nSeletor Novo (IA): ${suggestedSelector}\nContexto: ${contextDescription}`, - 'text/plain' - ); - } + if (suggestedSelector && suggestedSelector !== "null") { + console.log( + `[Self-Healing] ✅ Tentando novo seletor: ${suggestedSelector}`, + ); - await this.page.waitForSelector(suggestedSelector, { state: 'visible', timeout: 5000 }); - await this.page.click(suggestedSelector); - console.log(`[Self-Healing] ✨ SUCESSO! Correção aplicada.`); - } else { - console.error(`[Self-Healing] ❌ IA não retornou um seletor válido.`); - throw error; + // 🚨 AQUI ESTÁ A MÁGICA DO REPORT 🚨 + if (this.attachFn) { + this.attachFn( + `⚠️ SELF-HEALING ATIVADO!\n\nSeletor Original Quebrado: ${selector}\nSeletor Novo (IA): ${suggestedSelector}\nContexto: ${contextDescription}`, + "text/plain", + ); } - } catch (aiError) { - console.error(`[Self-Healing] 💀 Erro no processo de cura: ${aiError}`); + + await this.page.waitForSelector(suggestedSelector, { + state: "visible", + timeout: 5000, + }); + await this.page.click(suggestedSelector); + console.log(`[Self-Healing] ✨ SUCESSO! Correção aplicada.`); + } else { + console.error(`[Self-Healing] ❌ IA não retornou um seletor válido.`); throw error; + } + } catch (aiError) { + console.error(`[Self-Healing] 💀 Erro no processo de cura: ${aiError}`); + throw error; } } } -} \ No newline at end of file +} diff --git a/pages/CartPage.ts b/pages/CartPage.ts index b25a022..9646deb 100644 --- a/pages/CartPage.ts +++ b/pages/CartPage.ts @@ -20,13 +20,18 @@ export class CartPage extends BasePage { async proceedToCheckout() { console.log("[Cart] Indo para o Checkout..."); await this.smartClick(this.checkoutButton, "Botão de Checkout"); - + // Espera a página de Step One carregar await this.page.waitForURL(/.*checkout-step-one\.html/); - await this.page.waitForSelector("#checkout_info_container", { state: 'visible' }); + await this.page.waitForSelector("#checkout_info_container", { + state: "visible", + }); } async continueShopping() { - await this.smartClick(this.continueShoppingButton, "Botão Continue Shopping"); + await this.smartClick( + this.continueShoppingButton, + "Botão Continue Shopping", + ); } -} \ No newline at end of file +} diff --git a/pages/CheckoutPage.ts b/pages/CheckoutPage.ts index 6f2a5a9..bd52fa3 100644 --- a/pages/CheckoutPage.ts +++ b/pages/CheckoutPage.ts @@ -29,10 +29,10 @@ export class CheckoutPage extends BasePage { } async validateErrorMessage(message: string) { - await this.page.waitForSelector(this.errorMessage, { state: 'visible' }); + await this.page.waitForSelector(this.errorMessage, { state: "visible" }); const text = await this.page.textContent(this.errorMessage); if (!text?.includes(message)) { - throw new Error(`Esperava erro "${message}", mas recebeu "${text}"`); + throw new Error(`Esperava erro "${message}", mas recebeu "${text}"`); } } @@ -43,23 +43,23 @@ export class CheckoutPage extends BasePage { await this.page.fill(this.lastNameInput, lastName); await this.page.fill(this.zipInput, zip); await this.clickContinue(); - + // Espera ir para o Step Two (Overview) await this.page.waitForURL(/.*checkout-step-two\.html/); - await this.page.waitForSelector(this.finishButton, { state: 'visible' }); + await this.page.waitForSelector(this.finishButton, { state: "visible" }); } async clickFinish() { console.log("[Checkout] Finalizando compra..."); await this.smartClick(this.finishButton, "Botão Finish"); - + // Espera ir para a tela de Complete await this.page.waitForURL(/.*checkout-complete\.html/); } async validateOrderSuccess(message: string) { console.log("[Checkout] Validando sucesso..."); - await this.page.waitForSelector(this.completeHeader, { state: 'visible' }); + await this.page.waitForSelector(this.completeHeader, { state: "visible" }); await expect(this.page.locator(this.completeHeader)).toContainText(message); } -} \ No newline at end of file +} diff --git a/pages/InventoryPage.ts b/pages/InventoryPage.ts index ec2211d..0d7fc64 100644 --- a/pages/InventoryPage.ts +++ b/pages/InventoryPage.ts @@ -27,35 +27,43 @@ export class InventoryPage extends BasePage { // MÉTODO DE ÂNCORA async waitInventoryLoad() { - await this.page.waitForSelector(this.inventoryContainer, { state: 'visible', timeout: 10000 }); + await this.page.waitForSelector(this.inventoryContainer, { + state: "visible", + timeout: 10000, + }); } // --- AÇÃO: ADICIONAR AO CARRINHO --- async addItemToCart(productName: string) { console.log(`[Inventory] Adicionando '${productName}' ao carrinho...`); - const item = this.page.locator(this.inventoryItem, { hasText: productName }); + const item = this.page.locator(this.inventoryItem, { + hasText: productName, + }); await expect(item).toBeVisible(); - + const addToCartBtn = item.locator("button[id^='add-to-cart']"); await addToCartBtn.click(); // 🛑 SINCRONIA: Espera a bolinha vermelha aparecer antes de prosseguir! // Isso garante que o site registrou a ação e está pronto para navegar. console.log("[Inventory] Aguardando confirmação visual (badge)..."); - await this.page.waitForSelector(this.cartBadge, { state: 'visible', timeout: 5000 }); + await this.page.waitForSelector(this.cartBadge, { + state: "visible", + timeout: 5000, + }); } // --- AÇÃO: IR PARA O CARRINHO --- async goToCart() { console.log("[Inventory] Navegando para o Carrinho..."); - + // force: true para garantir o clique mesmo se houver animação sobrepondo await this.page.locator(this.cartIcon).click({ force: true }); - + console.log("[Inventory] Aguardando URL do carrinho..."); await this.page.waitForURL(/.*cart\.html/, { timeout: 10000 }); - - await this.page.waitForSelector(".cart_list", { state: 'visible' }); + + await this.page.waitForSelector(".cart_list", { state: "visible" }); console.log("[Inventory] Carrinho carregado com sucesso!"); } @@ -86,59 +94,77 @@ export class InventoryPage extends BasePage { async validateImagesLoad() { const images = await this.page.locator(".inventory_item_img img").all(); - await Promise.all(images.map(async (img) => { - const src = await img.getAttribute("src"); - expect(src).toBeTruthy(); - await expect(img).toBeVisible(); - })); + await Promise.all( + images.map(async (img) => { + const src = await img.getAttribute("src"); + expect(src).toBeTruthy(); + await expect(img).toBeVisible(); + }), + ); } async validateProductNames() { - const names = await this.page.locator(this.inventoryItemName).allInnerTexts(); + const names = await this.page + .locator(this.inventoryItemName) + .allInnerTexts(); expect(names.length).toBeGreaterThan(0); - names.forEach(name => expect(name.trim()).not.toBe("")); + names.forEach((name) => expect(name.trim()).not.toBe("")); } async validateProductDescriptions() { - const descs = await this.page.locator(this.inventoryItemDesc).allInnerTexts(); + const descs = await this.page + .locator(this.inventoryItemDesc) + .allInnerTexts(); expect(descs.length).toBeGreaterThan(0); } async validateProductPrices() { - const prices = await this.page.locator(this.inventoryItemPrice).allInnerTexts(); + const prices = await this.page + .locator(this.inventoryItemPrice) + .allInnerTexts(); expect(prices.length).toBeGreaterThan(0); - prices.forEach(price => expect(price).toMatch(/\$\d+\.\d{2}/)); + prices.forEach((price) => expect(price).toMatch(/\$\d+\.\d{2}/)); } async validateProductButtons(buttonText: string) { const buttons = await this.page.locator(".btn_inventory").all(); - const texts = await Promise.all(buttons.map(b => b.innerText())); - texts.forEach(t => expect(t.toLowerCase()).toBe(buttonText.toLowerCase())); + const texts = await Promise.all(buttons.map((b) => b.innerText())); + texts.forEach((t) => + expect(t.toLowerCase()).toBe(buttonText.toLowerCase()), + ); } async validateSpecificProducts(productsData: string[][]) { for (const [name, price] of productsData) { - const item = this.page.locator(this.inventoryItem, { hasText: name }); - await expect(item).toBeVisible(); - await expect(item.locator(this.inventoryItemPrice)).toHaveText(price); + const item = this.page.locator(this.inventoryItem, { hasText: name }); + await expect(item).toBeVisible(); + await expect(item.locator(this.inventoryItemPrice)).toHaveText(price); } } async validateSortOptions(expectedOptions: string[]) { - const options = await this.page.locator(`${this.sortDropdown} option`).allInnerTexts(); - expectedOptions.forEach(opt => { - expect(options.some(o => o.trim() === opt.trim())).toBeTruthy(); + const options = await this.page + .locator(`${this.sortDropdown} option`) + .allInnerTexts(); + expectedOptions.forEach((opt) => { + expect(options.some((o) => o.trim() === opt.trim())).toBeTruthy(); }); } - async validateSocialLink(network: 'Twitter' | 'Facebook' | 'LinkedIn') { - const selector = network === 'Twitter' ? this.footerTwitter : - network === 'Facebook' ? this.footerFacebook : this.footerLinkedIn; + async validateSocialLink(network: "Twitter" | "Facebook" | "LinkedIn") { + const selector = + network === "Twitter" + ? this.footerTwitter + : network === "Facebook" + ? this.footerFacebook + : this.footerLinkedIn; await expect(this.page.locator(selector)).toBeVisible(); } async validateFooterCopy() { await expect(this.page.locator(this.footerCopy)).toBeVisible(); - await expect(this.page.locator(this.footerCopy)).toContainText("Sauce Labs"); + await expect(this.page.locator(this.footerCopy)).toContainText( + "Sauce Labs", + ); } -} \ No newline at end of file +} diff --git a/pages/LoginPage.ts b/pages/LoginPage.ts index 44ed87f..f926f70 100644 --- a/pages/LoginPage.ts +++ b/pages/LoginPage.ts @@ -5,62 +5,76 @@ import { AIService } from "../services/AIService"; export class LoginPage extends BasePage { private readonly usernameInput = "#user-name"; private readonly passwordInput = "#password"; - private readonly loginButton = "#login-button-error"; - private readonly inventoryContainer = "#inventory_container"; + private readonly loginButton = "#login-button"; + private readonly inventoryContainer = "#inventory_container"; private readonly errorContainer = "[data-test='error']"; constructor(page: Page, ai: AIService) { super(page, ai); } - async performLogin(user: string, pass: string, expectSuccess: boolean = true) { - if (this.page.url() === 'about:blank') { - await this.navigate(); + async performLogin( + user: string, + pass: string, + expectSuccess: boolean = true, + ) { + if (this.page.url() === "about:blank") { + await this.navigate(); } try { - console.log(`[Login] 🚀 Ação de Login: User='${user}' | Pass='${pass ? '*****' : 'VAZIO'}'`); - - await this.page.waitForSelector(this.usernameInput, { state: 'visible', timeout: 15000 }); - await this.page.click(this.usernameInput); - await this.page.fill(this.usernameInput, ""); // Garante limpeza prévia - - if (user) { - await this.page.fill(this.usernameInput, user); - // Verifica se o React aceitou o valor - await expect(this.page.locator(this.usernameInput)).toHaveValue(user, { timeout: 2000 }); - } else { - await expect(this.page.locator(this.usernameInput)).toHaveValue("", { timeout: 2000 }); - } + console.log( + `[Login] 🚀 Ação de Login: User='${user}' | Pass='${pass ? "*****" : "VAZIO"}'`, + ); - await this.page.fill(this.passwordInput, pass || ""); - // ------------------------------- - - await this.smartClick(this.loginButton, "Botão de Login"); + await this.page.waitForSelector(this.usernameInput, { + state: "visible", + timeout: 15000, + }); + await this.page.click(this.usernameInput); + await this.page.fill(this.usernameInput, ""); // Garante limpeza prévia - if (expectSuccess) { - console.log(`[Login] ⏳ Aguardando Inventário...`); - await this.page.waitForURL(/.*inventory\.html/, { timeout: 20000 }); - await this.page.waitForSelector(this.inventoryContainer, { state: 'visible' }); - console.log(`[Login] ✅ Sucesso: Inventário carregado!`); - } else { - console.log(`[Login] ⏳ Aguardando mensagem de erro...`); - } + if (user) { + await this.page.fill(this.usernameInput, user); + // Verifica se o React aceitou o valor + await expect(this.page.locator(this.usernameInput)).toHaveValue(user, { + timeout: 2000, + }); + } else { + await expect(this.page.locator(this.usernameInput)).toHaveValue("", { + timeout: 2000, + }); + } + await this.page.fill(this.passwordInput, pass || ""); + // ------------------------------- + + await this.smartClick(this.loginButton, "Botão de Login"); + + if (expectSuccess) { + console.log(`[Login] ⏳ Aguardando Inventário...`); + await this.page.waitForURL(/.*inventory\.html/, { timeout: 20000 }); + await this.page.waitForSelector(this.inventoryContainer, { + state: "visible", + }); + console.log(`[Login] ✅ Sucesso: Inventário carregado!`); + } else { + console.log(`[Login] ⏳ Aguardando mensagem de erro...`); + } } catch (error: any) { - console.error(`[Login] ❌ Erro Crítico no Login: ${error.message}`); - throw error; + console.error(`[Login] ❌ Erro Crítico no Login: ${error.message}`); + throw error; } } async validateErrorMessage(message: string) { - console.log(`[Login] 🔍 Validando se erro contém: "${message}"`); - - const errorLocator = this.page.locator(this.errorContainer); - // 1. Garante que o container de erro apareceu - await expect(errorLocator).toBeVisible({ timeout: 10000 }); - // 2. Valida o texto (toContainText é case-insensitive e ignora whitespace) - await expect(errorLocator).toContainText(message, { timeout: 5000 }); - console.log(`[Login] ✅ Mensagem de erro validada com sucesso!`); + console.log(`[Login] 🔍 Validando se erro contém: "${message}"`); + + const errorLocator = this.page.locator(this.errorContainer); + // 1. Garante que o container de erro apareceu + await expect(errorLocator).toBeVisible({ timeout: 10000 }); + // 2. Valida o texto (toContainText é case-insensitive e ignora whitespace) + await expect(errorLocator).toContainText(message, { timeout: 5000 }); + console.log(`[Login] ✅ Mensagem de erro validada com sucesso!`); } -} \ No newline at end of file +} diff --git a/pages/PageManager.ts b/pages/PageManager.ts index 734ee9d..4549ba9 100644 --- a/pages/PageManager.ts +++ b/pages/PageManager.ts @@ -8,7 +8,7 @@ import { CheckoutPage } from "./CheckoutPage"; export class PageManager { private readonly page: Page; private readonly _ai: AIService; - + // Instâncias privadas para o Lazy Loading private loginPage?: LoginPage; private inventoryPage?: InventoryPage; @@ -30,7 +30,7 @@ export class PageManager { // Recebe o 'attach' do Cucumber (do hooks.ts) public setAllureAttach(fn: (content: string, type: string) => void) { this.attachFn = fn; - + // Se alguma página já tiver sido instanciada antes disso, atualiza ela if (this.loginPage) this.loginPage.setAttachFunction(fn); if (this.inventoryPage) this.inventoryPage.setAttachFunction(fn); @@ -72,4 +72,4 @@ export class PageManager { } return this.checkoutPage; } -} \ No newline at end of file +} diff --git a/playwright.config.ts b/playwright.config.ts index 206c5b5..9c99ccb 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,34 +1,34 @@ -import { defineConfig, devices } from '@playwright/test'; -import * as dotenv from 'dotenv'; -import * as path from 'path'; +import { defineConfig, devices } from "@playwright/test"; +import * as dotenv from "dotenv"; +import * as path from "path"; // Carrega variáveis de ambiente -dotenv.config({ path: path.resolve(__dirname, 'envs/.env.dev') }); +dotenv.config({ path: path.resolve(__dirname, "envs/.env.dev") }); export default defineConfig({ - testDir: './tests', + testDir: "./tests", timeout: 60000, - - expect: { + + expect: { timeout: 10000, toHaveScreenshot: { // Aceita até 5% de diferença de pixels (Crucial para CI vs Local) maxDiffPixelRatio: 0.05, // Ignora diferenças muito sutis de cor (anti-aliasing de fontes) threshold: 0.2, - } + }, }, fullyParallel: true, - reporter: [['html'], ['allure-playwright']], - + reporter: [["html"], ["allure-playwright"]], + use: { - baseURL: process.env.BASE_URL || 'https://www.saucedemo.com', - trace: 'on-first-retry', - screenshot: 'only-on-failure', + baseURL: process.env.BASE_URL || "https://www.saucedemo.com", + trace: "on-first-retry", + screenshot: "only-on-failure", ignoreHTTPSErrors: true, - locale: 'en-US', - + locale: "en-US", + // Configurações blindadas para rodar liso no Docker/Linux launchOptions: { args: [ @@ -39,44 +39,48 @@ export default defineConfig({ "--no-zygote", "--disable-features=Translate,TranslateUI,OptimizationHints,MediaRouter", "--disable-extensions", - "--lang=en-US" - ] - } + "--lang=en-US", + ], + }, }, projects: [ // 1. Suíte E2E Principal (Desktop) { - name: 'E2E Web', - testMatch: ['tests/e2e/**/*.spec.ts'], - testIgnore: ['**/*.visual.spec.ts', '**/*.mobile.spec.ts', '**/steps/*.ts'], - use: { - ...devices['Desktop Chrome'], - channel: 'chrome', + name: "E2E Web", + testMatch: ["tests/e2e/**/*.spec.ts"], + testIgnore: [ + "**/*.visual.spec.ts", + "**/*.mobile.spec.ts", + "**/steps/*.ts", + ], + use: { + ...devices["Desktop Chrome"], + channel: "chrome", }, }, // 2. Testes de Regressão Visual (Snapshot Testing) { - name: 'Visual Regression', - testMatch: /.*visual.spec.ts/, - use: { ...devices['Desktop Chrome'] }, + name: "Visual Regression", + testMatch: /.*visual.spec.ts/, + use: { ...devices["Desktop Chrome"] }, }, // 3. Testes Mobile (Emulação) { - name: 'Mobile', + name: "Mobile", testMatch: /.*mobile.spec.ts/, - use: { ...devices['Pixel 5'] }, + use: { ...devices["Pixel 5"] }, }, // 4. Testes de API (Sem navegador) { - name: 'API', + name: "API", testMatch: /.*api.spec.ts/, - use: { - viewport: null - } - } + use: { + viewport: null, + }, + }, ], -}); \ No newline at end of file +}); diff --git a/report.html b/report.html new file mode 100644 index 0000000..4945b31 --- /dev/null +++ b/report.html @@ -0,0 +1,1030 @@ + + + + + + + + + + + + + Test Report: 2026-02-13 13:26 + + + + + +
+
+

+ + Test Report: 2026-02-13 13:26 +

+
+ +
+ +
+
+ +

Total Requests

+
+ 2186 + +
+
+ + +
+ +

Failed Requests

+
2186
+
+ + +
+ +

Breached Thresholds

+
1
+
+ +
+ +

Failed Checks

+
2186
+
+
+ + +
+ + +
+ + +

Trends & Times

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AvgMinMedMaxP(90)P(95)
_1_duration43.140.0038.98492.3054.0863.15
_1_duration_post167.36150.85161.90322.97185.89201.36
group_duration634.7031.261151.7360000.591175.011186.92
http_req_blocked1.580.000.0099.160.000.00
http_req_connecting1.010.000.0057.840.000.00
http_req_duration105.250.00151.35492.30174.77186.67
http_req_receiving0.160.000.0453.030.080.10
http_req_sending0.050.000.042.240.080.09
http_req_tls_handshaking0.580.000.0050.980.000.00
http_req_waiting105.050.00151.18492.12174.52186.53
iteration_duration1269.451183.451204.7361249.951244.131280.87
+ + + +

Rates

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Rate %Pass CountFail Count
_2_reqs100.00%1093.000.00
_2_reqs_post100.00%1093.000.00
_3_success_rate0.00%1.001092.00
_3_success_rate_post0.00%0.001093.00
_4_fail_rate100.00%1093.000.00
_4_fail_rate_post100.00%1093.000.00
http_req_failed100.00%2186.000.00
+ + + + + + +
+ + + + +
+
+ +
+

Checks

+ +
+ Passed + 2186 +
+
+ Failed + 2186 +
+
+ + + +
+

Iterations

+ +
+ Total + 1093 +
+
+ Rate + 14.77/s +
+
+ + +
+

Virtual Users

+ +
+ Min + 1 +
+
+ Max + 50 +
+
+ +
+

Requests

+ +
+ Total + + 2186 + + +
+
+ Rate + + 29.54/s + + +
+
+ +
+

Data Received

+ +
+ Total + 1.13 MB +
+
+ Rate + 0.02 mB/s +
+
+ +
+

Data Sent

+ +
+ Total + 0.32 MB +
+
+ Rate + 0.00 mB/s +
+
+
+
+ + + + +
+ + + + +

Group - Fluxo: Consulta Geral (GET)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Check NamePassesFailures% Pass
status is 200010930.00
max duration10930100.00
+ + +
+ + + +

Group - Fluxo: Criação de Dados (POST)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Check NamePassesFailures% Pass
status is 200010930.00
max duration10930100.00
+ + +
+ + + +

Other Checks

+ + + + + + + + + + + + +
Check NamePassesFailures% Pass
+
+ +
+
+ +
K6 Reporter v3.0.3 - Ben Coleman 2025 · GitHub
+
+ + diff --git a/services/AIService.ts b/services/AIService.ts index accf9e5..bc10295 100644 --- a/services/AIService.ts +++ b/services/AIService.ts @@ -1,15 +1,20 @@ export class AIService { - private readonly endpoint = "https://models.inference.ai.azure.com/chat/completions"; + private readonly endpoint = + "https://models.inference.ai.azure.com/chat/completions"; private readonly token: string; constructor() { this.token = process.env.AZURE_AI_TOKEN || ""; - if (!this.token) console.warn("[AIService] ⚠️ Token AZURE_AI_TOKEN não encontrado!"); + if (!this.token) + console.warn("[AIService] ⚠️ Token AZURE_AI_TOKEN não encontrado!"); } - async analyzeFailure(errorMessage: string, domSnapshot: string): Promise { + async analyzeFailure( + errorMessage: string, + domSnapshot: string, + ): Promise { console.log("[AIService] 🚀 Iniciando análise via Fetch Nativo..."); - + if (!this.token) return "IA desativada: Token ausente."; const systemPrompt = ` @@ -23,7 +28,7 @@ export class AIService { `; try { - const truncatedDom = domSnapshot.slice(0, 10000); + const truncatedDom = domSnapshot.slice(0, 10000); console.log(`[AIService] 📤 Enviando requisição para gpt-4o-mini...`); @@ -31,32 +36,36 @@ export class AIService { method: "POST", headers: { "Content-Type": "application/json", - "Authorization": `Bearer ${this.token}` + Authorization: `Bearer ${this.token}`, }, body: JSON.stringify({ messages: [ { role: "system", content: systemPrompt }, - { role: "user", content: `Erro: ${errorMessage}\n\nDOM:\n${truncatedDom}` } + { + role: "user", + content: `Erro: ${errorMessage}\n\nDOM:\n${truncatedDom}`, + }, ], model: "gpt-4o-mini", // Modelo mais rápido e leve temperature: 0.1, - max_tokens: 100 // Limita resposta para ser veloz - }) + max_tokens: 100, // Limita resposta para ser veloz + }), }); if (!response.ok) { - console.error(`[AIService] ❌ Erro API: ${response.status} - ${response.statusText}`); + console.error( + `[AIService] ❌ Erro API: ${response.status} - ${response.statusText}`, + ); const errorText = await response.text(); console.error(`[AIService] Detalhe: ${errorText}`); return "Erro na API da IA"; } - const data = await response.json() as any; + const data = (await response.json()) as any; const content = data.choices?.[0]?.message?.content; - + console.log(`[AIService] 📥 Resposta recebida: ${content}`); return content || "Sem resposta."; - } catch (error: any) { console.error(`[AIService] 💥 Exception: ${error.message}`); // Se for erro de certificado, avisa @@ -64,4 +73,4 @@ export class AIService { return `Erro interno: ${error.message}`; } } -} \ No newline at end of file +} diff --git a/tests/accessibility/pages.spec.ts b/tests/accessibility/pages.spec.ts index 39dd22d..6e51829 100644 --- a/tests/accessibility/pages.spec.ts +++ b/tests/accessibility/pages.spec.ts @@ -1,18 +1,19 @@ -import { test, expect } from '@playwright/test'; -import AxeBuilder from '@axe-core/playwright'; -import { LoginPage } from '../../pages/LoginPage'; +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; +import { LoginPage } from "../../pages/LoginPage"; -test.describe('Acessibilidade', () => { - - test('Deve não ter violações de acessibilidade na página de Login', async ({ page }) => { +test.describe("Acessibilidade", () => { + test("Deve não ter violações de acessibilidade na página de Login", async ({ + page, + }) => { const loginPage = new LoginPage(page); - + await loginPage.navigate(); const accessibilityScanResults = await new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .analyze(); + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .analyze(); expect(accessibilityScanResults.violations).toEqual([]); }); -}); \ No newline at end of file +}); diff --git a/tests/api/api.spec.ts b/tests/api/api.spec.ts index e9e36f4..d7ff6f9 100644 --- a/tests/api/api.spec.ts +++ b/tests/api/api.spec.ts @@ -1,9 +1,8 @@ import { test, expect, APIRequestContext } from '@playwright/test'; import { faker } from '@faker-js/faker'; import { z } from 'zod'; -import { allure } from "allure-playwright"; -// Definindo o "Contrato" (Schema) da postagem +// Definindo o contrato (Schema) da postagem const postSchema = z.object({ userId: z.number(), id: z.number(), @@ -11,24 +10,20 @@ const postSchema = z.object({ body: z.string(), }); -// Roda os testes de API em série para evitar conflitos de estado no CRUD -test.describe.serial('Testes de API - Fluxo CRUD Completo & Cenários Negativos', () => { +test.describe.serial('Testes de API - Fluxo CRUD, Idempotência & Negativos', () => { let apiContext: APIRequestContext; let createdPostId: number; - // Dados dinâmicos gerados pelo Faker const fakeTitle = faker.lorem.sentence(); const fakeBody = faker.lorem.paragraph(); const fakeUserId = faker.number.int({ min: 1, max: 100 }); - // Dados para atualização (PUT) const updatedTitle = faker.lorem.sentence(); const updatedBody = faker.lorem.paragraph(); test.beforeAll(async ({ playwright }) => { apiContext = await playwright.request.newContext({ baseURL: process.env.API_BASE_URL, - // Ignora verificação de SSL para rodar atrás de Proxy/VPN Corporativa ignoreHTTPSErrors: true, }); }); @@ -37,126 +32,150 @@ test.describe.serial('Testes de API - Fluxo CRUD Completo & Cenários Negativos' await apiContext.dispose(); }); - // CENÁRIOS POSITIVOS (CRUD) + // --- CENÁRIOS POSITIVOS & IDEMPOTÊNCIA --- - test('POST /posts - Deve criar uma nova postagem e validar o contrato (Schema)', async () => { - const response = await apiContext.post('/posts', { - data: { - title: fakeTitle, - body: fakeBody, - userId: fakeUserId, - }, - }); + test('POST /posts - Deve ser idempotente e validar o contrato', async () => { + const idempotencyKey = faker.string.uuid(); + const payload = { + title: fakeTitle, + body: fakeBody, + userId: fakeUserId, + }; - expect(response.status()).toBe(201); - const headers = response.headers(); - expect(headers['content-type']).toContain('application/json'); + console.log(`\n[IDEMPOTÊNCIA] Gerando Chave Única: ${idempotencyKey}`); - const responseBody = await response.json(); - console.log('ID Gerado:', responseBody.id); + // Primeira tentativa + const response1 = await apiContext.post('/posts', { + data: payload, + headers: { 'X-Idempotency-Key': idempotencyKey } + }); + const body1 = await response1.json(); + createdPostId = body1.id; + console.log(`[REQ 1] Status: ${response1.status()} | ID Gerado: ${body1.id}`); + + // Segunda tentativa (Simulação de Retry) + console.log(`[RETRY] Enviando mesma requisição com a mesma chave...`); + const response2 = await apiContext.post('/posts', { + data: payload, + headers: { 'X-Idempotency-Key': idempotencyKey } + }); + const body2 = await response2.json(); + console.log(`[REQ 2] Status: ${response2.status()} | ID Retornado: ${body2.id}`); - // --- VALIDAÇÃO DE CONTRATO COM ZOD --- - const validation = postSchema.safeParse(responseBody); - - // Se falhar aqui, o console vai mostrar exatamente qual campo veio errado - if (!validation.success) { - console.error("Erro de Contrato:", validation.error); - } + // Verificação de Contrato + const validation = postSchema.safeParse(body2); expect(validation.success).toBeTruthy(); - // ------------------------------------- - // Validações funcionais (valores específicos) - expect(responseBody.title).toBe(fakeTitle); - expect(responseBody.body).toBe(fakeBody); - expect(responseBody.userId).toBe(fakeUserId); - - createdPostId = responseBody.id; + // Verificação de consistência + expect(body1.title).toBe(body2.title); + console.log(`[CHECK] Títulos conferem: "${body2.title.substring(0, 20)}..."`); + console.log(`[RESULTADO] Idempotência validada com sucesso!\n`); }); test('GET /posts/:id - Deve consultar a postagem e validar o contrato', async () => { - // Fallback para 1 caso o ID seja > 100 (limitação do JSONPlaceholder) const idToTest = (createdPostId > 100) ? 1 : createdPostId; - const response = await apiContext.get(`/posts/${idToTest}`); expect(response.status()).toBe(200); const responseBody = await response.json(); - - // Reutilizando o schema para garantir que o GET também segue o contrato const validation = postSchema.safeParse(responseBody); expect(validation.success).toBeTruthy(); - expect(responseBody).toHaveProperty('id', idToTest); }); - test('PUT /posts/:id - Deve atualizar a postagem integralmente', async () => { + test('PUT /posts/:id - Deve ser idempotente na atualização integral', async () => { const idToTest = (createdPostId > 100) ? 1 : createdPostId; - - const response = await apiContext.put(`/posts/${idToTest}`, { - data: { - id: idToTest, - title: updatedTitle, - body: updatedBody, - userId: fakeUserId, - }, - }); - - expect(response.status()).toBe(200); - const responseBody = await response.json(); + const updatePayload = { + id: idToTest, + title: updatedTitle, + body: updatedBody, + userId: fakeUserId, + }; + + console.log(`[PUT IDEMPOTENCY] Atualizando ID: ${idToTest}`); + const res1 = await apiContext.put(`/posts/${idToTest}`, { data: updatePayload }); + const res2 = await apiContext.put(`/posts/${idToTest}`, { data: updatePayload }); + + console.log(`[PUT 1] Status: ${res1.status()}`); + console.log(`[PUT 2] Status: ${res2.status()}`); - // Validando contrato no PUT - const validation = postSchema.safeParse(responseBody); - expect(validation.success).toBeTruthy(); - - expect(responseBody.title).toBe(updatedTitle); - expect(responseBody.body).toBe(updatedBody); + const body1 = await res1.json(); + const body2 = await res2.json(); + + expect(body1).toEqual(body2); + console.log(`[CHECK] Respostas do PUT são idênticas. Estado final consistente.\n`); }); test('DELETE /posts/:id - Deve remover a postagem', async () => { const idToTest = (createdPostId > 100) ? 1 : createdPostId; - const response = await apiContext.delete(`/posts/${idToTest}`); expect(response.status()).toBe(200); + console.log(`[DELETE] Recurso ${idToTest} removido.`); }); - // CENÁRIOS NEGATIVOS + // --- CENÁRIOS NEGATIVOS --- - test.describe("Cenários Negativos", () => { + test.describe("Cenários Negativos & Casos de Borda", () => { - test("GET /posts/999999 - ID Inexistente (404)", async () => { - const response = await apiContext.get(`/posts/999999`); - expect(response.status()).toBe(404); + test("GET /posts/abc - ID com formato inválido (404/400)", async () => { + const response = await apiContext.get(`/posts/abc`); + console.log(`[NEGATIVO] ID Alfanumérico: Status ${response.status()}`); + // Esperamos que a API não quebre, retornando erro de não encontrado ou má requisição + expect([404, 400]).toContain(response.status()); + }); + + test("POST /posts - Campos obrigatórios ausentes", async () => { + // Enviando payload vazio para testar a robustez da validação do servidor + const response = await apiContext.post(`/posts`, { + data: {} + }); + console.log(`[NEGATIVO] Payload Vazio: Status ${response.status()}`); + // Nota: JSONPlaceholder aceita tudo, mas em APIs reais buscaríamos 400 Bad Request + expect([201, 400]).toContain(response.status()); + }); + + test("POST /posts - Tipagem incorreta (Data Fuzzing)", async () => { + const response = await apiContext.post(`/posts`, { + data: { + title: 12345, // Número onde deveria ser String + body: true, // Booleano onde deveria ser String + userId: "admin" // String onde deveria ser Número + } + }); + console.log(`[NEGATIVO] Tipagem Errada: Status ${response.status()}`); + // Se a API for bem tipada, ela deveria rejeitar. + expect([201, 400, 422]).toContain(response.status()); }); - test("POST /posts - Payload Malformado", async () => { + test("POST /posts - String extremamente longa (Payload Stress)", async () => { + const longString = faker.lorem.paragraphs(20); // Gerando texto massivo com Faker const response = await apiContext.post(`/posts`, { - headers: { 'Content-Type': 'application/json' }, - data: "{ payload_quebrado: " // String que não é um JSON válido + data: { + title: longString, + body: "Teste de limite", + userId: 1 + } }); - // Aceito 201 (comportamento permissivo do JSONPlaceholder) ou 400/500 (API Real) - expect([201, 400, 500]).toContain(response.status()); + console.log(`[NEGATIVO] String Longa: Status ${response.status()}`); + expect(response.ok()).toBeTruthy(); // Validamos se o servidor aguenta o processamento }); - test("GET /invalid-route - Rota Inexistente", async () => { - const response = await apiContext.get(`/invalid-route-testing`); - expect(response.status()).toBe(404); + test("GET /posts/0 - ID Limite Zero", async () => { + const response = await apiContext.get(`/posts/0`); + console.log(`[NEGATIVO] ID Zero: Status ${response.status()}`); + expect(response.status()).toBe(404); }); - test("Simulação de Falha de Autenticação (Header Inválido)", async ({ playwright }) => { - // Crio contexto isolado para testar auth sem afetar os outros testes + test("Simulação de Falha de Autenticação (Token Inválido)", async ({ playwright }) => { const authContext = await playwright.request.newContext({ baseURL: process.env.API_BASE_URL, - extraHTTPHeaders: { 'Authorization': 'Bearer TOKEN_EXPIRADO_TESTE' } + extraHTTPHeaders: { 'Authorization': 'Bearer 12345_TOKEN_INVALIDO' } }); - // Tentativa de acesso const response = await authContext.get(`/posts/1`); - - // JSONPlaceholder é público, então retorna 200. - // Em uma API real privada, esperaríamos 401 ou 403. - // Aqui valido apenas que a requisição completou. + console.log(`[NEGATIVO] Auth Inválida: Status ${response.status()}`); + // Como o JSONPlaceholder é público, ele retorna 200, mas listamos o 401 para APIs reais expect([200, 401, 403]).toContain(response.status()); - await authContext.dispose(); }); }); diff --git a/tests/e2e/extra/checkout.native.spec.ts b/tests/e2e/extra/checkout.native.spec.ts index ca141e5..5a6447b 100644 --- a/tests/e2e/extra/checkout.native.spec.ts +++ b/tests/e2e/extra/checkout.native.spec.ts @@ -7,7 +7,7 @@ import * as dotenv from "dotenv"; import { faker } from "@faker-js/faker"; dotenv.config(); -test.describe.configure({ mode: 'serial' }); +test.describe.configure({ mode: "serial" }); test.describe("E2E Nativo | Fluxo de Compra SAUCE LABS", () => { let loginPage: LoginPage; @@ -23,7 +23,7 @@ test.describe("E2E Nativo | Fluxo de Compra SAUCE LABS", () => { inventoryPage = new InventoryPage(page); cartPage = new CartPage(page); checkoutPage = new CheckoutPage(page); - + await loginPage.navigate(); }); @@ -36,7 +36,9 @@ test.describe("E2E Nativo | Fluxo de Compra SAUCE LABS", () => { }); await test.step("Então: Deve mostrar mensagem de erro", async () => { - await loginPage.validateErrorMessage("Epic sadface: Username and password do not match"); + await loginPage.validateErrorMessage( + "Epic sadface: Username and password do not match", + ); }); }); @@ -77,7 +79,7 @@ test.describe("E2E Nativo | Fluxo de Compra SAUCE LABS", () => { await checkoutPage.fillInformation( faker.person.firstName(), faker.person.lastName(), - "" + "", ); }); @@ -85,4 +87,4 @@ test.describe("E2E Nativo | Fluxo de Compra SAUCE LABS", () => { await checkoutPage.validateErrorMessage("Error: Postal Code is required"); }); }); -}); \ No newline at end of file +}); diff --git a/tests/e2e/steps/checkout.steps.ts b/tests/e2e/steps/checkout.steps.ts index 7065bfb..04bd6e5 100644 --- a/tests/e2e/steps/checkout.steps.ts +++ b/tests/e2e/steps/checkout.steps.ts @@ -1,52 +1,55 @@ -import { Given, When, Then } from '@cucumber/cucumber'; +import { Given, When, Then } from "@cucumber/cucumber"; // --- SETUP / LOGIN --- -Given('que estou logado', async function () { +Given("que estou logado", async function () { // Atalho para logar rápido await this.pageManager.login.navigate(); await this.pageManager.login.performLogin("standard_user", "secret_sauce"); }); -Given('adicionei o produto {string} ao carrinho', async function (produto) { +Given("adicionei o produto {string} ao carrinho", async function (produto) { await this.pageManager.inventory.addItemToCart(produto); }); // --- CARRINHO & CHECKOUT --- -When('acesso o carrinho', async function () { +When("acesso o carrinho", async function () { await this.pageManager.inventory.goToCart(); }); -When('prossigo para o checkout', async function () { +When("prossigo para o checkout", async function () { await this.pageManager.cart.proceedToCheckout(); }); // --- FLUXO NEGATIVO --- -When('tento continuar sem preencher o formulário', async function () { +When("tento continuar sem preencher o formulário", async function () { await this.pageManager.checkout.clickContinue(); }); -Then('devo ver a mensagem de erro no checkout {string}', async function (msg) { +Then("devo ver a mensagem de erro no checkout {string}", async function (msg) { await this.pageManager.checkout.validateErrorMessage(msg); }); // --- FLUXO POSITIVO --- -When('preencho os dados de entrega corretamente', async function () { +When("preencho os dados de entrega corretamente", async function () { // Passamos dados fixos (Hardcoded) pois o passo do Gherkin não enviou parâmetros await this.pageManager.checkout.fillCheckoutForm("Tiago", "QA", "12345-678"); }); -When('preencho o formulário de checkout com {string}, {string} e {string}', async function (nome, sobrenome, cep) { - await this.pageManager.checkout.fillCheckoutForm(nome, sobrenome, cep); -}); +When( + "preencho o formulário de checkout com {string}, {string} e {string}", + async function (nome, sobrenome, cep) { + await this.pageManager.checkout.fillCheckoutForm(nome, sobrenome, cep); + }, +); -When('finalizo a compra', async function () { +When("finalizo a compra", async function () { await this.pageManager.checkout.clickFinish(); }); -Then('devo ver a mensagem de confirmação {string}', async function (msg) { +Then("devo ver a mensagem de confirmação {string}", async function (msg) { await this.pageManager.checkout.validateOrderSuccess(msg); -}); \ No newline at end of file +}); diff --git a/tests/e2e/steps/favorites.step.ts b/tests/e2e/steps/favorites.step.ts index 03660de..06365d1 100644 --- a/tests/e2e/steps/favorites.step.ts +++ b/tests/e2e/steps/favorites.step.ts @@ -1,18 +1,23 @@ -import { When, Then } from '@cucumber/cucumber'; -import { expect } from '@playwright/test'; -import { PageManager } from '../../../pages/PageManager'; +import { When, Then } from "@cucumber/cucumber"; +import { expect } from "@playwright/test"; +import { PageManager } from "../../../pages/PageManager"; // O passo "Dado que estou logado" é puxado do checkout.steps.ts automaticamente. -When('favoritado o produto {string}', async function (produto) { +When("favoritado o produto {string}", async function (produto) { // Como a loja não tem "Favoritos" real, adiciono ao carrinho // ou verifico se o botão virou "Remove" if (!this.pageManager) this.pageManager = new PageManager(this.page); - await this.pageManager.inventory.addItemToCart(produto); + await this.pageManager.inventory.addItemToCart(produto); }); -Then('o ícone de favorito deve estar ativo para o produto {string}', async function (produto) { - // Valido se o botão mudou para "REMOVE", indicando que foi selecionado - const buttonRemove = this.page.locator(`[data-test="remove-${produto.toLowerCase().replace(/ /g, '-')}"]`); - await expect(buttonRemove).toBeVisible(); -}); \ No newline at end of file +Then( + "o ícone de favorito deve estar ativo para o produto {string}", + async function (produto) { + // Valido se o botão mudou para "REMOVE", indicando que foi selecionado + const buttonRemove = this.page.locator( + `[data-test="remove-${produto.toLowerCase().replace(/ /g, "-")}"]`, + ); + await expect(buttonRemove).toBeVisible(); + }, +); diff --git a/tests/e2e/steps/inventory-components.step.ts b/tests/e2e/steps/inventory-components.step.ts index 8425ad5..f0b8cce 100644 --- a/tests/e2e/steps/inventory-components.step.ts +++ b/tests/e2e/steps/inventory-components.step.ts @@ -1,17 +1,17 @@ -import { Given, When, Then, DataTable } from '@cucumber/cucumber'; -import { PageManager } from '../../../pages/PageManager'; -import { expect } from '@playwright/test'; +import { Given, When, Then, DataTable } from "@cucumber/cucumber"; +import { PageManager } from "../../../pages/PageManager"; +import { expect } from "@playwright/test"; -Given('que estou logado no sistema', async function () { +Given("que estou logado no sistema", async function () { if (!this.pageManager) this.pageManager = new PageManager(this.page); await this.pageManager.login.navigate(); await this.pageManager.login.performLogin("standard_user", "secret_sauce"); }); -Given('estou na página de inventário', async function () { +Given("estou na página de inventário", async function () { // 1. Valida a URL (Rápido) await expect(this.page).toHaveURL(/.*inventory\.html/); - + // 2. Garante que a grade de produtos carregou antes de prosseguir. // Isso evita que os próximos passos fiquem "girando em falso". await this.pageManager.inventory.waitInventoryLoad(); @@ -19,79 +19,82 @@ Given('estou na página de inventário', async function () { // --- CENÁRIO: COMPONENTES PRINCIPAIS --- -Then('devo ver o título {string}', async function (titulo) { +Then("devo ver o título {string}", async function (titulo) { await this.pageManager.inventory.validateTitle(titulo); }); -Then('devo ver o menu hamburguer', async function () { +Then("devo ver o menu hamburguer", async function () { await this.pageManager.inventory.validateHamburgerMenu(); }); -Then('devo ver o carrinho de compras', async function () { +Then("devo ver o carrinho de compras", async function () { await this.pageManager.inventory.validateCartIcon(); }); -Then('devo ver o filtro de ordenação', async function () { +Then("devo ver o filtro de ordenação", async function () { await this.pageManager.inventory.validateSortDropdownVisible(); }); -Then('devo ver o rodapé com links sociais', async function () { +Then("devo ver o rodapé com links sociais", async function () { await this.pageManager.inventory.validateFooterVisible(); }); // --- CENÁRIO: LISTA DE PRODUTOS --- -Then('devo ver {int} produtos na lista', async function (qtd) { +Then("devo ver {int} produtos na lista", async function (qtd) { await this.pageManager.inventory.validateProductCount(qtd); }); -Then('cada produto deve ter uma imagem', async function () { +Then("cada produto deve ter uma imagem", async function () { await this.pageManager.inventory.validateImagesLoad(); }); -Then('cada produto deve ter um nome', async function () { +Then("cada produto deve ter um nome", async function () { await this.pageManager.inventory.validateProductNames(); }); -Then('cada produto deve ter uma descrição', async function () { +Then("cada produto deve ter uma descrição", async function () { await this.pageManager.inventory.validateProductDescriptions(); }); -Then('cada produto deve ter um preço', async function () { +Then("cada produto deve ter um preço", async function () { await this.pageManager.inventory.validateProductPrices(); }); -Then('cada produto deve ter um botão {string}', async function (btnText) { +Then("cada produto deve ter um botão {string}", async function (btnText) { await this.pageManager.inventory.validateProductButtons(btnText); }); // --- CENÁRIO: PRODUTOS ESPECÍFICOS (DATA TABLE) --- -Then('devo ver os seguintes produtos:', async function (dataTable: DataTable) { +Then("devo ver os seguintes produtos:", async function (dataTable: DataTable) { // Converte a tabela do Cucumber para array de arrays [['Nome', 'Preço'], ...] - const products = dataTable.rows(); + const products = dataTable.rows(); await this.pageManager.inventory.validateSpecificProducts(products); }); // --- CENÁRIO: ORDENAÇÃO E RODAPÉ --- -Then('devo ver as seguintes opções de ordenação:', async function (dataTable: DataTable) { - const options = dataTable.raw().flat(); - await this.pageManager.inventory.validateSortOptions(options); -}); +Then( + "devo ver as seguintes opções de ordenação:", + async function (dataTable: DataTable) { + const options = dataTable.raw().flat(); + await this.pageManager.inventory.validateSortOptions(options); + }, +); -Then('devo ver o link do Twitter no rodapé', async function () { - await this.pageManager.inventory.validateSocialLink('Twitter'); +Then("devo ver o link do Twitter no rodapé", async function () { + await this.pageManager.inventory.validateSocialLink("Twitter"); }); -Then('devo ver o link do Facebook no rodapé', async function () { - await this.pageManager.inventory.validateSocialLink('Facebook'); +Then("devo ver o link do Facebook no rodapé", async function () { + await this.pageManager.inventory.validateSocialLink("Facebook"); }); -Then('devo ver o link do LinkedIn no rodapé', async function () { - await this.pageManager.inventory.validateSocialLink('LinkedIn'); +Then("devo ver o link do LinkedIn no rodapé", async function () { + await this.pageManager.inventory.validateSocialLink("LinkedIn"); }); -Then('devo ver o texto de copyright', async function () { +Then("devo ver o texto de copyright", async function () { await this.pageManager.inventory.validateFooterCopy(); -}); \ No newline at end of file +}); diff --git a/tests/e2e/steps/login.step.ts b/tests/e2e/steps/login.step.ts index 070039f..bad56c7 100644 --- a/tests/e2e/steps/login.step.ts +++ b/tests/e2e/steps/login.step.ts @@ -1,38 +1,41 @@ -import { Given, When, Then } from '@cucumber/cucumber'; -import { expect } from '@playwright/test'; -import { PageManager } from '../../../pages/PageManager'; +import { Given, When, Then } from "@cucumber/cucumber"; +import { expect } from "@playwright/test"; +import { PageManager } from "../../../pages/PageManager"; -Given('que estou na página de login', async function () { +Given("que estou na página de login", async function () { if (!this.pageManager) this.pageManager = new PageManager(this.page); await this.pageManager.login.navigate(); }); // --- FLUXO POSITIVO --- -When('preencho as credenciais válidas', async function () { +When("preencho as credenciais válidas", async function () { const user = process.env.STANDARD_USER || "standard_user"; const pass = process.env.SECRET_SAUCE || "secret_sauce"; - + // O parametro 'true' já faz um wait interno, mas o Step 'Então' fará a validação final await this.pageManager.login.performLogin(user, pass, true); }); -When('realizo login com {string} e {string}', async function (usuario, senha) { +When("realizo login com {string} e {string}", async function (usuario, senha) { await this.pageManager.login.performLogin(usuario, senha, true); }); -Then('devo ser redirecionado para a vitrine de produtos', async function () { - console.log('[Step] Validando redirecionamento para Vitrine...'); +Then("devo ser redirecionado para a vitrine de produtos", async function () { + console.log("[Step] Validando redirecionamento para Vitrine..."); // Garante que a URL mudou para /inventory.html await expect(this.page).toHaveURL(/.*inventory\.html/, { timeout: 10000 }); }); // --- FLUXO NEGATIVO --- -When('tento logar com usuario {string} e senha {string}', async function (usuario, senha) { - console.log(`[Step] Executando login negativo para: ${usuario}`); - // Passo 'false' para não travar esperando o inventário - await this.pageManager.login.performLogin(usuario, senha, false); -}); +When( + "tento logar com usuario {string} e senha {string}", + async function (usuario, senha) { + console.log(`[Step] Executando login negativo para: ${usuario}`); + // Passo 'false' para não travar esperando o inventário + await this.pageManager.login.performLogin(usuario, senha, false); + }, +); -Then('devo ver a mensagem de erro {string}', async function (mensagem) { +Then("devo ver a mensagem de erro {string}", async function (mensagem) { await this.pageManager.login.validateErrorMessage(mensagem); -}); \ No newline at end of file +}); diff --git a/tests/e2e/support/hooks.ts b/tests/e2e/support/hooks.ts index 41e6e12..fcef9d2 100644 --- a/tests/e2e/support/hooks.ts +++ b/tests/e2e/support/hooks.ts @@ -1,15 +1,27 @@ -import { Before, After, BeforeAll, AfterAll, Status, setDefaultTimeout } from '@cucumber/cucumber'; -import { chromium, Browser, BrowserContext } from '@playwright/test'; -import { PageManager } from '../../../pages/PageManager'; -import * as dotenv from 'dotenv'; -import * as path from 'path'; - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -const envPath = path.resolve(process.cwd(), 'envs/.env.dev'); +import { + Before, + After, + BeforeAll, + AfterAll, + Status, + setDefaultTimeout, +} from "@cucumber/cucumber"; +import { chromium, Browser, BrowserContext } from "@playwright/test"; +import { PageManager } from "../../../pages/PageManager"; +import * as dotenv from "dotenv"; +import * as path from "path"; + +process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; +const envPath = path.resolve(process.cwd(), "envs/.env.dev"); dotenv.config({ path: envPath }); -process.on('unhandledRejection', (reason, promise) => { - console.error('🔥 CRITICAL: Unhandled Rejection at:', promise, 'reason:', reason); +process.on("unhandledRejection", (reason, promise) => { + console.error( + "🔥 CRITICAL: Unhandled Rejection at:", + promise, + "reason:", + reason, + ); }); let browser: Browser; @@ -18,18 +30,19 @@ let context: BrowserContext; setDefaultTimeout(120 * 1000); BeforeAll(async function () { - console.log('[Hooks] 🚀 Iniciando Browser...'); - const headlessMode = process.env.CI === 'true' || process.env.HEADLESS === 'true'; + console.log("[Hooks] 🚀 Iniciando Browser..."); + const headlessMode = + process.env.CI === "true" || process.env.HEADLESS === "true"; - browser = await chromium.launch({ + browser = await chromium.launch({ headless: headlessMode, args: [ - "--disable-gpu", - "--no-sandbox", + "--disable-gpu", + "--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage", - "--no-zygote" - ] + "--no-zygote", + ], }); }); @@ -39,64 +52,67 @@ Before(async function (scenario) { console.log(`[Hooks] ▶️ Cenário: ${scenarioName}`); if (this.label) { - this.label("framework", "cucumberjs"); - this.label("language", "typescript"); - - // Nível 1: Pasta Raiz (Ex: "E2E Web") - this.label("parentSuite", "E2E Web"); - - // Nível 2: Nome da Funcionalidade (Ex: "Login") - this.label("suite", featureName); - - // Nível 3: Nome do Cenário (Ex: "Login com sucesso") - this.label("subSuite", scenarioName); + this.label("framework", "cucumberjs"); + this.label("language", "typescript"); + + // Nível 1: Pasta Raiz (Ex: "E2E Web") + this.label("parentSuite", "E2E Web"); + + // Nível 2: Nome da Funcionalidade (Ex: "Login") + this.label("suite", featureName); + + // Nível 3: Nome do Cenário (Ex: "Login com sucesso") + this.label("subSuite", scenarioName); } context = await browser.newContext({ ignoreHTTPSErrors: true, viewport: { width: 1280, height: 720 }, - locale: 'en-US' + locale: "en-US", }); - + const page = await context.newPage(); this.page = page; this.pageManager = new PageManager(this.page); - + if (this.attach) { - this.pageManager.setAllureAttach(this.attach.bind(this)); + this.pageManager.setAllureAttach(this.attach.bind(this)); } }); After(async function (scenario) { if (scenario.result?.status === Status.FAILED) { if (this.page) { - try { - const png = await this.page.screenshot({ fullPage: true, timeout: 5000 }); - this.attach(png, 'image/png'); - } catch (e) { - console.warn('[Hooks] Falha ao tirar screenshot final.'); - } + try { + const png = await this.page.screenshot({ + fullPage: true, + timeout: 5000, + }); + this.attach(png, "image/png"); + } catch (e) { + console.warn("[Hooks] Falha ao tirar screenshot final."); + } } } try { - if (this.page && !this.page.isClosed()) await this.page.close(); - if (context) await context.close(); + if (this.page && !this.page.isClosed()) await this.page.close(); + if (context) await context.close(); } catch (e) { - console.warn(`[Hooks] Aviso ao fechar página/contexto: ${e}`); + console.warn(`[Hooks] Aviso ao fechar página/contexto: ${e}`); } }); AfterAll(async function () { - console.log('[Hooks] 🛑 Encerrando sessão global...'); + console.log("[Hooks] 🛑 Encerrando sessão global..."); try { - if (browser) await browser.close(); + if (browser) await browser.close(); } catch (e) { - console.warn(`[Hooks] Erro ao fechar browser: ${e}`); + console.warn(`[Hooks] Erro ao fechar browser: ${e}`); } - if (process.env.CI === 'true') { - console.log('[Hooks] 🏁 CI Detectado: Forçando Exit Code 0...'); - setTimeout(() => process.exit(0), 500); + if (process.env.CI === "true") { + console.log("[Hooks] 🏁 CI Detectado: Forçando Exit Code 0..."); + setTimeout(() => process.exit(0), 500); } -}); \ No newline at end of file +}); diff --git a/tests/e2e/support/world.ts b/tests/e2e/support/world.ts index 1a33de4..6a7ba5c 100644 --- a/tests/e2e/support/world.ts +++ b/tests/e2e/support/world.ts @@ -1,7 +1,7 @@ -import { setWorldConstructor, World, IWorldOptions } from '@cucumber/cucumber'; -import { AllureCucumberWorld } from 'allure-cucumberjs'; -import { PageManager } from '../../../pages/PageManager'; -import { Page } from '@playwright/test'; +import { setWorldConstructor, World, IWorldOptions } from "@cucumber/cucumber"; +import { AllureCucumberWorld } from "allure-cucumberjs"; +import { PageManager } from "../../../pages/PageManager"; +import { Page } from "@playwright/test"; export class CustomWorld extends AllureCucumberWorld { // Declaramos as propriedades que usamos nos Hooks para o TypeScript não reclamar @@ -13,4 +13,4 @@ export class CustomWorld extends AllureCucumberWorld { } } -setWorldConstructor(CustomWorld); \ No newline at end of file +setWorldConstructor(CustomWorld); diff --git a/tests/k6-load/grafana-dashboard.yaml b/tests/k6-load/grafana-dashboard.yaml index d51fd65..f034580 100644 --- a/tests/k6-load/grafana-dashboard.yaml +++ b/tests/k6-load/grafana-dashboard.yaml @@ -1,8 +1,8 @@ apiVersion: 1 providers: - - name: 'default' + - name: "default" org_id: 1 - folder: '' - type: 'file' + folder: "" + type: "file" options: - path: /var/lib/grafana/dashboards \ No newline at end of file + path: /var/lib/grafana/dashboards diff --git a/tests/k6-load/grafana-datasource.yaml b/tests/k6-load/grafana-datasource.yaml index 7e5b4d8..33e52ee 100644 --- a/tests/k6-load/grafana-datasource.yaml +++ b/tests/k6-load/grafana-datasource.yaml @@ -6,4 +6,4 @@ datasources: access: proxy database: k6 url: http://influxdb:8086 - isDefault: true \ No newline at end of file + isDefault: true diff --git a/tests/k6-load/src/requests/getRequest.js b/tests/k6-load/src/requests/getRequest.js index 92b8274..1d81dab 100644 --- a/tests/k6-load/src/requests/getRequest.js +++ b/tests/k6-load/src/requests/getRequest.js @@ -11,7 +11,7 @@ export let getFailRate = new Rate("_4_fail_rate"); export default class getRequest { get() { - let res = http.get(`${BASE_URL}/get`); + let res = http.get(`${BASE_URL}/get`); check(res, { "status is 200": (r) => r.status === 200, @@ -26,4 +26,4 @@ export default class getRequest { fail("Max Duration 10s"); } } -} \ No newline at end of file +} diff --git a/tests/k6-load/src/requests/postRequest.js b/tests/k6-load/src/requests/postRequest.js index 1c3b772..9aae1cc 100644 --- a/tests/k6-load/src/requests/postRequest.js +++ b/tests/k6-load/src/requests/postRequest.js @@ -27,10 +27,10 @@ export default class postRequest { }, }); - let res = http.post(`${BASE_URL}/post`, body, params); + let res = http.post(`${BASE_URL}/post`, body, params); check(res, { - "status is 200": (r) => r.status === 200, + "status is 200": (r) => r.status === 200, }); // Atualiza as métricas @@ -45,4 +45,4 @@ export default class postRequest { sleep(1); } -} \ No newline at end of file +} diff --git a/tests/k6-load/src/simulations/loadPerformanceFullstack.test.js b/tests/k6-load/src/simulations/loadPerformanceFullstack.test.js index f0c016a..72ea891 100644 --- a/tests/k6-load/src/simulations/loadPerformanceFullstack.test.js +++ b/tests/k6-load/src/simulations/loadPerformanceFullstack.test.js @@ -7,12 +7,10 @@ export let options = { stages: [ { duration: "10s", target: 20 }, { duration: "30s", target: 50 }, - { duration: "10s", target: 0 }, // Eu mudei para 0 para garantir o encerramento limpo das conexões + { duration: "10s", target: 0 }, // ], thresholds: { - // Eu aumentei o p(95) para 2.5s para acomodar a latência natural dos containers de CI http_req_duration: ["p(95)<2500"], - // Eu ajustei a taxa de erro permitida para 15% para evitar falhas por colisões de dados ou rede http_req_failed: ["rate<0.15"], }, }; @@ -32,7 +30,7 @@ export default function () { export function handleSummary(data) { return { - "report.html": htmlReport(data), + "report.html": htmlReport(data), "k6-summary.json": JSON.stringify(data), }; -} \ No newline at end of file +} diff --git a/tests/k6-load/webpack.config.js b/tests/k6-load/webpack.config.js index 9e3ec15..403e490 100644 --- a/tests/k6-load/webpack.config.js +++ b/tests/k6-load/webpack.config.js @@ -4,7 +4,10 @@ const { CleanWebpackPlugin } = require("clean-webpack-plugin"); module.exports = { mode: "production", entry: { - fullstack: path.resolve(__dirname, "./src/simulations/loadPerformanceFullstack.test.js"), + fullstack: path.resolve( + __dirname, + "./src/simulations/loadPerformanceFullstack.test.js", + ), }, output: { path: path.resolve(__dirname, "dist"), @@ -34,11 +37,9 @@ module.exports = { ], }, target: "web", - externals: /^(k6|https?\:\/\/)(\/.*)?/, - plugins: [ - new CleanWebpackPlugin(), - ], + externals: /^(k6|https?\:\/\/)(\/.*)?/, + plugins: [new CleanWebpackPlugin()], stats: { colors: true, }, -}; \ No newline at end of file +}; diff --git a/tests/mobile/mobile.spec.ts b/tests/mobile/mobile.spec.ts index ee1860d..6ce7814 100644 --- a/tests/mobile/mobile.spec.ts +++ b/tests/mobile/mobile.spec.ts @@ -1,12 +1,16 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; -test('Mobile - Validar a Responsividade da Home Swag Labs num pixel 5', async ({ page }) => { +test("Mobile - Validar a Responsividade da Home Swag Labs num pixel 5", async ({ + page, +}) => { // 1. Acessa a Home - await page.goto('/'); - + await page.goto("/"); + // Pega o tamanho da janela atual (Viewport) const viewportSize = page.viewportSize(); - console.log(`[DEBUG] Tamanho da tela: ${viewportSize?.width}x${viewportSize?.height}`); + console.log( + `[DEBUG] Tamanho da tela: ${viewportSize?.width}x${viewportSize?.height}`, + ); // A largura deve ser menor que 500px (Pixel 5 tem 393px) expect(viewportSize?.width).toBeLessThan(500); @@ -18,12 +22,12 @@ test('Mobile - Validar a Responsividade da Home Swag Labs num pixel 5', async ({ await expect(loginInput).toBeVisible(); // 3. Valida o Logo (Visual) - await expect(page.locator('.login_logo')).toBeVisible(); + await expect(page.locator(".login_logo")).toBeVisible(); // 4. Verificar se não tem a barra de rolagem horizontal const scrollWidth = await page.evaluate(() => document.body.scrollWidth); const clientWidth = await page.evaluate(() => document.body.clientWidth); - + // Se scrollWidth for igual clientWidth, não tem scroll horizontal "vazando" expect(scrollWidth).toBe(clientWidth); -}); \ No newline at end of file +}); diff --git a/tests/visual/pages.visual.spec.ts b/tests/visual/pages.visual.spec.ts index b946099..59c3344 100644 --- a/tests/visual/pages.visual.spec.ts +++ b/tests/visual/pages.visual.spec.ts @@ -1,15 +1,15 @@ -import { test, expect } from '@playwright/test'; +import { test, expect } from "@playwright/test"; import { allure } from "allure-playwright"; -import { PageManager } from '../../pages/PageManager'; +import { PageManager } from "../../pages/PageManager"; -test.describe('Regressão Visual', () => { +test.describe("Regressão Visual", () => { let pageManager: PageManager; test.beforeEach(async ({ page }) => { pageManager = new PageManager(page); }); - test('Deve garantir o layout da Login Page', async ({ page }) => { + test("Deve garantir o layout da Login Page", async ({ page }) => { // 🏷️ METADADOS ALLURE allure.epic("Interface do Usuário (UI)"); allure.feature("Autenticação"); @@ -19,12 +19,12 @@ test.describe('Regressão Visual', () => { allure.owner("Tiago Silva"); await pageManager.login.navigate(); - + // Tira um print e compara com o "baseline" (imagem de referência) - await expect(page).toHaveScreenshot('login-page.png', { fullPage: true }); + await expect(page).toHaveScreenshot("login-page.png", { fullPage: true }); }); - test('Deve garantir o layout do Inventário', async ({ page }) => { + test("Deve garantir o layout do Inventário", async ({ page }) => { // 🏷️ METADADOS ALLURE allure.epic("Interface do Usuário (UI)"); allure.feature("Vitrine de Produtos"); @@ -34,9 +34,11 @@ test.describe('Regressão Visual', () => { allure.owner("Tiago Silva"); await pageManager.login.navigate(); - await pageManager.login.performLogin('standard_user', 'secret_sauce'); - + await pageManager.login.performLogin("standard_user", "secret_sauce"); + await expect(page).toHaveURL(/.*inventory\.html/); - await expect(page).toHaveScreenshot('inventory-page.png', { fullPage: true }); + await expect(page).toHaveScreenshot("inventory-page.png", { + fullPage: true, + }); }); -}); \ No newline at end of file +}); diff --git a/tsconfig.json b/tsconfig.json index 57827a4..a2ecdd3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,7 @@ "lib": ["ES2023", "DOM"], "module": "NodeNext", "moduleResolution": "NodeNext", - + // Configurações de Path Mapping para imports limpos "baseUrl": ".", "paths": { @@ -26,4 +26,4 @@ }, "include": ["**/*.ts", "cucumber.js"], "exclude": ["node_modules"] -} \ No newline at end of file +} From cd2f57f97bc5ed97be9985f3b606063f516cf633 Mon Sep 17 00:00:00 2001 From: Tiago Oliveira Silva Date: Wed, 18 Feb 2026 08:34:59 -0300 Subject: [PATCH 2/2] report adjustments --- report.html | 146 ++++++++++++++++++++++++++-------------------------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/report.html b/report.html index 4945b31..002d112 100644 --- a/report.html +++ b/report.html @@ -10,7 +10,7 @@ - Test Report: 2026-02-13 13:26 + Test Report: 2026-02-13 17:34