Este blog post explora um exemplo prático de testes end-to-end (E2E) utilizando Cypress em um projeto de Gerador de QR Code. Analisaremos o código-fonte dos testes, detalhando cada etapa e explicando como o Cypress interage com a aplicação, manipula downloads e até decodifica QR Codes para validação.

O Código Completo do Teste Cypress

Começaremos com o código completo do teste E2E localizado em cypress/e2e/qrCodeGen.cy.js:

describe('QR Code Generator', () => {
  beforeEach(() => {
    cy.visit('https://v0-gerador-de-qr-code-sepia.vercel.app/')
  })

  it('successfully generates a QR code, dowloads it, and access the website enconded on it', () => {
    cy.get('input[placeholder="https://exemplo.com"]').type('https://walmyr.dev')
    cy.contains('button', 'Gerar').click()
    cy.contains('button', 'Baixar QR Code').click()

    cy.readFile('./cypress/downloads/qrcode.png', 'base64')
      .then(base64 => cy.task('decodeQRFromBase64', base64, { log: false }))
      .then(encodedUrl => encodedUrl)
      .then(url => {
        cy.visit(url)
        cy.url().should('be.equal', 'https://walmyr.dev/')
      })
  })
})

Entendendo a Estrutura do Teste

describe e beforeEach

describe('QR Code Generator', () => {
  beforeEach(() => {
    cy.visit('https://v0-gerador-de-qr-code-sepia.vercel.app/')
  })
  // ...
})
  • describe('QR Code Generator', () => { ... }): Define uma suíte de testes. É uma função global do Mocha (framework de testes que o Cypress utiliza) que agrupa testes relacionados. O primeiro argumento é o nome da suíte, e o segundo é uma função de callback que define os testes.
  • beforeEach(() => { ... }): É um hook do Mocha que executa um bloco de código antes de cada teste (it) dentro da suíte describe. Neste caso, ele garante que a aplicação do gerador de QR Code seja visitada antes de cada teste.
  • cy.visit('https://v0-gerador-de-qr-code-sepia.vercel.app/'): Este comando do Cypress navega para a URL especificada. É o ponto de partida para a interação com a aplicação.

O Caso de Teste Principal (it)

it('successfully generates a QR code, dowloads it, and access the website enconded on it', () => {
  // ...
})
  • it('...', () => { ... }): Define um caso de teste individual. Assim como describe, é uma função do Mocha. O primeiro argumento é uma descrição legível do que o teste deve fazer, e o segundo é a função de callback que define os passos do teste.

Interagindo com a Interface de Usuário

cy.get('input[placeholder="https://exemplo.com"]').type('https://walmyr.dev')
cy.contains('button', 'Gerar').click()
cy.contains('button', 'Baixar QR Code').click()
  • cy.get('input[placeholder="https://exemplo.com"]'): Este comando seleciona um elemento do DOM. Aqui, ele busca um elemento “ que tenha o atributo placeholder com o valor 'https://exemplo.com'. Este é o campo onde o usuário insere a URL do QR Code.
  • .type("https://walmyr.dev"): Após selecionar o campo de input, este comando simula a digitação do texto https://walmyr.dev nele.
  • cy.contains("button", "Gerar").click(): Este comando localiza um botão que contém o texto “Gerar” e simula um clique nele. Isso aciona a geração do QR Code.
  • cy.contains("button", "Baixar QR Code").click(): Similar ao anterior, este comando encontra o botão “Baixar QR Code” e clica nele, iniciando o download da imagem do QR Code.

Lendo e decodificando o QR Code

Esta é a parte mais interessante e técnica do teste, na qual o Cypress interage com o sistema de arquivos e utiliza uma tarefa customizada para decodificar o QR Code.

cy.readFile('./cypress/downloads/qrcode.png', 'base64')
  .then(base64 => cy.task('decodeQRFromBase64', base64, { log: false }))
  .then(encodedUrl => encodedUrl)
  .then(url => {
    cy.visit(url)
    cy.url().should('be.equal', 'https://walmyr.dev/')
  })
  • cy.readFile('./cypress/downloads/qrcode.png', 'base64'): Este comando lê o arquivo qrcode.png que foi baixado para o diretório cypress/downloads. O segundo argumento, 'base64', especifica que o conteúdo do arquivo deve ser lido como uma string Base64. O Cypress tem acesso ao sistema de arquivos do sistema operacional onde os testes estão sendo executados, o que é crucial para este passo.
  • .then(base64 => cy.task('decodeQRFromBase64', base64, { log: false })): Após ler o arquivo, o conteúdo Base64 é passado para a próxima etapa da cadeia de comandos. Aqui, o comando cy.task() é utilizado para invocar uma tarefa customizada do Cypress chamada decodeQRFromBase64. As tarefas customizadas são executadas no ambiente Node.js do Cypress (fora do navegador) e são ideais para operações que exigem acesso a bibliotecas Node.js ou ao sistema de arquivos de forma mais complexa. O log: false evita que a tarefa seja registrada no Command Log do Cypress.
  • .then(encodedUrl => encodedUrl): Este .then() simplesmente passa o resultado da decodificação (a URL codificada no QR Code) para a próxima etapa.
  • .then(url => { ... }): Com a URL decodificada em mãos, o teste prossegue:
    • cy.visit(url): O Cypress navega para a URL que foi extraída do QR Code. Isso simula o comportamento de um usuário que escanearia o QR Code e acessaria o link.
    • cy.url().should('be.equal', 'https://walmyr.dev/'): Finalmente, uma asserção é feita para verificar se a URL atual do navegador é exatamente https://walmyr.dev/. Isso confirma que o QR Code foi gerado corretamente, baixado, decodificado e que o link resultante leva ao destino esperado.

