Playwright & E2E Testing
End-to-end test automation with Playwright — cross-browser, parallel execution, and Page Object Model architecture.
How I use it
Playwright is my primary E2E testing tool. I build test suites that cover full user flows across Chromium, Firefox, and WebKit — running in parallel with isolated browser contexts. Every suite follows a Page Object Model architecture for maintainability and reuse.
Real example: Login flow test
This is a simplified version of a real login flow test. Notice the POM pattern — the test reads like a user story, while the page object handles DOM selectors and interactions.
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { DashboardPage } from '../pages/DashboardPage';
test.describe('Authentication', () => {
let loginPage: LoginPage;
let dashboardPage: DashboardPage;
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page);
dashboardPage = new DashboardPage(page);
await loginPage.navigate();
});
test('valid credentials redirect to dashboard', async () => {
await loginPage.login('admin@test.com', 'securePass123');
await expect(dashboardPage.welcomeMessage).toBeVisible();
await expect(dashboardPage.welcomeMessage)
.toHaveText(/Welcome, Admin/);
});
test('invalid credentials show error', async () => {
await loginPage.login('wrong@test.com', 'badpass');
await expect(loginPage.errorAlert).toBeVisible();
await expect(loginPage.errorAlert)
.toContainText('Invalid credentials');
});
}); Page Object pattern
Every page in the application gets a corresponding class that encapsulates selectors and actions. This makes tests resilient to UI changes — if a selector changes, I fix it in one place.
import { type Page, type Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorAlert: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorAlert = page.getByRole('alert');
}
async navigate() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
} What I focus on
- Stable selectors —
getByRole,getByLabel, andgetByTestIdover fragile CSS selectors - Test isolation — each test gets its own browser context, no shared state leaks
- Parallel execution — Playwright's built-in parallelism with configurable worker count
- CI integration — headless runs in GitHub Actions with artifact collection on failure
- Visual regression — screenshot comparisons for critical UI components