Now that you’ve mastered all aspects of Playwright from basic automation to advanced framework design and CI integration it’s time to bring everything together in a real-world project example.
In this chapter, we’ll build a complete Playwright automation framework that includes:
- End-to-end tests (login, dashboard, API validation, file upload/download)
- Reusable fixtures and Page Object Model (POM)
- Reporting, CI configuration, and artifacts collection
This practical example mirrors how enterprise teams use Playwright in production.
1. Project Overview
Application Under Test (AUT):
For this example, we’ll test a web application “SauceStore” an e-commerce site with:
- Login page
- Dashboard displaying products
- API endpoint for user data
2. Project structure
Here’s the recommended project structure for our real-world Playwright framework:
/playwright-framework
┣ tests
┃ ┣ login.spec.js
┃ ┣ dashboard.spec.js
┃ ┣ api.spec.js
┣ pages
┃ ┣ LoginPage.js
┃ ┣ DashboardPage.js
┣ fixtures
┃ ┗ authFixture.js
┣ utils
┃ ┣ testData.js
┃ ┣ apiHelper.js
┃ ┗ config.js
┣ reports
┣ playwright.config.js
┣ package.json
┗ README.md
3. Configuration (playwright.config.js)
This is the heart of your Playwright framework managing browsers, retries, traces, and reports.
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30 * 1000,
retries: 1,
reporter: [
['list'],
['html', { outputFolder: 'playwright-report', open: 'never' }],
['junit', { outputFile: 'results.xml' }],
],
use: {
baseURL: 'https://saucedemo.com',
headless: true,
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
});
4. Utilities
utils/config.js
export const config = {
baseURL: 'https://www.saucedemo.com',
validUser: { username: 'standard_user', password: 'secret_sauce' },
invalidUser: { username: 'fake_user', password: 'wrong_pass' },
};
utils/testData.js
export const testData = {
products: ['Sauce Labs Backpack', 'Sauce Labs Bike Light'],
uploadFile: 'test-data/sample.txt',
};
utils/apiHelper.js
import { request } from '@playwright/test';
export async function getUserData() {
const context = await request.newContext();
const response = await context.get('https://jsonplaceholder.typicode.com/users/1');
return await response.json();
}
5. Page Objects
pages/LoginPage.js
export class LoginPage {
constructor(page) {
this.page = page;
this.usernameInput = page.locator('#user-name');
this.passwordInput = page.locator('#password');
this.loginButton = page.locator('#login-button');
this.errorMessage = page.locator('[data-test="error"]');
}
async goto() {
await this.page.goto('/');
}
async login(username, password) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
pages/DashboardPage.js
export class DashboardPage {
constructor(page) {
this.page = page;
this.productTitles = page.locator('.inventory_item_name');
this.cartButton = page.locator('.shopping_cart_link');
}
async verifyProducts(expectedProducts) {
const productCount = await this.productTitles.count();
const productNames = [];
for (let i = 0; i < productCount; i++) {
productNames.push(await this.productTitles.nth(i).innerText());
}
expectedProducts.forEach(product => expect(productNames).toContain(product));
}
}
6. Fixtures
fixtures/authFixture.js
import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
export const test = base.extend({
loggedInPage: async ({ browser }, use) => {
const context = await browser.newContext();
const page = await context.newPage();
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('standard_user', 'secret_sauce');
await use(page);
await context.close();
},
});
This fixture logs in once and shares the authenticated session for all tests using loggedInPage.
7. Test Cases
tests/login.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { config } from '../utils/config';
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(config.validUser.username, config.validUser.password);
await expect(page).toHaveURL(/inventory/);
});
test('failed login with invalid credentials', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login(config.invalidUser.username, config.invalidUser.password);
await expect(loginPage.errorMessage).toBeVisible();
});
tests/dashboard.spec.js
import { test, expect } from '../fixtures/authFixture';
import { DashboardPage } from '../pages/DashboardPage';
import { testData } from '../utils/testData';
test('verify product list on dashboard', async ({ loggedInPage }) => {
const dashboard = new DashboardPage(loggedInPage);
await dashboard.verifyProducts(testData.products);
});
tests/api.spec.js
import { test, expect } from '@playwright/test';
import { getUserData } from '../utils/apiHelper';
test('validate API user data', async () => {
const user = await getUserData();
expect(user).toHaveProperty('username');
expect(user.id).toBe(1);
});
8. CI/CD Integration
Here’s a sample GitHub Actions workflow for running this project.
.github/workflows/playwright.yml
name: Playwright Framework CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: 18
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test --reporter=html
- name: Upload Playwright HTML Report
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
9. Running the project locally
To execute all tests locally:
npx playwright test
To run a specific test file:
npx playwright test tests/login.spec.js
To generate and open the HTML report:
npx playwright show-report
10. Debugging and artifacts
Enable traces, screenshots, and videos for failed tests.
export default {
use: {
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'retain-on-failure',
},
};
After the run, use:
npx playwright show-trace trace.zip
This opens an interactive timeline showing DOM snapshots, network calls, and console logs.
Key Takeaways
- Use POM + Fixtures for maintainability.
- Keep config centralized in
playwright.config.js. - Store test data and utilities separately.
- Integrate with CI/CD pipelines for continuous testing.
- Collect and review artifacts for debugging.
- Modularize your framework to add new features easily.
12. Exercise
Task 1: Add a test that verifies the shopping cart functionality.
Task 2: Extend apiHelper.js to perform POST and DELETE operations.
Task 3: Add parallel browser execution in your config.
Task 4: Integrate Slack notifications for failed CI tests.
Bonus: Deploy this project to GitHub and set up scheduled nightly test runs.
Summary
In this final, real-world project, you built a complete, production-grade Playwright framework with:
- Modular Page Objects
- Fixtures and utilities
- API and UI integration tests
- File upload/download handling
- CI/CD integration and reporting
Congratulations! You’ve reached the end of Practical Playwright Automation with JavaScript a complete, hands-on journey from Playwright basics to advanced, production-level automation.
By now, you’ve not only learned how to use Playwright but also how to design, optimize, and deploy a full-featured, real-world automation framework.