A Tarefa Customizada decodeQRFromBase64

A tarefa decodeQRFromBase64 é definida em cypress/support/tasks/index.js e é responsável por decodificar a imagem do QR Code. Ela utiliza as bibliotecas jimp para processamento de imagens e jsqr para a decodificação do QR Code.

const { Jimp } = require('jimp')
const jsQR = require('jsqr')

module.exports = on => {
  on('task', {
    decodeQRFromBase64(base64) {
      return (async () => {
        const buffer = Buffer.from(base64, 'base64')
        const image = await Jimp.read(buffer)
        const imageData = {
          data: new Uint8ClampedArray(image.bitmap.data),
          width: image.bitmap.width,
          height: image.bitmap.height
        }
        const decoded = jsQR(imageData.data, imageData.width, imageData.height)
        return decoded ? decoded.data : null
      })()
    }
  })
}

Explicação da Tarefa

  • const { Jimp } = require('jimp') e const jsQR = require('jsqr'): Importam as bibliotecas necessárias. Jimp é uma biblioteca de processamento de imagens para Node.js e jsQR é uma biblioteca para decodificação de QR Code.
  • module.exports = on => { ... }: Exporta uma função que recebe o objeto on do Cypress. Este objeto é usado para registrar event listeners.
  • on('task', { ... }): Registra uma nova tarefa customizada. O objeto passado contém as definições das tarefas.
  • decodeQRFromBase64(base64) { ... }: Esta é a função da tarefa que será chamada pelo comando cy.task(). Ela recebe a string Base64 da imagem do QR Code.
  • const buffer = Buffer.from(base64, 'base64'): Converte a string Base64 de volta para um buffer de dados binários.
  • const image = await Jimp.read(buffer): Jimp lê o buffer para criar um objeto de imagem que pode ser manipulado.
  • const imageData = { ... }: Prepara os dados da imagem no formato esperado pela biblioteca jsQR. Isso inclui os dados de pixel, largura e altura da imagem.
  • const decoded = jsQR(imageData.data, imageData.width, imageData.height): jsQR tenta decodificar o QR Code a partir dos dados da imagem.
  • return decoded ? decoded.data : null: Retorna os dados decodificados (a URL) se o QR Code for encontrado, caso contrário, retorna null.

Configuração do Cypress (cypress.config.js)

O arquivo cypress.config.js é onde as configurações globais do Cypress são definidas, incluindo a integração das tarefas customizadas.

const { defineConfig } = require('cypress')
const tasks = require('./cypress/support/tasks')

module.exports = defineConfig({
  allowCypressEnv: false,
  e2e: {
    fixturesFolder: false,
    supportFile: false,
    setupNodeEvents(on, config) {
      tasks(on)
      return config
    },
  },
  retries: {
    openMode: 0,
    runMode: 2,
  },
})

Explicação da Configuração

  • const { defineConfig } = require('cypress'): Importa a função defineConfig do Cypress para ajudar na definição da configuração.
  • const tasks = require('./cypress/support/tasks'): Importa o arquivo index.js que contém a definição das tarefas customizadas.
  • module.exports = defineConfig({ ... }): Exporta o objeto de configuração.
  • e2e: { ... }: Configurações específicas para testes E2E.
    • setupNodeEvents(on, config) { ... }: Esta função é executada no ambiente do Node.js antes que os testes sejam iniciados. É o local ideal para registrar tarefas customizadas, plugins e outras configurações de ambiente Node.js.
    • tasks(on): Aqui, a função tasks importada é chamada, passando o objeto on. Isso registra todas as tarefas definidas em cypress/support/tasks/index.js com o Cypress.
    • return config: É importante retornar o objeto config modificado (ou não) para que o Cypress continue com suas configurações padrão.
  • retries: { ... }: Configurações de retentativas para os testes.
    • openMode: 0: Não tenta novamente os testes quando executados no modo interativo (Cypress App).
    • runMode: 2: Tenta novamente os testes até 2 vezes quando executados no modo headless (CLI), o que pode ser útil para lidar com instabilidades intermitentes.

Conclusão

Este exemplo demonstra a capacidade do Cypress de ir além da simples interação com a interface do usuário. Ao combinar comandos de interação com o DOM, acesso ao sistema de arquivos e tarefas customizadas executadas no ambiente Node.js, é possível criar testes E2E robustos que validam cenários complexos, como a geração e decodificação de QR Codes. A modularidade das tarefas customizadas permite estender o Cypress para atender a praticamente qualquer necessidade de teste, tornando-o uma ferramenta poderosa para garantir a qualidade de aplicações web.


Confira o repositório completo em: https://github.com/wlsf82/qr-code-generator.


Quer aprender mais sobre automação de testes web? Conheça os cursos da Escola TAT.

Deixe um comentário