Skip to the content.

Playwright-elements

Awesome

Playwright-elements is a powerful testing framework that extends Playwright with:


Installation

npm install -D playwright-elements

For projects that only need test ID generation without Playwright:

npm install -D @playwright-elements/testids

Quick Start

import { $, test } from 'playwright-elements';

const loginForm = $('.login-form').with({
  username: $('input[name="username"]'),
  password: $('input[name="password"]'),
  submit: $('button[type="submit"]'),
  async login(user: string, pass: string) {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.submit.click();
  }
});

test('user login', async ({ goto }) => {
  await goto('/login');
  await loginForm.login('admin', 'password123');
  await loginForm.submit.expect().toBeVisible();
});

🏗️ Core Concepts

Component Hierarchy

Create nested component structures that mirror your application’s DOM:

// Multi-level component structure
const app = $('.app').with({
  header: $('.header').with({
    logo: $('.logo'),
    navigation: $('.nav').with({
      items: $('.nav-item')
    })
  }),
  main: $('.main-content'),
  footer: $('.footer')
});

// Usage in tests
await app.header.navigation.items.first().click();

Type-Safe Test IDs

Prevent selector typos and ensure type safety across your entire test suite using branded types:

import { factory, sid, bareFactory, $byTestId, testIdProps, assertNoPrefixCollisions } from 'playwright-elements';

// Define typed IDs with branded types for compile-time safety
const ids = {
  login: {
    username: sid<'login.username'>('username-input'),
    password: sid<'login.password'>('password-input'),
  },
  button: factory<'button'>('btn')
} as const;

// Validate no prefix collisions at module load time
assertNoPrefixCollisions(ids);

// Use in React/Vue/Angular components
function MyComponent() {
  return <input {...testIdProps(ids.login.username)} />;
}

// Use in tests
const usernameField = $byTestId(ids.login.username);
await usernameField.fill('admin');

// bareFactory for entity-based IDs (no prefix)
const ruleRow = bareFactory<'rules.row'>();
// Usage: ruleRow(123) -> TestId<'rules.row'> with value '123'

Benefits of Branded Types:

Zero-dependency import: For projects that only need the test IDs module without Playwright dependencies:

Option 1 — Standalone package (recommended for unit tests and production code):

npm install -D @playwright-elements/testids
import { sid, factory, bareFactory, testIdProps } from '@playwright-elements/testids';

This installs a separate package with zero runtime dependencies — no Playwright, no lodash in node_modules.

Option 2 — Subpath re-export (backward compatible):

import { sid, factory, bareFactory, testIdProps } from 'playwright-elements/testids';

This has zero import-time dependencies (loads no Playwright at runtime) but still installs the full playwright-elements package.


Advanced Component Methods

The .with() method supports both elements and methods for complete component encapsulation:

const loginForm = $('.login-form').with({
  // Child elements
  username: $('input[name="username"]'),
  password: $('input[name="password"]'),
  submit: $('button[type="submit"]'),
  
  // Methods for component actions
  async login(user: string, pass: string) {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.submit.click();
  }
});

// Usage
await loginForm.login('admin', 'password123');

Browser and Page Management

Access the current page and context through BrowserInstance:

import { BrowserInstance, $, usePage, test } from 'playwright-elements';

// Get the current page
const currentPage = BrowserInstance.currentPage;

// Check if running in mobile context (uses Playwright's public isMobile fixture)
if (BrowserInstance.isContextMobile) {
  // Mobile-specific logic
}

// Use with custom pages - execute code in specific page context
const result = await usePage(customPage, async () => {
  // All playwright-elements operations here use the provided page
  const element = $('.my-element');
  await element.click();
  return await element.textContent();
});

// In Playwright Test, isContextMobile is automatically set from the test fixture
test.use({ ...devices['iPhone 13'] });
test('mobile test', async ({}) => {
  // BrowserInstance.isContextMobile will be true
});

Assertion Configuration

WebElement provides seamless integration with @playwright/test assertions. No configuration needed!

Automatic Setup

When importing from 'playwright-elements', assertions work automatically:

import { test } from 'playwright-elements';

test('my test', async ({}) => {
  // Works automatically
  await $('.element').expect().toBeVisible();
  await $('.element').softExpect().toBeVisible();
});

Custom Matchers

Extend Playwright’s expect with custom matchers - they will work automatically with WebElement:

import { expect } from '@playwright/test';
import { test } from 'playwright-elements';
import { $ } from 'playwright-elements';

// Extend expect with custom matcher
expect.extend({
  async toHaveCustomValue(locator, expected) {
    const actual = await locator.getAttribute('data-custom');
    return {
      pass: actual === expected,
      message: () => `Expected custom value to be ${expected}, but got ${actual}`
    };
  }
});

test('custom matcher', async ({ goto }) => {
  await goto('/');
  // Custom matcher works with full TypeScript autocomplete
  await $('.element').expect().toHaveCustomValue('test');
});

How it works: WebElement.expect() directly uses @playwright/test’s expect, so any matchers added via expect.extend() are automatically available.


Advanced Selectors

Filter elements by text and other criteria:

// Filter by text content
const items = $('.item').filter({ hasText: 'Product' });

// Get all matching elements
const allButtons = $('.button').getAll();

// Chain filters
const visibleProducts = $('.product')
  .filter({ hasText: /Product/ })
  .filter({ isVisible: true });

Accessing Underlying Playwright Locator

Every WebElement has a .locator property that provides access to the underlying Playwright Locator. Use this to access Playwright methods not directly exposed by WebElement:

// Access the underlying Playwright Locator
const element = $('.my-element');
const locator = element.locator;

// Use Playwright methods directly
const value = await locator.evaluate((el) => el.getAttribute('data-value'));
const boundingBox = await locator.boundingBox();
const screenshot = await locator.screenshot();

// Or use the shorthand alias
const result = await element._.evaluate((el) => el.textContent);

Common use cases:


Utility Functions

Generate index files and initialize test environments:

import { generateIndexFile, initDesktopOrMobile } from 'playwright-elements';

// Generate index.ts files in a directory
generateIndexFile('./test', { watch: false });

// Initialize test environment for desktop or mobile
initDesktopOrMobile('desktop');

Use the CLI for index generation:

npx generate-index ./test

Page Object Pattern

Traditional page object approach with automatic instantiation:

// pages/loginPage.ts
import { $ } from 'playwright-elements';

export class LoginPage {
  readonly header = $('.header').with({
    logo: $('.header-logo'),
    avatar: $('.avatar')
  });
  
  readonly form = $('.login-form').with({
    usernameInput: $('input[name="username"]'),
    passwordInput: $('input[name="password"]'),
    loginButton: $('button[type="submit"]'),
    async fillForm(userName: string, password: string) {
      await this.usernameInput.fill(userName);
      await this.passwordInput.fill(password);
      await this.loginButton.click();
    }
  });
}

// fixtures.ts
import { test as baseTest, buildPageObject, PageObject } from 'playwright-elements';
import * as pageObjectModule from '../pages';

type TestFixtures = { pageObject: PageObject<typeof pageObjectModule> };

export const test = baseTest.extend<TestFixtures>({
  pageObject: [async ({}, use) => {
    // IMPORTANT: First parameter MUST use object destructuring pattern
    // async (_deps, use) => { ... } will fail at collection time
    await use(buildPageObject(pageObjectModule));
  }, { scope: 'test' }],
});

// test.ts
test('check login page', async ({ pageObject }) => {
  await pageObject.login.form.fillForm('UserName', 'Pass!');
  await pageObject.login.header.logo.expect().toBeVisible();
  await pageObject.login.header.avatar.expect().toBeVisible();
});

Type-Safe Test IDs with playwright-elements

Use type-safe test IDs with playwright-elements component structure:

// testIds.ts
import { factory, sid } from 'playwright-elements';

export const ids = {
  login: {
    usernameInput: sid<'login.username'>('username-input'),
    passwordInput: sid<'login.password'>('password-input'),
    submitButton: sid<'login.submit'>('submit-button'),
  },
  header: {
    logo: sid<'header.logo'>('header-logo'),
  },
} as const;

// components/LoginForm.tsx
import { testIdProps } from 'playwright-elements';
import { ids } from '../testIds';

export function LoginForm() {
  return (
    <form>
      <input {...testIdProps(ids.login.usernameInput)} type="text" />
      <input {...testIdProps(ids.login.passwordInput)} type="password" />
      <button {...testIdProps(ids.login.submitButton)} type="submit">Login</button>
    </form>
  );
}

// elements.ts - define component structure using test IDs
import { $, $byTestId } from 'playwright-elements';
import { ids } from './testIds';

export const loginForm = $('.form-login').with({
  usernameInput: $byTestId(ids.login.usernameInput),
  passwordInput: $byTestId(ids.login.passwordInput),
  submitButton: $byTestId(ids.login.submitButton),
  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }
});

export const header = $('.app-header').with({
  logo: $byTestId(ids.header.logo)
});

// tests/login.spec.ts
import { test } from 'playwright-elements';
import { loginForm, header } from '../elements';

test('login form submission', async ({ goto }) => {
  await goto('/login');
  await loginForm.login('admin', 'password123');
  await header.logo.expect().toBeVisible();
});

See Test IDs Module for complete documentation.


🎯 Use Cases

✅ When to Use playwright-elements:

❌ When NOT to Use:


Documentation

Section Description
Get Started Installation and basic usage
Web Element Complete WebElement API reference
Test IDs Type-safe test ID system with production stripping
Page Objects Page object pattern guide
Fixtures Test fixture configuration
Browser Management Advanced browser control
Architecture Framework design principles
Best Practices Recommended patterns and tips
Migration Guide Upgrade instructions
FAQ Common questions and solutions

For version-specific changes and release notes, see the CHANGELOG.


Stars Forks License