DEV Community

Manivannan
Manivannan

Posted on

Full Stack Automation Testing with Playwright: The Complete Guide

Full Stack Automation Testing with Playwright: The Complete Guide

Published: April 2026 | Author: Automation Engineer | Read Time: ~20 min


Introduction

Playwright has rapidly become one of the most powerful automation testing frameworks in the industry. Originally built by Microsoft, it supports Chromium, Firefox, and WebKit out of the box, and covers everything from lightning-fast unit tests to complex end-to-end (E2E) flows — and even API testing. In this blog, we'll walk through a full-stack automation strategy using Playwright across all testing layers.

Whether you're building a testing suite from scratch or migrating from Selenium or Cypress, this guide will help you architect a robust, maintainable automation framework.


Table of Contents

  1. Why Playwright?
  2. Installation & Project Setup
  3. Unit & Integration Testing with Playwright
  4. API Testing with Playwright
  5. UI & End-to-End Testing
  6. Page Object Model (POM)
  7. Test Data Management
  8. CI/CD Integration
  9. Reporting & Debugging
  10. Best Practices
  11. Conclusion

Why Playwright?

Before diving into code, let's understand why Playwright stands out:

Feature Playwright Cypress Selenium
Multi-browser support ✅ Chromium, Firefox, WebKit ⚠️ Chrome, Firefox (limited) ✅ All
API Testing built-in ✅ Yes ❌ No ❌ No
Auto-wait mechanism ✅ Smart waits ✅ Smart waits ❌ Manual waits
Parallel execution ✅ Native ⚠️ Dashboard (paid) ⚠️ Grid required
Network interception ✅ Yes ✅ Yes ❌ Limited
Headless mode ✅ Yes ✅ Yes ✅ Yes
TypeScript support ✅ First-class ✅ Yes ⚠️ Partial
Mobile emulation ✅ Yes ❌ No ❌ Limited

Playwright's ability to handle API, UI, and integration testing in one framework makes it an ideal choice for full-stack teams.


Installation & Project Setup

Prerequisites

  • Node.js v18 or above
  • npm or yarn

Step 1: Initialize a New Project

mkdir playwright-fullstack-tests
cd playwright-fullstack-tests
npm init -y
Enter fullscreen mode Exit fullscreen mode

Step 2: Install Playwright

npm init playwright@latest
Enter fullscreen mode Exit fullscreen mode

This will prompt you to:

  • Choose TypeScript or JavaScript (recommend TypeScript)
  • Set the test directory (default: tests/)
  • Add a GitHub Actions workflow (optional but recommended)
  • Install Playwright browsers

Step 3: Recommended Folder Structure

playwright-fullstack-tests/
├── tests/
│   ├── unit/
│   │   └── utils.spec.ts
│   ├── integration/
│   │   └── auth.spec.ts
│   ├── api/
│   │   └── users-api.spec.ts
│   └── e2e/
│       └── checkout.spec.ts
├── pages/                    # Page Object Models
│   ├── LoginPage.ts
│   └── DashboardPage.ts
├── fixtures/                 # Custom fixtures
│   └── base.fixture.ts
├── test-data/                # Test data files
│   └── users.json
├── utils/                    # Helper utilities
│   └── api-helper.ts
├── playwright.config.ts
├── .env
└── package.json
Enter fullscreen mode Exit fullscreen mode

Step 4: Configure 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 ? 4 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['list'],
    ['junit', { outputFile: 'results.xml' }],
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'api',
      testDir: './tests/api',
      use: {
        baseURL: process.env.API_BASE_URL || 'https://api.yourapp.com',
      },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

Unit & Integration Testing with Playwright

While Playwright is predominantly known for browser automation, it integrates seamlessly with component testing (via @playwright/experimental-ct-react for React) and works alongside Jest/Vitest for pure unit tests.

Integration Test Example: Authentication Flow

// tests/integration/auth.spec.ts
import { test, expect, request } from '@playwright/test';

test.describe('Authentication Integration', () => {
  let authToken: string;

  test.beforeAll(async () => {
    const apiContext = await request.newContext({
      baseURL: 'https://api.yourapp.com',
    });

    const response = await apiContext.post('/auth/login', {
      data: {
        username: 'testuser@example.com',
        password: 'SecurePass123!',
      },
    });

    expect(response.ok()).toBeTruthy();
    const body = await response.json();
    authToken = body.token;
  });

  test('should access protected endpoint with valid token', async ({ request }) => {
    const response = await request.get('/api/profile', {
      headers: { Authorization: `Bearer ${authToken}` },
    });
    expect(response.status()).toBe(200);
    const data = await response.json();
    expect(data).toHaveProperty('email');
  });

  test('should reject request without token', async ({ request }) => {
    const response = await request.get('/api/profile');
    expect(response.status()).toBe(401);
  });
});
Enter fullscreen mode Exit fullscreen mode

