Skip to main content
/tayyab/portfolio — zsh
tayyab
TA
// dispatch.read --classified=false --access-level: public

The Complete Playwright TypeScript Guide for QA Engineers (2026)

March 18, 2026 EST. READ: 18 MIN #Quality Assurance

If you're a QA automation engineer in 2026, you're watching something significant happen: 70% of your peers are switching from Selenium to Playwright. The question isn't whether Playwright is better—it's whether you can afford to stay behind.

But here's what most tutorials miss: they show you how to write a single test. They don't show you how to build a framework that scales to hundreds of tests, stays maintainable as your application evolves, and integrates seamlessly into your CI/CD pipeline.

This guide bridges that gap. Over the past two years, I've built Playwright frameworks for five major projects—from fintech applications to AI-powered platforms—and learned exactly what separates "tests that work" from "test automation that scales."

Why Playwright Over Selenium in 2026?

Let me be direct: Playwright is faster, more reliable, and significantly easier to maintain than Selenium. Here's why:

  • Built-in waits: No more wrestling with implicit/explicit waits. Playwright automatically waits for elements, reducing flaky tests by 60%+.
  • Multi-browser testing: Write once, test on Chrome, Firefox, and Safari simultaneously without code changes.
  • True parallel execution: Run 100+ tests in minutes instead of hours. On my Wells Fargo automation project, we cut test execution from 45 minutes to 12 minutes.
  • Excellent TypeScript support: Type-safe testing means fewer bugs in your test code itself.
  • Modern debugging: Built-in inspector, trace viewer, and screenshot capabilities eliminate hours of investigation.
  • API testing: Use the same framework for browser + API testing. No need for Postman + Selenium separately.

The bottom line: If you're starting a new test automation project in 2026, Playwright is the default choice. And if you're maintaining an older Selenium suite, the migration path is clearer than ever.

Getting Started: Installation & Your First Test

Step 1: Create a New Node.js Project

mkdir playwright-qa-framework
cd playwright-qa-framework
npm init -y

Step 2: Install Playwright & Dependencies

npm install -D @playwright/test
npm install -D typescript ts-node @types/node
npm install -D dotenv

Step 3: Initialize TypeScript Configuration

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Step 4: Configure Playwright

Create playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: 'html',
  use: {
    baseURL: 'https://example.com',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Step 5: Write Your First Test

Create tests/example.spec.ts:

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

test('should navigate to homepage and verify title', async ({ page }) => {
  // Navigate to the application
  await page.goto('/');
  
  // Verify page title
  await expect(page).toHaveTitle(/Example Domain/i);
  
  // Verify heading is visible
  await expect(page.locator('h1')).toBeVisible();
  
  // Take a screenshot
  await page.screenshot({ path: 'screenshot.png' });
});

Run your first test:

npx playwright test

That's it. You now have a working Playwright test. But here's where most tutorials stop—and where real automation engineering begins.

Building a Scalable Framework: Page Object Model

Writing tests inline is fine for learning. Building a framework that survives 12 months of application evolution requires architecture. The Page Object Model (POM) is the proven pattern.

The principle is simple: separate test logic from UI interaction logic. When your application's login form changes from using a CSS class to a data-testid, you update it in one place, not in 50 tests.

Create Your Base Page Class

Create src/pages/BasePage.ts:

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

export class BasePage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async navigate(path: string = '') {
    await this.page.goto(`${this.page.url()}${path}`);
  }

  async click(locator: Locator) {
    await locator.click();
  }

  async fill(locator: Locator, text: string) {
    await locator.clear();
    await locator.fill(text);
  }

  async getText(locator: Locator): Promise {
    return await locator.textContent() || '';
  }

  async isVisible(locator: Locator): Promise {
    return await locator.isVisible();
  }

  async waitForURL(urlPattern: string | RegExp) {
    await this.page.waitForURL(urlPattern);
  }
}

Create a Login Page Object

Create src/pages/LoginPage.ts:

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

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.locator('input[data-testid="email"]');
    this.passwordInput = page.locator('input[data-testid="password"]');
    this.loginButton = page.locator('button[type="submit"]');
    this.errorMessage = page.locator('[role="alert"]');
  }

  async login(email: string, password: string) {
    await this.fill(this.emailInput, email);
    await this.fill(this.passwordInput, password);
    await this.click(this.loginButton);
  }

  async verifyErrorMessage(expectedText: string) {
    const error = await this.getText(this.errorMessage);
    return error.includes(expectedText);
  }
}

Write Tests Using Page Objects

Create tests/login.spec.ts:

import { test, expect } from '@playwright/test';
import { LoginPage } from '../src/pages/LoginPage';

test.describe('Login Feature', () => {
  let loginPage: LoginPage;

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.navigate('/login');
  });

  test('should login successfully with valid credentials', async ({ page }) => {
    await loginPage.login('user@example.com', 'password123');
    await loginPage.waitForURL('/dashboard');
    expect(page.url()).toContain('/dashboard');
  });

  test('should show error message with invalid credentials', async () => {
    await loginPage.login('user@example.com', 'wrongpassword');
    const hasError = await loginPage.verifyErrorMessage('Invalid credentials');
    expect(hasError).toBe(true);
  });
});

Benefits of this approach:

  • ✅ When the login form changes, update only LoginPage.ts
  • ✅ Tests are readable: what the test does, not how selectors work
  • ✅ Reusable methods reduce code duplication by 70%
  • ✅ New team members can write tests without understanding CSS selectors

Advanced Assertions & Waiting Strategies

Flaky tests are the #1 reason QA teams abandon automation. Playwright's built-in waits eliminate 90% of flakiness.

Use Specific Assertions

// ✅ GOOD: Wait for specific condition
await expect(page.locator('.success-message')).toBeVisible();
await expect(page.locator('.count')).toHaveText('5');
await expect(page).toHaveURL('/confirmation');

// ❌ BAD: Arbitrary waits (causes flakiness)
await page.waitForTimeout(2000); // What if it takes 2.1 seconds?

Advanced Wait Patterns

// Wait for API response
const responsePromise = page.waitForResponse(
  response => response.url().includes('/api/login')
);
await loginPage.click(loginButton);
await responsePromise;

// Wait for navigation
await Promise.all([
  page.waitForNavigation(),
  page.click(submitButton)
]);

// Wait for multiple conditions
const [response] = await Promise.all([
  page.waitForResponse(response => response.status() === 200),
  page.click(confirmButton)
]);

CI/CD Integration: GitHub Actions Pipeline

Tests that only run on your machine are just expensive debugging. Real automation runs on every commit. Here's exactly how to set it up.

Create .github/workflows/playwright-tests.yml:

name: Playwright Tests

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      
      - name: Run tests
        run: npm run test:playwright
      
      - name: Upload test report
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

Add to package.json:

{
  "scripts": {
    "test:playwright": "playwright test",
    "test:playwright:debug": "playwright test --debug",
    "test:playwright:ui": "playwright test --ui"
  }
}

Now every pull request automatically runs all your tests.** Failed tests block merging. This is how you prevent bugs from reaching production.

API Testing with Playwright

One of Playwright's superpowers: you can test APIs using the same framework as UI tests. No need for Postman + Selenium separately.

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

test('should fetch user data via API', async ({ request }) => {
  const response = await request.get('https://api.example.com/users/1');
  
  expect(response.status()).toBe(200);
  
  const user = await response.json();
  expect(user).toHaveProperty('id');
  expect(user.name).toBe('John Doe');
});

test('should create user via API and verify in UI', async ({ page, request }) => {
  // Create user via API
  const createResponse = await request.post('https://api.example.com/users', {
    data: {
      name: 'Test User',
      email: 'test@example.com'
    }
  });
  
  const newUser = await createResponse.json();
  
  // Verify in UI
  await page.goto(`/users/${newUser.id}`);
  await expect(page.locator('h1')).toContainText('Test User');
});

Real Project Example: Wells Fargo Automation

Let me show you exactly how this works in production. On the Wells Fargo financial services platform, we built a Playwright framework to test critical transaction flows.

Challenge: Legacy Selenium suite was flaky (40% of tests failed randomly), took 45 minutes to run, and developers avoided running tests locally.

Solution: Playwright + TypeScript + GitHub Actions

  • Migrated 150 tests from Selenium to Playwright (2 week sprint)
  • Eliminated flakiness using Playwright's built-in waits
  • Cut execution time from 45 min → 12 min (parallel execution)
  • Integrated into GitHub Actions: now runs on every PR
  • Result: Developers now run tests locally before pushing (flakiness was the blocker)

Key learning: The framework's architecture (Page Objects, proper waits, CI/CD) mattered more than the framework itself.

Performance Optimization: Running Tests Faster

This is where most tutorials end, but it's where real QA engineering happens. A 12-minute test suite still blocks your developers.

Enable Parallel Execution

// playwright.config.ts
export default defineConfig({
  // Run all test files in parallel by default
  fullyParallel: true,
  
  // Set number of workers (recommended: number of CPU cores)
  workers: process.env.CI ? 1 : 4,
});

Split Tests Across Browsers Smartly

// playwright.config.ts
export default defineConfig({
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
      // Only run critical tests on Firefox
      testMatch: '**/critical/**',
    },
  ],
});

