Daily driver 3+ years E2E testing

Playwright & E2E Testing

End-to-end test automation with Playwright — cross-browser, parallel execution, and Page Object Model architecture.

PlaywrightTypeScriptPOMCross-browserCI/CD

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 selectorsgetByRole, getByLabel, and getByTestId over 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