Back to catalog

playwright-helpers

Using helper functions, page objects, and fixtures for maintainable Playwright tests

  • Category: discovered
  • Tags: github, community
  • Version: 1.0.0
  • Author: walid
  • Downloads: 0
  • GitHub stars: 1
  • Updated: 2025-11-21

Download the latest version (v1.0.0) from this page.

SKILL.md

---
name: playwright-helpers
description: Using helper functions, page objects, and fixtures for maintainable Playwright tests
---

# Playwright Helpers and Patterns

This guide covers reusable patterns, helper functions, page objects, and fixtures for writing maintainable Playwright tests.

## Helper Functions

Helper functions are located in `playwright_tests/tests/support/`.

### Authentication Helpers

Located in `tests/support/auth-helpers.js`:

```javascript
const { loginAsTestUser, login, logout, register, generateTestEmail } = require('../support/auth-helpers');

// Login with test user from environment
test('my test', async ({ page }) => {
  await loginAsTestUser(page);
  // User is now logged in
});

// Login with specific credentials
await login(page, 'user@example.com', 'password');

// Logout
await logout(page);

// Register new user
const email = generateTestEmail('testuser');
await register(page, email, 'Password123!');

// Generate unique email
const uniqueEmail = generateTestEmail(); // testuser1699999999@example.com
```

### Screenshot Helpers

**IMPORTANT**: All screenshots must be 1080p (1920x1080) viewport only.
**IMPORTANT**: Use the centralized screenshot configuration helper for all tests.

```javascript
const { createScreenshotHelper } = require('../../../../screenshot-config');

test.describe('My Tests', () => {
  const screenshot = createScreenshotHelper(__dirname);

  test('my test', async ({ page }) => {
    await page.goto('/dashboard');
    await screenshot(page, '01-dashboard');

    await page.click('[data-testid="create-button"]');
    await screenshot(page, '02-create-dialog');
  });
});
```

**Screenshot Rules:**
- ✅ Always use `createScreenshotHelper(__dirname)`
- ✅ Screenshots automatically saved to correct location based on test path
- ✅ Works at any nesting depth
- ✅ All screenshots are 1920x1080 viewport only (fullPage: false)
- ✅ Use descriptive names with step numbers (e.g., `01-sign-in`, `02-after-login`)

**For theme variants (light/dark mode):**

```javascript
const { createScreenshotHelper } = require('../../../../screenshot-config');

test.describe('My Tests', () => {
  const screenshot = createScreenshotHelper(__dirname);

  async function captureThemeScreenshots(page, name) {
    // Light mode
    await page.evaluate(() => {
      document.documentElement.classList.remove('dark');
      localStorage.setItem('theme', 'light');
    });
    await page.waitForTimeout(300);
    await screenshot(page, `${name}-light`);

    // Dark mode
    await page.evaluate(() => {
      document.documentElement.classList.add('dark');
      localStorage.setItem('theme', 'dark');
    });
    await page.waitForTimeout(300);
    await screenshot(page, `${name}-dark`);
  }

  test('dashboard theme test', async ({ page }) => {
    await page.goto('/dashboard');
    await captureThemeScreenshots(page, '01-dashboard');
  });
});
```

### General Test Helpers

Located in `tests/support/test-helpers.js`:

```javascript
const {
  waitForElement,
  fillForm,
  waitForNetworkIdle,
  elementExists,
  clickWithRetry,
  getTextContent,
  waitForURL,
  performSearch,
} = require('../support/test-helpers');

// Wait for element to be visible
await waitForElement(page, '.result', 10000);

// Fill multiple form fields
await fillForm(page, {
  'input[name="name"]': 'John Doe',
  'input[name="email"]': 'john@example.com',
  'input[name="phone"]': '555-1234',
});

// Wait for network to be idle
await waitForNetworkIdle(page);

// Check if element exists (doesn't have to be visible)
const exists = await elementExists(page, '.optional-element');

// Click with retry logic
await clickWithRetry(page, '.sometimes-hidden-button', 3);

// Get text content
const heading = await getTextContent(page, 'h1');

// Wait for specific URL
await waitForURL(page, /.*dashboard/, 10000);

// Perform search
await performSearch(page, 'contact name');
```

## Page Object Model (POM)

Page Objects encapsulate page-specific logic and selectors, making tests more maintainable.

### Creating a Page Object

Located in `tests/support/pages/`:

```javascript
// tests/support/pages/contacts-page.js
class ContactsPage {
  constructor(page) {
    this.page = page;

    // Selectors
    this.addButton = '[data-test="add-contact"]';
    this.searchInput = 'input[type="search"]';
    this.contactsList = '.contacts-list';
    this.contactItem = '.contact-item';
  }

  async goto() {
    await this.page.goto('/contacts');
    await this.page.waitForLoadState('networkidle');
  }

  async addContact(name, email, phone) {
    await this.page.click(this.addButton);
    await this.page.fill('input[name="name"]', name);
    await this.page.fill('input[name="email"]', email);
    await this.page.fill('input[name="phone"]', phone);
    await this.page.click('button[type="submit"]');
    await this.page.waitForLoadState('networkidle');
  }

  async searchContacts(query) {
    await this.page.fill(this.searchInput, query);
    await this.page.keyboard.press('Enter');
    await this.page.waitForLoadState('networkidle');
  }

  async getContactCount() {
    return await this.page.locator(this.contactItem).count();
  }

  async clickContact(name) {
    await this.page.click(`${this.contactItem} >> text=${name}`);
  }

  async isContactVisible(name) {
    return await this.page.locator(`text=${name}`).isVisible();
  }
}

module.exports = { ContactsPage };
```

