Stop Breaking Production: A Practical Guide to Playwright, TypeScript, and CI/CD

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

The 4:00 PM Friday Deployment Disaster

It is late Friday afternoon. Your team just pushed what looked like a harmless UI tweak to production. Ten minutes later, the support tickets start rolling in. The login button is dead, and the checkout flow is a ghost town. A tiny change in a CSS selector just broke the entire user journey, and now your weekend plans are replaced by an emergency rollback.

This scenario happens because manual testing cannot keep pace with modern development. As your app grows, the number of possible user paths explodes. Checking every form, button, and redirect manually before a release is a losing game. It leads to “release anxiety,” where teams become terrified of deploying code because they simply don’t know what might break next.

Why Legacy Tools Struggle with Modern Frameworks

Manual regression testing fails because humans get bored and skip “obvious” checks. But even early automation had its flaws. If you have 50 different screens to verify, a human might take three hours to click through them all. An automated script can do it in under three minutes, but only if the tool is reliable.

For a long time, Selenium was the only real option. However, it gained a reputation for being “flaky.” Tests would often fail because the browser hadn’t finished rendering an element before the script tried to click it. Developers spent more time fixing broken tests than writing new features. Managing complex browser drivers and synchronization issues eventually made E2E testing feel like more trouble than it was worth.

The Modern Toolkit: Why Playwright Wins

Today’s Single Page Applications (SPAs) built with React or Next.js need a testing tool that actually understands how the modern web works. Here is how the current landscape looks:

  • Selenium: Flexible but slow. It requires manual wait times and heavy driver management that feels clunky for JavaScript-heavy sites.
  • Cypress: Offers a great developer experience but runs inside the browser. This makes it difficult to handle multiple tabs or cross-domain testing without complex workarounds.
  • Playwright: Built by Microsoft, this tool is the current gold standard. It supports Chromium, WebKit (Safari), and Firefox out of the box. Its “Auto-wait” feature virtually eliminates the flakiness that plagued older tools by waiting for elements to be actionable before interacting.

Moving from a junior role to a senior engineer requires building software that people can trust. Combining Playwright with TypeScript gives you a type-safe, lightning-fast testing suite that catches bugs before your users ever see them.

Building Your Testing Framework

A good testing suite should be easy to write and even easier for your teammates to read. Let’s set up a professional environment from scratch.

1. Getting Started

Open your terminal in your project root and run the following command:

npm init playwright@latest

Choose TypeScript when prompted. The installer will create a tests folder and even offer to set up a GitHub Actions workflow. Select “Yes” for the workflow—it saves you the manual setup later.

2. Writing a Resilient Test

Let’s script a login flow. Instead of targeting fragile CSS classes like .btn-blue, we will use locators that mimic how a real person interacts with the page. Create tests/auth.spec.ts:

import { test, expect } from '@playwright/test';

test('user should be able to login with valid credentials', async ({ page }) => {
  await page.goto('https://example.com/login');

  // Using accessible locators makes tests 90% more stable
  await page.getByLabel('Username').fill('testuser');
  await page.getByLabel('Password').fill('password123');
  
  await page.getByRole('button', { name: 'Log in' }).click();

  // Verify the result
  await expect(page).toHaveURL(/.*dashboard/);
  await expect(page.getByText('Welcome, testuser')).toBeVisible();
});

By using getByRole, your test doesn’t care if you change the button color from blue to red. As long as it is still a button labeled “Log in,” the test will pass. This approach also ensures your app remains accessible to screen readers.

3. Scaling with the Page Object Model (POM)

If you have ten different tests that all start with a login, and your login URL changes, you shouldn’t have to update ten different files. The Page Object Model (POM) centralizes your UI logic. Create pages/LoginPage.ts:

import { Page, Locator } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.usernameInput = page.getByLabel('Username');
    this.passwordInput = page.getByLabel('Password');
    this.loginButton = page.getByRole('button', { name: 'Log in' });
  }

  async navigate() {
    await this.page.goto('/login');
  }

  async login(user: string, pass: string) {
    await this.usernameInput.fill(user);
    await this.passwordInput.fill(pass);
    await this.loginButton.click();
  }
}

Now, your actual test file is clean and readable:

test('refined login test', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.navigate();
  await loginPage.login('testuser', 'password123');
  await expect(page).toHaveURL(/.*dashboard/);
});

Automating the Safety Net with GitHub Actions

A test suite is only useful if it runs every time code changes. You cannot rely on memory. You need the system to enforce the rules. Your .github/workflows/playwright.yml file should look like this:

name: Playwright Tests
on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20
    - name: Install dependencies
      run: npm ci
    - name: Install Browsers
      run: npx playwright install --with-deps
    - name: Run tests
      run: npx playwright test
    - uses: actions/upload-artifact@v4
      if: always()
      with:
        name: playwright-report
        path: playwright-report/

Now, every Pull Request triggers a fresh test run on a virtual machine. If a developer accidentally breaks the checkout flow, the CI/CD pipeline will turn red, and the “Merge” button will stay disabled. This simple automation prevents bugs from reaching your customers.

The Bottom Line

Switching to automated testing with Playwright changes your entire development workflow. It replaces “hope” with data. Start by automating your most critical path—usually the login or checkout—and expand from there. With TypeScript and the Page Object Model, you aren’t just writing scripts; you are building a maintainable insurance policy for your code.

Share: