Sobre nós Guias Projetos Contactos
Админка
please wait

Puppeteer é a biblioteca Node.js da Google para controlar os navegadores headless Chrome e Chromium. Destaca-se em testes end-to-end, web scraping e tarefas automatizadas no browser. Este guia aborda a implementação de testes E2E abrangentes com Puppeteer, na perspetiva de um developer sénior.

Porquê Puppeteer

O Puppeteer oferece vantagens convincentes:

  1. Chrome DevTools Protocol: Controlo direto do browser
  2. Headless/Headed: Executar de forma invisível ou ver os testes a correr
  3. Screenshot/PDF: Capturar evidência visual
  4. Network Interception: Simular respostas de API
  5. Performance Metrics: Medir a experiência real do utilizador

Instalação

# Instalar o Puppeteer (faz download do Chromium)
npm install puppeteer
# Ou puppeteer-core (traga o seu próprio Chrome)
npm install puppeteer-core
# Com Jest como framework de testes
npm install --save-dev jest puppeteer jest-puppeteer

Utilização Básica

Script Simples

const puppeteer = require('puppeteer');
async function run() {
// Iniciar o browser
const browser = await puppeteer.launch({
headless: 'new', // Usar o novo modo headless
// headless: false, // Ver o browser durante os testes
// slowMo: 100, // Abrandar as ações para debugging
});
// Criar página
const page = await browser.newPage();
// Definir viewport
await page.setViewport({ width: 1280, height: 800 });
// Navegar
await page.goto('https://example.com', {
waitUntil: 'networkidle2' // Aguardar que a rede fique idle
});
// Obter o título da página
const title = await page.title();
console.log('Page title:', title);
// Screenshot
await page.screenshot({ path: 'screenshot.png', fullPage: true });
// Fechar o browser
await browser.close();
}
run();

Seletores e Interações

Encontrar Elementos

// Seletores CSS
await page.click('button.submit');
await page.click('#login-form input[name="email"]');
await page.click('[data-testid="submit-button"]');
// XPath
const [element] = await page.$x('//button[contains(text(), "Submit")]');
await element.click();
// Aguardar pelo seletor
await page.waitForSelector('.loaded-content');
await page.waitForSelector('.modal', { visible: true });
await page.waitForSelector('.spinner', { hidden: true });
// Verificar se o elemento existe
const exists = await page.$('.optional-element') !== null;

Interações do Utilizador

// Clicar
await page.click('#submit-btn');
await page.click('a.nav-link', { clickCount: 2 }); // Duplo clique
// Escrever
await page.type('#email', '[email protected]');
await page.type('#password', 'secret123', { delay: 100 }); // Semelhante a humano
// Limpar e escrever
await page.click('#search', { clickCount: 3 }); // Selecionar tudo
await page.type('#search', 'new search term');
// Ou limpar programaticamente
await page.$eval('#search', el => el.value = '');
await page.type('#search', 'new value');
// Teclado
await page.keyboard.press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.down('Shift');
await page.keyboard.press('Tab');
await page.keyboard.up('Shift');
// Selecionar dropdown
await page.select('#country', 'US');
await page.select('#colors', 'red', 'blue'); // Seleção múltipla
// Checkbox/Radio
await page.click('#agree-terms');
// Ou verificar primeiro o estado
const isChecked = await page.$eval('#agree-terms', el => el.checked);
if (!isChecked) await page.click('#agree-terms');
// Upload de ficheiro
const fileInput = await page.$('input[type="file"]');
await fileInput.uploadFile('/path/to/file.pdf');
// Hover
await page.hover('.dropdown-trigger');
// Drag and drop
await page.mouse.move(100, 100);
await page.mouse.down();
await page.mouse.move(200, 200);
await page.mouse.up();

Extrair Dados

