Skip to the content.

Go to Main Page »

Architecture Overview

Understanding the design principles and architecture of playwright-elements

Design Philosophy

Playwright-elements is designed with the following core principles:

  1. Minimal Boilerplate: Reduce repetitive code in page objects
  2. Type Safety: Provide full TypeScript support with proper type inference
  3. Chainable API: Enable expressive and readable test code
  4. Framework Agnostic: Work with any test framework (Playwright, Mocha, Jest, etc.)
  5. Production Ready: Core functionality works without test dependencies

Core Components

1. WebElement Class

The central class that wraps Playwright’s Locator with additional functionality.

Key Features:

Architecture Diagram:

WebElement
├── Locator (Playwright-core)
│   ├── All standard Locator methods
│   └── Element selection
├── Selector Building
│   ├── CSS selectors
│   ├── Playwright locators (getBy*, etc.)
│   └── Chaining (and, or, has, hasNot, etc.)
├── Sub-Elements
│   └── Type-safe nested elements
└── Assertions (direct @playwright/test)
    ├── expect()
    └── softExpect()

2. Assertions (1.19.0)

Starting from version 1.19.0, expect() and softExpect() directly use @playwright/test’s expect. When you import from 'playwright-elements' (or 'playwright-elements/test'), the framework configures this automatically, so assertions work out of the box:

┌─────────────────────────────────────────────────────┐
│                      WebElement                        │
│  ┌───────────────────────────────────────────────┐  │
│  │              expect() / softExpect()             │  │
│  │                   │                               │  │
│  └─────────────────────┼────────────────────────────┘  │
│                        │                                 │
└────────────────────────┼─────────────────────────────────┘
                         ▼
┌─────────────────────────────────────────────────────┐
│         @playwright/test's expect (automatic)         │
│   custom matchers via expect.extend() work too        │
└─────────────────────────────────────────────────────┘

For custom (non-@playwright/test) frameworks, you can inject your own assertion library once via WebElement.setExpectProvider({ expect, softExpect }) — this is optional and unnecessary for @playwright/test.

Benefits:

  1. Zero setup for @playwright/test: assertions work automatically
  2. Custom matchers: expect.extend() matchers work automatically with full TypeScript autocomplete
  3. Flexibility: custom frameworks can inject their own assertions via setExpectProvider()
  4. Production Use: Core WebElement can be used without test dependencies

3. BrowserInstance Singleton

Manages Playwright’s Browser, BrowserContext, and Page instances.

Responsibilities:

Pattern: Singleton with static methods and properties

Key Features:


4. Fixtures Integration

Playwright-elements extends Playwright’s test fixtures with custom fixtures:

Configuration:

// playwright.test.fixtures.ts
import { test as base, Page, Response, expect } from '@playwright/test';
import { BrowserInstance, usePage, WebElement } from './index';

// Assertions work automatically: configure @playwright/test's expect for WebElement
WebElement.setExpectProvider({
    expect: expect,
    softExpect: expect.soft
});

export { expect } from '@playwright/test';

// ... fixture definitions

Dependency Graph

User Code
│
├── Page Objects (contain WebElement instances)
│   │
│   └── WebElement
│       ├── Locator (Playwright-core)
│       │   └── Browser API
│       │
│       ├── BrowserInstance (optional, for page/context/browser access)
│       │   └── Playwright Browser/Context/Page
│       │
│       └── Assertions (direct @playwright/test; optional setExpectProvider for custom frameworks)
│           └── @playwright/test's expect
│
└── Fixtures (optional, for Playwright Test)
    └── playwright.test.fixtures.ts
        ├── BrowserInstance configuration
        └── Assertions configuration (automatic)

Type System

Type-Safe Method Chaining

WebElement uses TypeScript’s advanced type features for type-safe method chaining:

// with() method preserves type information
const header = $('.header')
    .with({
        logo: $('.logo'),           // WebElement
        search: $('.search')        // WebElement
            .with({
                input: $('input'),  // WebElement
                button: $('button')  // WebElement
            })
    });

// header type: WebElement & { 
//   logo: WebElement, 
//   search: WebElement & { 
//     input: WebElement, 
//     button: WebElement 
//   } 
// }

Conditional Types

Methods like parent<T>() use conditional types for type safety:

parent<T extends WebElement>(this: WebElement): T extends { parent: infer P } ? P : undefined

Utility Types

Custom utility types for:


Performance Considerations

  1. Lazy Evaluation: Locators are built lazily and evaluated only when needed
  2. Caching: Built locators are cached within the WebElement instance
  3. Minimal Overhead: WebElement adds minimal overhead to Playwright’s native Locator
  4. No Proxies: Unlike some page object libraries, WebElement doesn’t use Proxy objects (which can have performance implications)

Security Considerations

  1. No Evaluations: WebElement doesn’t use evaluate() or execute arbitrary code
  2. Type Safety: Full TypeScript support prevents many runtime errors
  3. Input Validation: Selector validation through Playwright’s native mechanisms
  4. Error Handling: Clear error messages for misconfigurations

Design Patterns Used

1. Automatic Assertions (direct @playwright/test)

2. Singleton Pattern

3. Builder Pattern

4. Factory Method Pattern

5. Decorator Pattern


Comparison with Other Approaches

Playwright Native Locators

// Native Playwright
const button = page.locator('button');
await expect(button).toBeVisible();

// With playwright-elements
const button = $('button');
await button.expect().toBeVisible();

Advantages of playwright-elements:

Other Page Object Libraries

Feature playwright-elements Other Libraries
Type Safety ✅ Full TypeScript ⚠️ Varies
Chainable ✅ Yes ⚠️ Sometimes
Framework Agnostic ✅ Yes (v1.19.0+) ❌ Usually not
Production Ready ✅ Yes ❌ Usually not
No Dependencies ✅ Only Playwright-core ❌ Often have many
Extensible ✅ Yes ⚠️ Sometimes

Best Practices for Extending

1. Creating Custom Elements

// Extend WebElement with custom methods
class CustomButton extends WebElement {
    async safeClick(options?: ClickOptions): Promise<void> {
        await this.waitFor();
        await this.click(options);
    }
}

// Export factory function
export function $button(selector: string): CustomButton {
    return new CustomButton(selector);
}

// Use in page objects
const loginPage = {
    submitButton: $button('#submit')
};

2. Adding Custom Assertions

// Create custom expect with matchers
const customExpect = expect.extend({
    async toHaveCustomAttribute(locator, attr, value) {
        const actual = await locator.getAttribute(attr);
        return {
            pass: actual === value,
            message: () => `Expected ${attr} to be ${value}, but got ${actual}`
        };
    }
});

// Configure WebElement to use it
WebElement.setExpectProvider({
    expect: customExpect,
    softExpect: customExpect.soft
});

Conclusion

Playwright-elements combines the power of Playwright’s native locators with:

Since v1.19.0, expect() / softExpect() directly use @playwright/test’s expect, so assertions and custom matchers work out of the box — with optional setExpectProvider() injection for custom frameworks.


Go to Main Page »