<h2>Entendendo Testes End-to-End e a Importância do Playwright</h2>
<p>Testes end-to-end (E2E) são aqueles que simulam o comportamento real do usuário no seu aplicativo, do início ao fim. Diferente dos testes unitários que isolam uma função ou dos testes de integração que testam módulos específicos, os E2E validam fluxos completos: login, navegação, preenchimento de formulários, e confirmação de resultados finais. O Playwright é uma ferramenta moderna que automatiza navegadores como Chrome, Firefox e Safari, permitindo escrever testes robustos e confiáveis.</p>
<p>Por que escolher Playwright? Ele é mantido pela Microsoft, oferece suporte multiplataforma, é rápido, e possui excelente documentação. Quando combinado com TypeScript, você ganha tipagem estática — o compilador avisa erros antes da execução. Isso reduz bugs relacionados a propriedades inexistentes ou valores inválidos. Nesta aula, vamos além do básico e implementaremos o padrão <strong>Page Objects com tipagem completa</strong>, transformando seus testes em código profissional e mantível.</p>
<h2>O Padrão Page Object e Tipagem em TypeScript</h2>
<h3>O que é Page Object?</h3>
<p>O padrão Page Object encapsula os detalhes de uma página (ou componente) em uma classe. Em vez de espalhar seletores CSS ou XPath por toda a suite de testes, você centraliza a lógica de interação em objetos fortemente tipados. Um teste fica assim: <code>await loginPage.fillEmail('user@example.com')</code> em vez de <code>await page.fill('input[name="email"]', 'user@example.com')</code>. Isso torna os testes legíveis, mantíveis e reutilizáveis.</p>
<h3>Tipagem Completa no TypeScript</h3>
<p>TypeScript permite definir interfaces e classes com tipos explícitos. Quando criamos Page Objects tipados, definimos os retornos de cada método, validamos os parâmetros de entrada, e aproveitamos o intellisense da IDE. Se você tenta chamar um método que não existe ou passar um tipo inválido, o TypeScript grita antes do teste rodar. Essa segurança é especialmente valiosa em grandes suites de testes.</p>
<h2>Estruturando seu Projeto Playwright + TypeScript</h2>
<h3>Instalação e Configuração Inicial</h3>
<p>Comece criando um novo projeto Node.js com Playwright e TypeScript:</p>
<pre><code class="language-bash">npm init -y
npm install --save-dev @playwright/test typescript @types/node
npx tsc --init</code></pre>
<p>Configure o <code>tsconfig.json</code> com compilação moderna:</p>
<pre><code class="language-json">{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/*/"],
"exclude": ["node_modules"]
}</code></pre>
<p>Crie a estrutura de diretórios:</p>
<pre><code>projeto/
├── src/
│ ├── pages/
│ │ ├── basePage.ts
│ │ ├── loginPage.ts
│ │ └── dashboardPage.ts
│ ├── tests/
│ │ └── login.spec.ts
│ └── fixtures/
│ └── testFixtures.ts
├── playwright.config.ts
└── tsconfig.json</code></pre>
<h3>Configuração do Playwright</h3>
<p>Crie <code>playwright.config.ts</code>:</p>
<pre><code class="language-typescript">import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './src/tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});</code></pre>
<h2>Implementando Page Objects Tipados</h2>
<h3>Base Page: A Fundação</h3>
<p>Toda página herda de uma classe base que encapsula ações comuns. Isso evita duplicação de código e centraliza a lógica de espera e manipulação do Playwright:</p>
<pre><code class="language-typescript">import { Page, Locator } from '@playwright/test';
export class BasePage {
protected page: Page;
constructor(page: Page) {
this.page = page;
}
async navigate(url: string): Promise<void> {
await this.page.goto(url);
}
async fill(locator: Locator, value: string): Promise<void> {
await locator.fill(value);
}
async click(locator: Locator): Promise<void> {
await locator.click();
}
async getText(locator: Locator): Promise<string> {
return await locator.textContent() || '';
}
async isVisible(locator: Locator): Promise<boolean> {
return await locator.isVisible();
}
async waitForElement(locator: Locator, timeout: number = 5000): Promise<void> {
await locator.waitFor({ state: 'visible', timeout });
}
}</code></pre>
<p>A classe <code>BasePage</code> é genérica e oferece métodos que qualquer página pode reutilizar. Note que todos os métodos são assíncronos (retornam <code>Promise</code>) — exigência do Playwright.</p>
<h3>Login Page: Exemplo Prático</h3>
<p>Agora criamos uma página específica para login. Defina interfaces para dados de entrada e saída, tornando o contrato explícito:</p>
<pre><code class="language-typescript">import { Page, Locator } from '@playwright/test';
import { BasePage } from './basePage';
interface LoginCredentials {
email: string;
password: string;
}
interface LoginResult {
success: boolean;
errorMessage?: string;
}
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorAlert: Locator;
constructor(page: Page) {
super(page);
// Defina os seletores uma única vez na classe
this.emailInput = page.locator('input[name="email"]');
this.passwordInput = page.locator('input[name="password"]');
this.loginButton = page.locator('button[type="submit"]');
this.errorAlert = page.locator('[role="alert"]');
}
async goToLoginPage(): Promise<void> {
await this.navigate('/login');
}
async fillEmail(email: string): Promise<void> {
await this.fill(this.emailInput, email);
}
async fillPassword(password: string): Promise<void> {
await this.fill(this.passwordInput, password);
}
async clickLoginButton(): Promise<void> {
await this.click(this.loginButton);
}
async login(credentials: LoginCredentials): Promise<void> {
await this.goToLoginPage();
await this.fillEmail(credentials.email);
await this.fillPassword(credentials.password);
await this.clickLoginButton();
}
async getErrorMessage(): Promise<string> {
return await this.getText(this.errorAlert);
}
async isErrorDisplayed(): Promise<boolean> {
return await this.isVisible(this.errorAlert);
}
async validateLoginResult(): Promise<LoginResult> {
const isError = await this.isErrorDisplayed();
if (isError) {
return {
success: false,
errorMessage: await this.getErrorMessage(),
};
}
return { success: true };
}
}</code></pre>
<p>Observe que:</p>
<ul>
<li><strong>Seletores são propriedades da classe</strong> (<code>emailInput</code>, <code>passwordInput</code>) — você não repete eles em cada método.</li>
<li><strong>Métodos possuem nomes descritivos</strong> — <code>fillEmail()</code> vs genérico <code>fill()</code>.</li>
<li><strong>Interfaces tipam entradas e saídas</strong> — <code>LoginCredentials</code> garante que você passa <code>email</code> e <code>password</code>.</li>
<li><strong>Métodos compostos</strong> — <code>login()</code> agrupa ações lógicas que sempre andam juntas.</li>
</ul>
<h3>Dashboard Page: Outro Exemplo</h3>
<p>Para demonstrar que o padrão escala, aqui está uma página do dashboard:</p>
<pre><code class="language-typescript">import { Page, Locator } from '@playwright/test';
import { BasePage } from './basePage';
interface UserInfo {
name: string;
email: string;
role: string;
}
export class DashboardPage extends BasePage {
readonly welcomeMessage: Locator;
readonly userProfileButton: Locator;
readonly logoutButton: Locator;
readonly dataTable: Locator;
constructor(page: Page) {
super(page);
this.welcomeMessage = page.locator('h1, h2');
this.userProfileButton = page.locator('button[aria-label="User profile"]');
this.logoutButton = page.locator('button:has-text("Logout")');
this.dataTable = page.locator('table');
}
async getWelcomeText(): Promise<string> {
return await this.getText(this.welcomeMessage);
}
async getUserInfo(): Promise<UserInfo> {
// Exemplo fictício — adapte aos seletores reais
const name = await this.page.locator('[data-testid="user-name"]').textContent() | | ''; const email = await this.page.locator('[data-testid="user-email"]').textContent() || ''; const role = await this.page.locator('[data-testid="user-role"]').textContent() || '';
return { name, email, role };
}
async logout(): Promise<void> {
await this.click(this.userProfileButton);
await this.click(this.logoutButton);
}
async isTableVisible(): Promise<boolean> {
return await this.isVisible(this.dataTable);
}
}</code></pre>
<h2>Escrevendo Testes com Page Objects Tipados</h2>
<h3>Teste de Login Bem-Sucedido</h3>
<p>Agora que temos os Page Objects, escrever testes é simples e legível:</p>
<pre><code class="language-typescript">import { test, expect, Page } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';
import { DashboardPage } from '../pages/dashboardPage';
test.describe('Login Flow', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
});
test('should login successfully with valid credentials', async ({ page }) => {
// Arrange
const validCredentials = {
email: 'user@example.com',
password: 'SecurePassword123!',
};
// Act
await loginPage.login(validCredentials);
// Assert
await expect(page).toHaveURL(/.*dashboard/);
const welcomeText = await dashboardPage.getWelcomeText();
expect(welcomeText).toContain('Welcome');
});
test('should display error with invalid credentials', async () => {
// Arrange
const invalidCredentials = {
email: 'wrong@example.com',
password: 'WrongPassword123!',
};
// Act
await loginPage.login(invalidCredentials);
// Assert
const result = await loginPage.validateLoginResult();
expect(result.success).toBe(false);
expect(result.errorMessage).toContain('Invalid credentials');
});
test('should logout successfully', async ({ page }) => {
// Arrange — fazer login primeiro
const credentials = {
email: 'user@example.com',
password: 'SecurePassword123!',
};
// Act
await loginPage.login(credentials);
await page.waitForURL(/.*dashboard/);
await dashboardPage.logout();
// Assert
await expect(page).toHaveURL(/.*login/);
});
});</code></pre>
<p>Veja como o teste é <strong>declarativo e fácil de ler</strong>. Quem não conhece Playwright ainda consegue entender o que está sendo testado apenas lendo o código.</p>
<h3>Fixtures Customizados para Reutilização</h3>
<p>Para evitar repetir a inicialização de Page Objects, crie um fixture customizado:</p>
<pre><code class="language-typescript">import { test as baseTest, Page } from '@playwright/test';
import { LoginPage } from '../pages/loginPage';
import { DashboardPage } from '../pages/dashboardPage';
interface TestFixtures {
loginPage: LoginPage;
dashboardPage: DashboardPage;
authenticatedPage: Page;
}
export const test = baseTest.extend<TestFixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
const dashboardPage = new DashboardPage(page);
await use(dashboardPage);
},
authenticatedPage: async ({ page, loginPage }, use) => {
await loginPage.login({
email: 'user@example.com',
password: 'SecurePassword123!',
});
await page.waitForURL(/.*dashboard/);
await use(page);
},
});
export { expect };</code></pre>
<p>Agora seus testes ficam ainda mais limpios:</p>
<pre><code class="language-typescript">import { test, expect } from '../fixtures/testFixtures';
test('should display user info on dashboard', async ({ dashboardPage, authenticatedPage }) => {
const userInfo = await dashboardPage.getUserInfo();
expect(userInfo.name).toBeTruthy();
expect(userInfo.email).toContain('@');
});</code></pre>
<p>O fixture <code>authenticatedPage</code> já faz o login automaticamente — você não precisa repetir essa lógica em cada teste.</p>
<h2>Boas Práticas e Padrões Avançados</h2>
<h3>Evitando Esperas Implícitas</h3>
<p>O Playwright já espera por elementos automaticamente antes de interagir. Porém, às vezes você precisa de controle fino. Use <code>waitForElement()</code> com prudência:</p>
<pre><code class="language-typescript">async waitForLoadingToComplete(): Promise<void> {
const spinner = this.page.locator('[data-testid="loading-spinner"]');
await spinner.waitFor({ state: 'hidden', timeout: 10000 });
}</code></pre>
<h3>Encapsulando Validações Complexas</h3>
<p>Não coloque assertions (expect) dentro dos Page Objects — eles devem apenas retornar dados. As assertions ficam nos testes:</p>
<pre><code class="language-typescript"></code></pre>
<h3>Tratamento de Cenários de Erro</h3>
<p>Crie métodos que retornam estados conhecidos:</p>
<pre><code class="language-typescript">async attemptLogin(credentials: LoginCredentials): Promise<LoginResult> {
await this.login(credentials);
try {
await this.page.waitForURL(/.*dashboard/, { timeout: 3000 });
return { success: true };
} catch {
const error = await this.getErrorMessage();
return { success: false, errorMessage: error };
}
}</code></pre>
<h2>Executando e Monitorando os Testes</h2>
<h3>Rodando os Testes</h3>
<pre><code class="language-bash"># Todos os testes
npx playwright test
Apenas um arquivo
npx playwright test src/tests/login.spec.ts
Modo watch (para desenvolvimento)
npx playwright test --watch
Com UI interativo
npx playwright test --ui
Modo debug (passo a passo)
npx playwright test --debug</code></pre>
<h3>Gerando Relatórios</h3>
<p>O Playwright gera automaticamente relatórios HTML. Abra com:</p>
<pre><code class="language-bash">npx playwright show-report</code></pre>
<h2>Conclusão</h2>
<p>Ao dominar testes E2E com Playwright, TypeScript e Page Objects tipados, você constrói uma suite robusta, mantível e escalável. Os três pontos principais que levam você de iniciante a profissional são: <strong>(1) Page Objects centralizam seletores e lógica de página, eliminando duplicação e tornando mudanças de UI simples;</strong> <strong>(2) TypeScript com interfaces explícitas evita erros de tipagem em tempo de compilação, não de execução;</strong> <strong>(3) Fixtures customizados eliminam repetição de setup, deixando testes focados apenas na lógica de negócio.</strong></p>
<p>Esses padrões não são apenas boas práticas — são investimentos que multiplicam sua produtividade conforme a suite cresce de 10 para 100 para 1000 testes.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://playwright.dev" target="_blank" rel="noopener noreferrer">Documentação Oficial do Playwright</a></li>
<li><a href="https://playwright.dev/docs/pom" target="_blank" rel="noopener noreferrer">Guia de Page Objects em Playwright</a></li>
<li><a href="https://www.typescriptlang.org/docs/" target="_blank" rel="noopener noreferrer">TypeScript Official Documentation</a></li>
<li><a href="https://martinfowler.com/articles/testing-strategies.html" target="_blank" rel="noopener noreferrer">Testing Best Practices - Martin Fowler</a></li>
<li><a href="https://playwright.dev/docs/api/class-test" target="_blank" rel="noopener noreferrer">Playwright Test Runner API Reference</a></li>
</ul>
<p><!-- FIM --></p>