Use Test Tags for Selective Execution

// Mark tests with tags
test('should process payment @critical @smoke', async () => { ... });
test('should show help text @ui', async () => { ... });

// Run only critical tests: npx playwright test --grep @critical
// Skip UI tests: npx playwright test --grep-invert @ui

Debugging & Troubleshooting

When tests fail in CI but pass locally (the worst feeling), Playwright's tools save hours of debugging.

Generate Test Reports

npx playwright test --reporter=html
npx playwright show-report

Use the Inspector

npx playwright test --debug

The Inspector opens a browser you can step through test-by-test, see what's happening, and debug in real-time.

Record Traces

// playwright.config.ts
use: {
  trace: 'on-first-retry', // Record trace if test fails on retry
  screenshot: 'only-on-failure',
  video: 'retain-on-failure',
}

Frequently Asked Questions

Is Playwright production-ready in 2026?

Absolutely. Playwright is used by Microsoft teams, Google, and enterprise QA teams worldwide. It's stable, well-maintained, and actively developed. I've run it in production on fintech, healthcare, and SaaS platforms. If you're concerned about maturity, Playwright is past that point.

Should I migrate my existing Selenium tests to Playwright?

If your Selenium suite is small (< 50 tests) and working well, the migration isn't urgent. But if you have 100+ tests, frequent flakiness, or slow execution, migration pays for itself in 2-3 months through reduced maintenance overhead. We successfully migrated 150+ tests from Selenium in a 2-week sprint on the Wells Fargo project.

Does Playwright support all browsers?

Yes. Playwright supports Chromium (Chrome, Edge), Firefox, and WebKit (Safari). You write tests once and run them on all three without code changes. Most teams test on Chrome for speed, then use Firefox/Safari for critical flows.

Can I test native mobile apps with Playwright?

No, Playwright is for web testing. For mobile native apps, you'd use Appium. However, Playwright does support testing mobile web browsers (responsive design testing) through device emulation.

How do I handle authentication in tests?

Store credentials in environment variables (never in code). For speed, reuse authentication state between tests instead of logging in for each test. Playwright provides context.storageState() to save and restore authentication.

What's the cost difference between Playwright and Selenium?

Both are open-source and free. The difference is in maintenance cost and developer time. Playwright reduces maintenance overhead by ~60% through better waits and debugging tools, which translates to cost savings from day one on larger teams.

Can Playwright test Single Page Applications (SPAs)?

Yes, Playwright excels at SPA testing. Built-in waits handle dynamic content loading naturally. I've tested heavily on AI-powered SPA applications where timing is critical—Playwright's wait strategies work better than Selenium here.

What testing patterns does Playwright enforce?

Playwright enforces test isolation (tests can run in any order) and encourages the Page Object Model through its design. It doesn't mandate a specific pattern, but POM works naturally with Playwright's architecture.

How do I handle dynamic/flaky elements?

Use Playwright's built-in waits and specific assertions. Instead of waiting for an element, wait for a specific condition: expect(locator).toBeVisible() instead of page.waitForSelector(). This handles 95% of flakiness automatically.

Can I use Playwright for performance testing?

Playwright can measure performance metrics (navigation timing, resource timing) but isn't designed for load testing. For load testing, combine Playwright (functional tests) with K6 or JMeter (performance tests). I cover this in our K6 tutorial.

Next Steps: Build Your Playwright Framework

You now have everything you need to:

  1. ✅ Set up a modern Playwright + TypeScript framework
  2. ✅ Structure tests using Page Objects for maintainability
  3. ✅ Write reliable tests that don't flake
  4. ✅ Integrate tests into CI/CD pipelines
  5. ✅ Test APIs alongside UI tests
  6. ✅ Debug failures quickly

The framework you build today should handle hundreds of tests without significant refactoring. That's what separates test automation from test frameworks—and that's what separates junior QA engineers from senior automation architects.

If you're looking to implement a Playwright framework for your team or want help migrating from Selenium, I offer test automation framework setup services that include architecture design, implementation, and CI/CD integration. I can also help your team with training and coaching to get up to speed quickly.

Let's discuss how to build a Playwright framework that scales with your application

Related Articles:

Tayyab Akmal
// author

Tayyab Akmal

AI & QA Automation Engineer

Automation & AI Engineer with 6+ years in scalable test automation and real-world AI solutions. I build intelligent frameworks, QA pipelines, and AI agents that make testing faster, smarter, and more reliable.

// feedback_channel

FOUND THIS USEFUL?

Share your thoughts or let's discuss automation testing strategies.

→ Start Conversation
Available for hire