API Testing with Playwright

Playwright's APIRequestContext is a first-class citizen for REST and GraphQL testing. No need for external libraries like Supertest or Axios in your test suite.

REST API Testing

// tests/api/users-api.spec.ts
import { test, expect } from '@playwright/test';

const BASE_URL = 'https://jsonplaceholder.typicode.com';

test.describe('Users API', () => {

  test('GET /users - should return list of users', async ({ request }) => {
    const response = await request.get(`${BASE_URL}/users`);
    expect(response.status()).toBe(200);
    const users = await response.json();
    expect(Array.isArray(users)).toBeTruthy();
    expect(users[0]).toMatchObject({
      id: expect.any(Number),
      name: expect.any(String),
      email: expect.any(String),
    });
  });

  test('POST /users - should create a new user', async ({ request }) => {
    const newUser = { name: 'John Playwright', username: 'jplaywright', email: 'john@playwright.dev' };
    const response = await request.post(`${BASE_URL}/users`, { data: newUser });
    expect(response.status()).toBe(201);
    const created = await response.json();
    expect(created.name).toBe(newUser.name);
  });

  test('PUT /users/:id - should update user', async ({ request }) => {
    const response = await request.put(`${BASE_URL}/users/1`, { data: { name: 'Updated Name' } });
    expect(response.status()).toBe(200);
    const body = await response.json();
    expect(body.name).toBe('Updated Name');
  });

  test('DELETE /users/:id - should delete user', async ({ request }) => {
    const response = await request.delete(`${BASE_URL}/users/1`);
    expect(response.status()).toBe(200);
  });

});
Enter fullscreen mode Exit fullscreen mode

GraphQL API Testing

// tests/api/graphql.spec.ts
import { test, expect } from '@playwright/test';

test.describe('GraphQL API Tests', () => {

  test('should fetch user via GraphQL query', async ({ request }) => {
    const query = `
      query GetUser($id: ID!) {
        user(id: $id) { id name email posts { title } }
      }
    `;

    const response = await request.post('https://graphql.yourapp.com/graphql', {
      data: { query, variables: { id: '1' } },
      headers: { 'Content-Type': 'application/json' },
    });

    expect(response.ok()).toBeTruthy();
    const { data, errors } = await response.json();
    expect(errors).toBeUndefined();
    expect(data.user).toHaveProperty('name');
  });

});
Enter fullscreen mode Exit fullscreen mode

UI & End-to-End Testing

This is where Playwright truly shines. Smart auto-waits, network interception, and multi-browser support make complex user flows reliable and fast.

Basic UI Test

// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Login Flow', () => {

  test('should login successfully with valid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('user@example.com');
    await page.getByLabel('Password').fill('Password123!');
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page).toHaveURL('/dashboard');
    await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
  });

  test('should show error with invalid credentials', async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill('wrong@example.com');
    await page.getByLabel('Password').fill('WrongPass');
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page.getByText('Invalid email or password')).toBeVisible();
  });

});
Enter fullscreen mode Exit fullscreen mode

Network Interception & Mocking

