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ítedescribe. 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 comodescribe, é 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 atributoplaceholdercom 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 textohttps://walmyr.devnele.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 arquivoqrcode.pngque foi baixado para o diretóriocypress/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 comandocy.task()é utilizado para invocar uma tarefa customizada do Cypress chamadadecodeQRFromBase64. 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. Olog: falseevita 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 é exatamentehttps://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')econst jsQR = require('jsqr'): Importam as bibliotecas necessárias.Jimpé uma biblioteca de processamento de imagens para Node.js ejsQRé uma biblioteca para decodificação de QR Code.module.exports = on => { ... }: Exporta uma função que recebe o objetoondo 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 comandocy.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):Jimplê o buffer para criar um objeto de imagem que pode ser manipulado.const imageData = { ... }: Prepara os dados da imagem no formato esperado pela bibliotecajsQR. Isso inclui os dados de pixel, largura e altura da imagem.const decoded = jsQR(imageData.data, imageData.width, imageData.height):jsQRtenta 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, retornanull.
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çãodefineConfigdo Cypress para ajudar na definição da configuração.const tasks = require('./cypress/support/tasks'): Importa o arquivoindex.jsque 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çãotasksimportada é chamada, passando o objetoon. Isso registra todas as tarefas definidas emcypress/support/tasks/index.jscom o Cypress.return config: É importante retornar o objetoconfigmodificado (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.