// Obter conteúdo de texto
const heading = await page.$eval('h1', el => el.textContent);
// Obter atributo
const href = await page.$eval('a.main-link', el => el.getAttribute('href'));
// Obter múltiplos elementos
const links = await page.$$eval('nav a', elements =>
elements.map(el => ({
text: el.textContent,
href: el.href
}))
);
// Avaliar no contexto da página
const data = await page.evaluate(() => {
return {
url: window.location.href,
title: document.title,
itemCount: document.querySelectorAll('.item').length
};
});

Integração com Jest

Configuração

jest.config.js:

module.exports = {
preset: 'jest-puppeteer',
testMatch: ['**/*.e2e.js'],
testTimeout: 30000,
};

jest-puppeteer.config.js:

module.exports = {
launch: {
headless: 'new',
slowMo: process.env.SLOWMO ? 100 : 0,
args: ['--no-sandbox', '--disable-setuid-sandbox']
},
browserContext: 'default'
};

Exemplo de Teste

login.e2e.js:

describe('Login Flow', () => {
beforeAll(async () => {
await page.goto('http://localhost:3000/login');
});
beforeEach(async () => {
// Limpar o formulário entre testes
await page.reload();
});
it('should display login form', async () => {
await expect(page).toMatchElement('form#login');
await expect(page).toMatchElement('input[name="email"]');
await expect(page).toMatchElement('input[name="password"]');
await expect(page).toMatchElement('button[type="submit"]');
});
it('should show error for invalid credentials', async () => {
await page.type('input[name="email"]', '[email protected]');
await page.type('input[name="password"]', 'wrongpassword');
await page.click('button[type="submit"]');
await page.waitForSelector('.error-message');
const errorText = await page.$eval('.error-message', el => el.textContent);
expect(errorText).toContain('Invalid credentials');
});
it('should redirect on successful login', async () => {
await page.type('input[name="email"]', '[email protected]');
await page.type('input[name="password"]', 'correctpassword');
await page.click('button[type="submit"]');
await page.waitForNavigation();
expect(page.url()).toBe('http://localhost:3000/dashboard');
});
it('should validate email format', async () => {
await page.type('input[name="email"]', 'not-an-email');
await page.click('button[type="submit"]');
const validationMessage = await page.$eval(
'input[name="email"]',
el => el.validationMessage
);
expect(validationMessage).toBeTruthy();
});
});

Padrão Page Object

Organize os testes para facilitar a manutenção:

pages/LoginPage.js:

class LoginPage {
constructor(page) {
this.page = page;
this.url = 'http://localhost:3000/login';
// Seletores
this.emailInput = 'input[name="email"]';
this.passwordInput = 'input[name="password"]';
this.submitButton = 'button[type="submit"]';
this.errorMessage = '.error-message';
this.successMessage = '.success-message';
}
async navigate() {
await this.page.goto(this.url);
await this.page.waitForSelector(this.emailInput);
}
async login(email, password) {
await this.page.type(this.emailInput, email);
await this.page.type(this.passwordInput, password);
await this.page.click(this.submitButton);
}
async getErrorMessage() {
await this.page.waitForSelector(this.errorMessage);
return this.page.$eval(this.errorMessage, el => el.textContent);
}
async waitForRedirect(expectedUrl) {
await this.page.waitForNavigation();
return this.page.url() === expectedUrl;
}
}
module.exports = LoginPage;

login.e2e.js:

const LoginPage = require('./pages/LoginPage');
describe('Login', () => {
let loginPage;
beforeAll(async () => {
loginPage = new LoginPage(page);
});
beforeEach(async () => {
await loginPage.navigate();
});
it('should login successfully', async () => {
await loginPage.login('[email protected]', 'password123');
const redirected = await loginPage.waitForRedirect('http://localhost:3000/dashboard');
expect(redirected).toBe(true);
});
});

Network Interception

Simular Respostas de API

await page.setRequestInterception(true);
page.on('request', request => {
if (request.url().includes('/api/users')) {
request.respond({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Another User' }
])
});
} else {
request.continue();
}
});
// Navegar após configurar a interceção
await page.goto('http://localhost:3000/users');