test('should display mocked user data on dashboard', async ({ page }) => {
  await page.route('**/api/user/profile', async (route) => {
    await route.fulfill({
      status: 200,
      contentType: 'application/json',
      body: JSON.stringify({ name: 'Test User', email: 'test@example.com', plan: 'Pro' }),
    });
  });

  await page.goto('/dashboard');
  await expect(page.getByText('Test User')).toBeVisible();
  await expect(page.getByText('Pro')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

File Upload Testing

test('should upload a profile picture', async ({ page }) => {
  await page.goto('/settings/profile');
  const fileChooserPromise = page.waitForEvent('filechooser');
  await page.getByText('Upload Photo').click();
  const fileChooser = await fileChooserPromise;
  await fileChooser.setFiles('./test-data/avatar.png');
  await expect(page.getByText('Photo uploaded successfully')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

Mobile Emulation

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

test.use({ ...devices['iPhone 13'] });

test('should render mobile menu correctly', async ({ page }) => {
  await page.goto('/');
  await page.getByRole('button', { name: 'Menu' }).click();
  await expect(page.getByRole('navigation')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

Page Object Model (POM)

The Page Object Model keeps your tests clean and maintainable. Each page gets its own class encapsulating selectors and actions.

Creating a Page Object

// pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly signInButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.emailInput = page.getByLabel('Email');
    this.passwordInput = page.getByLabel('Password');
    this.signInButton = page.getByRole('button', { name: 'Sign In' });
    this.errorMessage = page.getByTestId('login-error');
  }

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

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

  async expectError(message: string) {
    await expect(this.errorMessage).toContainText(message);
  }
}
Enter fullscreen mode Exit fullscreen mode

Using POM in Tests

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

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

  test.beforeEach(async ({ page }) => {
    loginPage = new LoginPage(page);
    await loginPage.goto();
  });

  test('valid login redirects to dashboard', async ({ page }) => {
    await loginPage.login('user@example.com', 'Password123!');
    await expect(page).toHaveURL('/dashboard');
  });

  test('invalid login shows error', async () => {
    await loginPage.login('bad@email.com', 'wrongpass');
    await loginPage.expectError('Invalid email or password');
  });
});
Enter fullscreen mode Exit fullscreen mode

Test Data Management

Using JSON Fixtures

{
  "validUser": {
    "email": "testuser@example.com",
    "password": "ValidPass123!",
    "name": "Test User"
  },
  "adminUser": {
    "email": "admin@example.com",
    "password": "AdminPass456!",
    "name": "Admin User"
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom Fixtures with Playwright

// fixtures/base.fixture.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import users from '../test-data/users.json';

type MyFixtures = {
  loginPage: LoginPage;
  authenticatedPage: { page: any; token: string };
};

export const test = base.extend<MyFixtures>({
  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await use(loginPage);
  },

  authenticatedPage: async ({ page, request }, use) => {
    const response = await request.post('/auth/login', { data: users.validUser });
    const { token } = await response.json();
    await page.addInitScript((authToken) => {
      localStorage.setItem('authToken', authToken);
    }, token);
    await page.goto('/dashboard');
    await use({ page, token });
  },
});

export { expect } from '@playwright/test';
Enter fullscreen mode Exit fullscreen mode

CI/CD Integration

GitHub Actions Workflow

name: Playwright Tests

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

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run Playwright tests
        run: npx playwright test --shard=${{ matrix.shard }}/4
        env:
          BASE_URL: ${{ secrets.STAGING_URL }}
          API_BASE_URL: ${{ secrets.API_URL }}
      - name: Upload test report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report-shard-${{ matrix.shard }}
          path: playwright-report/
          retention-days: 30
Enter fullscreen mode Exit fullscreen mode

Reporting & Debugging

Built-in HTML Reporter

npx playwright test
npx playwright show-report
Enter fullscreen mode Exit fullscreen mode

Trace Viewer for Debugging

npx playwright test --trace on
npx playwright show-trace test-results/trace.zip
Enter fullscreen mode Exit fullscreen mode

Debug Mode (Step-by-step)

npx playwright test --debug
npx playwright test tests/e2e/login.spec.ts --debug
Enter fullscreen mode Exit fullscreen mode

Codegen (Auto-generate tests)

npx playwright codegen https://yourapp.com
Enter fullscreen mode Exit fullscreen mode

Best Practices

✅ DO's

  1. Use semantic locators — Prefer getByRole, getByLabel, getByTestId over CSS/XPath selectors
  2. Avoid hard-coded waits — Never use page.waitForTimeout(3000). Use waitForSelector, waitForResponse, or rely on Playwright's auto-wait
  3. Keep tests independent — Each test should set up its own state and not rely on others
  4. Use fixtures for shared setup/teardown logic
  5. Parametrize tests with test.each for data-driven scenarios
  6. Tag your tests with @smoke, @regression, @api for selective runs
  7. Mock external services to avoid flakiness from third-party dependencies
  8. Run in parallel — Playwright supports full parallelism; use shards for CI

❌ DON'Ts

  1. Don't use page.pause() in committed code — only use it during local debugging
  2. Don't test implementation details — test from the user's perspective
  3. Don't ignore flaky tests — investigate and fix them, don't just retry
  4. Don't commit .env files with real credentials
  5. Don't use generic locators like page.locator('div:nth-child(3)') — these break easily

Example: Data-Driven Test with test.each

const testCases = [
  { email: '', password: 'pass', error: 'Email is required' },
  { email: 'notanemail', password: 'pass', error: 'Invalid email format' },
  { email: 'user@ex.com', password: '', error: 'Password is required' },
];

testCases.forEach(({ email, password, error }) => {
  test(`shows "${error}" for email="${email}" password="${password}"`, async ({ page }) => {
    await page.goto('/login');
    await page.getByLabel('Email').fill(email);
    await page.getByLabel('Password').fill(password);
    await page.getByRole('button', { name: 'Sign In' }).click();
    await expect(page.getByText(error)).toBeVisible();
  });
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Playwright is more than just a UI automation tool — it's a complete full-stack testing platform. By combining API testing, integration testing, and end-to-end UI tests under one framework, you can:

  • Reduce tool sprawl (no need for separate Postman, Supertest, and Selenium setups)
  • Share authentication state across test layers
  • Ship faster with parallel execution and smart retries
  • Debug confidently with trace viewer and screenshots

Whether you're a solo developer or part of a large QA team, adopting Playwright across your full test stack will dramatically improve your test coverage, reliability, and developer experience.


Resources


Happy Testing! 🎭

Top comments (0)