๐ญ Test Organization: Fixtures & Page Objects in Playwright
Imagine youโre a chef preparing a big feast. Before cooking, you set up your workstation: clean pots, sharp knives, fresh ingredients all ready. Thatโs what fixtures doโthey prepare everything your tests need before they run!
๐ช The Big Picture: What Are Fixtures?
Think of fixtures like a magical helper that sets up your playground before you play.
Without fixtures: Every time you want to play, you have to:
- Find your toys
- Set them up
- Clean up after
With fixtures: Your magical helper does all this for you automatically!
graph TD A["๐ฌ Test Starts"] --> B["๐ง Fixture Sets Up"] B --> C["๐ฎ Test Runs"] C --> D["๐งน Fixture Cleans Up"] D --> E["โ Test Complete"]
Why Use Fixtures?
| Without Fixtures ๐ซ | With Fixtures ๐ |
|---|---|
| Repeat setup code everywhere | Write setup once |
| Forget to clean up | Auto cleanup |
| Tests depend on each other | Each test is fresh |
| Hard to maintain | Easy to change |
๐ Built-in Page Fixture
The page fixture is like getting a fresh browser tab handed to you.
Real Life Example:
- Opening a new tab in your browser = getting a
page - Each tab is separate and clean
- You can click, type, and see things on it
// Playwright gives you 'page' automatically!
test('visit website', async ({ page }) => {
// 'page' is already ready to use
await page.goto('https://example.com');
// Click a button
await page.click('button');
// Type in a box
await page.fill('input', 'Hello!');
});
What Can page Do?
graph TD P["๐ page"] --> A["๐ goto - visit websites"] P --> B["๐ click - click things"] P --> C["โจ๏ธ fill - type text"] P --> D["๐ locator - find elements"] P --> E["๐ธ screenshot - take pictures"]
Simple Example:
test('login test', async ({ page }) => {
await page.goto('/login');
await page.fill('#username', 'player1');
await page.fill('#password', 'secret');
await page.click('#submit');
// Check we logged in!
await expect(page).toHaveURL('/dashboard');
});
๐ Built-in Context Fixture
The context is like a private browser window (incognito mode).
Think of it this way:
context= A private browser windowpage= A tab inside that window- Each context is isolatedโcookies donโt mix!
test('fresh cookies each time', async ({ context }) => {
// Create a new tab in this context
const page = await context.newPage();
await page.goto('https://shop.com');
// Any cookies set here stay in THIS context
// Other tests won't see them!
});
When Do You Need Context?
| Situation | Use Context? |
|---|---|
| Test login as different users | โ Yes |
| Test with specific cookies | โ Yes |
| Simple page navigation | โ Just use page |
| Test multiple tabs at once | โ Yes |
test('two users chatting', async ({ context }) => {
// Create two tabs - like two people
const alice = await context.newPage();
const bob = await context.newPage();
// Alice sends a message
await alice.goto('/chat');
await alice.fill('#message', 'Hi Bob!');
await alice.click('#send');
// Bob receives it
await bob.goto('/chat');
await expect(bob.locator('.message'))
.toContainText('Hi Bob!');
});
๐ Built-in Browser Fixture
The browser fixture is the whole browser itselfโChrome, Firefox, or Safari.
Analogy:
browser= The entire browser appcontext= A window in that apppage= A tab in that window
graph TD B["๐ Browser"] --> C1["๐ช Context 1"] B --> C2["๐ช Context 2"] C1 --> P1["๐ Page A"] C1 --> P2["๐ Page B"] C2 --> P3["๐ Page C"]
test('test with browser control', async ({ browser }) => {
// Create a fresh incognito-like context
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
// Clean up when done
await context.close();
});
Why Use Browser Directly?
You need browser when you want:
- Multiple isolated sessions (different users)
- Custom context settings (viewport, locale)
- Fine control over browser resources
test('mobile vs desktop view', async ({ browser }) => {
// Desktop user
const desktop = await browser.newContext({
viewport: { width: 1920, height: 1080 }
});
// Mobile user
const mobile = await browser.newContext({
viewport: { width: 375, height: 667 }
});
const desktopPage = await desktop.newPage();
const mobilePage = await mobile.newPage();
// Test both views!
});
๐ Built-in Request Fixture
The request fixture lets you talk to APIs without a browser.
Like ordering food:
- Browser = Going to restaurant, sitting down
- Request = Calling for delivery directly
test('check API works', async ({ request }) => {
// Call the API directly
const response = await request.get('/api/users');
// Check response
expect(response.ok()).toBeTruthy();
const data = await response.json();
expect(data.users).toHaveLength(5);
});
When to Use Request vs Page?
Use request |
Use page |
|---|---|
| Test API endpoints | Test user interface |
| Fast backend checks | Visual testing |
| No UI needed | Need to click/type |
| Data validation | Screenshot tests |
test('create user via API', async ({ request }) => {
// POST request - create something
const response = await request.post('/api/users', {
data: {
name: 'Alex',
email: 'alex@test.com'
}
});
expect(response.status()).toBe(201);
const user = await response.json();
expect(user.name).toBe('Alex');
});
๐๏ธ Page Object Model Pattern
The Problem: Your tests are messy with selectors everywhere.
The Solution: Page Objectsโlike creating a remote control for each page!
graph LR T["๐งช Test"] --> R["๐ฎ Remote Control"] R --> P["๐ Web Page"] style R fill:#f9f,stroke:#333
Before Page Objects (Messy) ๐ต
test('login test', async ({ page }) => {
await page.goto('/login');
await page.fill('#email-input', 'user@test.com');
await page.fill('#password-input', 'pass123');
await page.click('#login-button');
await expect(page.locator('.welcome-msg'))
.toBeVisible();
});
test('another login test', async ({ page }) => {
// Same selectors repeated! ๐ซ
await page.fill('#email-input', 'other@test.com');
// ...
});
After Page Objects (Clean) ๐
test('login test', async ({ loginPage }) => {
await loginPage.login('user@test.com', 'pass123');
await loginPage.expectSuccess();
});
Why is this better?
- Change once, fix everywhere - selector changed? Update one file
- Readable tests - reads like a story
- Reusable - use login in many tests
๐ Creating Page Classes
A page class is like writing a user manual for a webpage.
Step 1: Create the Class
// pages/login-page.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
// Store the page
readonly page: Page;
// Store elements (like bookmarks)
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
// Define where things are
this.emailInput = page.locator('#email');
this.passwordInput = page.locator('#password');
this.submitButton = page.locator('#submit');
this.errorMessage = page.locator('.error');
}
}
Step 2: Add Actions (Methods)
export class LoginPage {
// ... constructor from above ...
// Action: Go to login page
async goto() {
await this.page.goto('/login');
}
// Action: Fill and submit login
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
// Check: Verify error shows
async expectError(message: string) {
await expect(this.errorMessage)
.toContainText(message);
}
}
Step 3: Use in Tests
import { LoginPage } from './pages/login-page';
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@test.com', 'secret');
// Test continues...
});
๐ง Page Objects with Fixtures
The ultimate combo: Create custom fixtures that give you ready-made page objects!
Step 1: Define Your Fixture
// fixtures.ts
import { test as base } from '@playwright/test';
import { LoginPage } from './pages/login-page';
import { HomePage } from './pages/home-page';
// Describe what fixtures you're adding
type MyFixtures = {
loginPage: LoginPage;
homePage: HomePage;
};
// Extend the base test with your fixtures
export const test = base.extend<MyFixtures>({
loginPage: async ({ page }, use) => {
// Create the page object
const loginPage = new LoginPage(page);
// Navigate to it
await loginPage.goto();
// Hand it to the test
await use(loginPage);
},
homePage: async ({ page }, use) => {
const homePage = new HomePage(page);
await use(homePage);
},
});
Step 2: Use Your Custom Fixtures
// my-test.spec.ts
import { test } from './fixtures';
test('login works', async ({ loginPage }) => {
// loginPage is ready to use!
// Already navigated to login page!
await loginPage.login('user@test.com', 'pass123');
await loginPage.expectSuccess();
});
test('home page loads', async ({ homePage }) => {
await homePage.expectWelcomeVisible();
});
The Magic Flow
graph TD A["๐ฌ Test Starts"] --> B["๐ง Fixture Runs"] B --> C["๐ฆ Creates LoginPage"] C --> D["๐ Navigates to /login"] D --> E["๐ Gives to Test"] E --> F["๐งช Test Runs"] F --> G["๐งน Auto Cleanup"]
Complete Example
// fixtures.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './pages/login-page';
import { DashboardPage } from './pages/dashboard-page';
type Fixtures = {
loginPage: LoginPage;
dashboardPage: DashboardPage;
loggedInPage: DashboardPage;
};
export const test = base.extend<Fixtures>({
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await use(loginPage);
},
dashboardPage: async ({ page }, use) => {
await use(new DashboardPage(page));
},
// A fixture that depends on another!
loggedInPage: async ({ loginPage, dashboardPage }, use) => {
// Use loginPage to log in first
await loginPage.login('test@test.com', 'password');
// Then give the dashboard
await use(dashboardPage);
},
});
export { expect };
// dashboard.spec.ts
import { test, expect } from './fixtures';
test('see dashboard after login', async ({ loggedInPage }) => {
// Already logged in thanks to fixture!
await expect(loggedInPage.welcomeMessage)
.toBeVisible();
});
๐ฏ Summary: Your New Superpowers
| Fixture | What It Gives You | Use When |
|---|---|---|
page |
A fresh browser tab | Most tests |
context |
Isolated browser window | Multiple users/sessions |
browser |
Full browser control | Custom viewports |
request |
Direct API access | Backend testing |
| Custom | Your page objects | Clean, reusable tests |
The Fixture Family Tree
graph TD B["๐ browser"] --> C["๐ช context"] C --> P["๐ page"] P --> PO["๐ฎ Page Objects"] R["๐ request"] --> API["๐ก API Testing"]
๐ You Did It!
You now understand:
- โ What fixtures are (magical setup helpers)
- โ Built-in fixtures (page, context, browser, request)
- โ Page Object Model (remote controls for pages)
- โ Creating page classes (user manuals for pages)
- โ Combining page objects with fixtures (the ultimate power!)
Remember: Fixtures = Less repeated code, cleaner tests, happier you! ๐
