Fixtures and Page Objects

Back

Loading concept...

๐ŸŽญ 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:

  1. Find your toys
  2. Set them up
  3. 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 window
  • page = 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 app
  • context = A window in that app
  • page = 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?

  1. Change once, fix everywhere - selector changed? Update one file
  2. Readable tests - reads like a story
  3. 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! ๐ŸŽ‰

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.