Bloquear Recursos

await page.setRequestInterception(true);
page.on('request', request => {
const resourceType = request.resourceType();
// Bloquear imagens e fontes para testes mais rápidos
if (['image', 'font', 'stylesheet'].includes(resourceType)) {
request.abort();
} else {
request.continue();
}
});

Aguardar Resposta da API

// Aguardar por um pedido específico
const [response] = await Promise.all([
page.waitForResponse(response =>
response.url().includes('/api/submit') && response.status() === 200
),
page.click('#submit-btn')
]);
const data = await response.json();
expect(data.success).toBe(true);

Screenshots e Testes Visuais

// Screenshot de página inteira
await page.screenshot({
path: 'screenshots/full-page.png',
fullPage: true
});
// Screenshot de elemento
const element = await page.$('.hero-section');
await element.screenshot({ path: 'screenshots/hero.png' });
// Geração de PDF
await page.pdf({
path: 'output.pdf',
format: 'A4',
printBackground: true
});
// Regressão visual com jest-image-snapshot
const { toMatchImageSnapshot } = require('jest-image-snapshot');
expect.extend({ toMatchImageSnapshot });
it('should match visual snapshot', async () => {
const screenshot = await page.screenshot();
expect(screenshot).toMatchImageSnapshot({
failureThreshold: 0.01,
failureThresholdType: 'percent'
});
});

Autenticação

Tratar o Login Uma Única Vez

// auth.setup.js
const fs = require('fs');
async function globalSetup() {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('http://localhost:3000/login');
await page.type('#email', '[email protected]');
await page.type('#password', 'password');
await page.click('#submit');
await page.waitForNavigation();
// Guardar cookies
const cookies = await page.cookies();
fs.writeFileSync('cookies.json', JSON.stringify(cookies));
await browser.close();
}
module.exports = globalSetup;
// Nos testes
beforeAll(async () => {
const cookies = JSON.parse(fs.readFileSync('cookies.json'));
await page.setCookie(...cookies);
});

Dicas de Debugging

// Iniciar com DevTools
const browser = await puppeteer.launch({
headless: false,
devtools: true
});
// Adicionar a instrução debugger em evaluate
await page.evaluate(() => {
debugger; // Pausa no DevTools
});
// Logs da consola da página
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
// Erros da página
page.on('pageerror', error => console.error('PAGE ERROR:', error));
// Tirar screenshot em caso de falha
afterEach(async () => {
if (jasmine.currentTest.failedExpectations.length > 0) {
await page.screenshot({
path: `screenshots/failure-${Date.now()}.png`
});
}
});

Executar Testes

{
"scripts": {
"test:e2e": "jest --config jest.e2e.config.js",
"test:e2e:watch": "jest --config jest.e2e.config.js --watch",
"test:e2e:headed": "HEADLESS=false jest --config jest.e2e.config.js"
}
}
# Executar todos os testes E2E
npm run test:e2e
# Executar um ficheiro de teste específico
npm run test:e2e -- login.e2e.js
# Modo watch para desenvolvimento
npm run test:e2e:watch
# Ver o browser (debugging)
npm run test:e2e:headed

Principais Conclusões

  1. Padrão Page Object: Manter seletores e ações organizados
  2. Aguardar corretamente: Use esperas explícitas, não timeouts arbitrários
  3. Simular APIs: Testes mais rápidos e fiáveis
  4. Screenshot em caso de falha: Fazer debugging de testes falhados facilmente
  5. Execução em paralelo: O Jest executa ficheiros em paralelo por predefinição
  6. Estado limpo: Repor entre testes para isolamento

Os testes E2E com Puppeteer detetam problemas de integração que os testes unitários não apanham — essenciais para garantir confiança na funcionalidade voltada para o utilizador.

 
 
 
Языки
Темы
Copyright © 1999 — 2026
ZK Interactive