### Using Page Objects in Tests

```javascript
const { test, expect } = require('@playwright/test');
const { ContactsPage } = require('../support/pages/contacts-page');
const { loginAsTestUser } = require('../support/auth-helpers');

test.describe('Contact Management', () => {

  test('should create a new contact', async ({ page }) => {
    // Given: I am logged in and on the contacts page
    await loginAsTestUser(page);
    const contactsPage = new ContactsPage(page);
    await contactsPage.goto();

    // When: I add a new contact
    await contactsPage.addContact('Jane Doe', 'jane@example.com', '555-5678');

    // Then: The contact should be visible
    const isVisible = await contactsPage.isContactVisible('Jane Doe');
    expect(isVisible).toBeTruthy();
  });

  test('should search for contacts', async ({ page }) => {
    // Given: I am on the contacts page with existing contacts
    await loginAsTestUser(page);
    const contactsPage = new ContactsPage(page);
    await contactsPage.goto();

    // When: I search for a contact
    await contactsPage.searchContacts('Jane');

    // Then: Search results should be displayed
    const count = await contactsPage.getContactCount();
    expect(count).toBeGreaterThan(0);
  });
});
```

## Page Object Best Practices

1. **Encapsulate selectors**: Store all selectors in the page object
2. **Encapsulate interactions**: Create methods for common actions
3. **Return page objects**: For method chaining
4. **Keep it focused**: One page object per page/component
5. **Use descriptive names**: Method names should explain what they do

### Advanced Page Object Example

```javascript
class DashboardPage {
  constructor(page) {
    this.page = page;
    this.header = page.locator('header');
    this.navigation = page.locator('nav');
    this.logoutButton = page.locator('[data-test="logout"]');
  }

  async goto() {
    await this.page.goto('/dashboard');
    return this; // Enable chaining
  }

  async logout() {
    await this.logoutButton.click();
    await this.page.waitForURL(/.*sign-in/);
    return this; // Enable chaining
  }

  async navigateTo(section) {
    await this.page.click(`nav >> text=${section}`);
    await this.page.waitForLoadState('networkidle');
    return this; // Enable chaining
  }

  async getWelcomeMessage() {
    return await this.page.locator('.welcome-message').textContent();
  }
}

// Usage with chaining
await new DashboardPage(page)
  .goto()
  .navigateTo('Contacts');
```

## Custom Fixtures

Fixtures provide reusable test setups. Located in `tests/fixtures/`.

### Authenticated Fixture

Located in `tests/fixtures/authenticated.js`:

```javascript
const { test } = require('../fixtures/authenticated');
const { expect } = require('@playwright/test');

test.describe('Authenticated Tests', () => {

  test('should access dashboard', async ({ page }) => {
    // User is already logged in
    await page.goto('/dashboard');
    await expect(page.locator('h1')).toContainText('Dashboard');
  });
});
```

### Creating Custom Fixtures

```javascript
// tests/fixtures/with-test-data.js
const { test as base } = require('@playwright/test');
const { loginAsTestUser } = require('../support/auth-helpers');

const test = base.extend({
  // Authenticated page with test data
  authenticatedPage: async ({ page }, use) => {
    // Setup: Login and create test data
    await loginAsTestUser(page);

    // Create test contact
    await page.goto('/contacts');
    await page.click('[data-test="add-contact"]');
    await page.fill('input[name="name"]', 'Test Contact');
    await page.fill('input[name="email"]', 'test@example.com');
    await page.click('button[type="submit"]');

    // Use the page
    await use(page);

    // Cleanup: Delete test data
    await page.goto('/contacts');
    await page.click('[data-test="contact-test-contact"]');
    await page.click('[data-test="delete-contact"]');
    await page.click('[data-test="confirm-delete"]');
  },
});

module.exports = { test };
```

### Using Custom Fixtures

```javascript
const { test } = require('../fixtures/with-test-data');
const { expect } = require('@playwright/test');

test('should find test contact', async ({ authenticatedPage }) => {
  await authenticatedPage.goto('/contacts');
  await expect(authenticatedPage.locator('text=Test Contact')).toBeVisible();
});
```

## Component Fixtures

For reusable page components:

```javascript
const { test as base } = require('@playwright/test');
const { LoginPage } = require('../support/pages/login-page');
const { ContactsPage } = require('../support/pages/contacts-page');

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

  contactsPage: async ({ page }, use) => {
    await use(new ContactsPage(page));
  },
});

// Usage
test('my test', async ({ loginPage, contactsPage }) => {
  await loginPage.login('user@example.com', 'password');
  await contactsPage.goto();
  await contactsPage.addContact('Name', 'email@example.com', '555-1234');
});
```

## Organizing Helpers

### Directory Structure

```
tests/support/
├── auth-helpers.js          # Authentication operations
├── test-helpers.js          # General utilities
├── data-helpers.js          # Test data generation
├── api-helpers.js           # API interactions
└── pages/                   # Page Object Models
    ├── login-page.js
    ├── dashboard-page.js
    ├── contacts-page.js
    └── deals-page.js
```

### Creating Helper Modules

```javascript
// tests/support/data-helpers.js
function generateContact(overrides = {}) {
  const timestamp = Date.now();
  return {
    name: `Test Contact ${timestamp}`,
    email: `contact${timestamp}@example.com`,
    phone: `555-${timestamp.toString().slice(-4)}`,
    ...overrides,
  };
}

function generateDeal(overrides = {}) {
  const timestamp = Date.now();
  return {
    title: `Deal ${timestamp}`,
    value: Math.floor(Math.random() * 100000),
    stage: 'prospecting',
    ...overrides,
  };
}

module.exports = { generateContact, generateDeal };
```

### Using Helper Modules

```javascript
const { generateContact } = require('../support/data-helpers');
const { fillForm } = require('../support/test-helpers');

test('create contact with generated data', async ({ page }) => {
  const contact = generateContact({ name: 'Specific Name' });

  await page.goto('/contacts/new');
  await fillForm(page, {
    'input[name="name"]': contact.name,
    'input[name="email"]': contact.email,
    'input[name="phone"]': contact.phone,
  });
  await page.click('button[type="submit"]');

  await expect(page.locator(`text=${contact.name}`)).toBeVisible();
});
```

## API Helpers

For API interactions during tests:

```javascript
// tests/support/api-helpers.js
async function createContactViaAPI(request, contactData) {
  const response = await request.post('/api/contacts', {
    data: contactData,
    headers: {
      'Content-Type': 'application/json',
    },
  });
  return await response.json();
}

async function deleteContactViaAPI(request, contactId) {
  await request.delete(`/api/contacts/${contactId}`);
}

module.exports = { createContactViaAPI, deleteContactViaAPI };
```

### Using API Helpers

```javascript
const { test, expect } = require('@playwright/test');
const { createContactViaAPI } = require('../support/api-helpers');

test('verify contact created via API appears in UI', async ({ page, request }) => {
  // Given: A contact exists (created via API)
  const contact = await createContactViaAPI(request, {
    name: 'API Contact',
    email: 'api@example.com',
  });

  // When: I view the contacts page
  await page.goto('/contacts');

  // Then: The contact should be visible
  await expect(page.locator('text=API Contact')).toBeVisible();
});
```

## Combining Patterns

```javascript
const { test } = require('../fixtures/authenticated');
const { expect } = require('@playwright/test');
const { ContactsPage } = require('../support/pages/contacts-page');
const { generateContact } = require('../support/data-helpers');
const { waitForNetworkIdle } = require('../support/test-helpers');

test.describe('Complete Contact Workflow', () => {

  test('should create, edit, and delete contact', async ({ page }) => {
    // Setup: Page object and test data
    const contactsPage = new ContactsPage(page);
    const contact = generateContact();

    // Given: I am on the contacts page (authenticated by fixture)
    await contactsPage.goto();

    // When: I create a contact
    await contactsPage.addContact(contact.name, contact.email, contact.phone);
    await waitForNetworkIdle(page);

    // Then: The contact should be visible
    expect(await contactsPage.isContactVisible(contact.name)).toBeTruthy();

    // When: I edit the contact
    await contactsPage.clickContact(contact.name);
    await page.click('[data-test="edit-contact"]');
    await page.fill('input[name="phone"]', '555-9999');
    await page.click('button[type="submit"]');

    // Then: The changes should be saved
    await expect(page.locator('text=555-9999')).toBeVisible();

    // Cleanup: Delete the contact
    await page.click('[data-test="delete-contact"]');
    await page.click('[data-test="confirm-delete"]');
    await waitForNetworkIdle(page);

    // Verify deletion
    expect(await contactsPage.isContactVisible(contact.name)).toBeFalsy();
  });
});
```

## Tips

1. **Start simple**: Use helpers before creating page objects
2. **Refactor gradually**: Convert repetitive code to helpers/page objects
3. **Keep helpers focused**: One responsibility per helper function
4. **Document complex helpers**: Add JSDoc comments
5. **Test helpers independently**: Ensure they work correctly
6. **Avoid over-abstraction**: Don't create helpers for one-off operations

## Resources

- Helper examples: `playwright_tests/tests/support/`
- Page object examples: `playwright_tests/tests/support/pages/`
- Fixture examples: `playwright_tests/tests/fixtures/`
- Usage examples: `playwright_tests/tests/examples/`