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
- Why Playwright?
- Installation & Project Setup
- Unit & Integration Testing with Playwright
- API Testing with Playwright
- UI & End-to-End Testing
- Page Object Model (POM)
- Test Data Management
- CI/CD Integration
- Reporting & Debugging
- Best Practices
- 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
Step 2: Install Playwright
npm init playwright@latest
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
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',
},
},
],
});
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);
});
});
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);
});
});
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');
});
});
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();
});
});
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();
});
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();
});
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();
});
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);
}
}
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');
});
});
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"
}
}
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';
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
Reporting & Debugging
Built-in HTML Reporter
npx playwright test
npx playwright show-report
Trace Viewer for Debugging
npx playwright test --trace on
npx playwright show-trace test-results/trace.zip
Debug Mode (Step-by-step)
npx playwright test --debug
npx playwright test tests/e2e/login.spec.ts --debug
Codegen (Auto-generate tests)
npx playwright codegen https://yourapp.com
Best Practices
✅ DO's
-
Use semantic locators — Prefer
getByRole,getByLabel,getByTestIdover CSS/XPath selectors -
Avoid hard-coded waits — Never use
page.waitForTimeout(3000). UsewaitForSelector,waitForResponse, or rely on Playwright's auto-wait - Keep tests independent — Each test should set up its own state and not rely on others
- Use fixtures for shared setup/teardown logic
-
Parametrize tests with
test.eachfor data-driven scenarios -
Tag your tests with
@smoke,@regression,@apifor selective runs - Mock external services to avoid flakiness from third-party dependencies
- Run in parallel — Playwright supports full parallelism; use shards for CI
❌ DON'Ts
- Don't use
page.pause()in committed code — only use it during local debugging - Don't test implementation details — test from the user's perspective
- Don't ignore flaky tests — investigate and fix them, don't just retry
- Don't commit
.envfiles with real credentials - 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();
});
});
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
- 📖 Playwright Official Docs
- 🛠️ Playwright GitHub
- 🎥 Playwright YouTube Channel
- 🧪 Playwright Test Examples
- 💬 Playwright Discord Community
Happy Testing! 🎭
Top comments (0)