| name | assertion-utilities |
| description | AssertUtils and ExpectUtils patterns for centralized test validation in Playwright. Use when implementing reusable assertions, soft assertions, or building a consistent validation layer across your test suite.
|
Assertion Utilities Skill
A comprehensive guide to implementing centralized assertion utilities (AssertUtils & ExpectUtils) in Playwright for consistent, reusable, and maintainable test validation.
Why Centralized Assertions?
As test suites grow, scattered and inconsistent assertions become a maintenance nightmare:
| Problem | Solution with Assertion Utilities |
|---|
| Inconsistent assertion messages | Standardized, descriptive error messages |
| Duplicate assertion logic | Reusable assertion methods |
| No soft assertion support | Built-in soft assertion mode |
| Hard to add custom validations | Extensible assertion classes |
| Mixed assertion styles | Unified API for all validations |
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ Test Files │
│ (use AssertUtils & ExpectUtils) │
└─────────────────────┬───────────────────────────────────┘
│
┌───────────┴───────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ AssertUtils │ │ ExpectUtils │
│ (Value-based) │ │ (DOM-based) │
│ │ │ │
│ • String equality │ │ • Element visible │
│ • Number comparison │ │ • Text content │
│ • Array contains │ │ • URL matching │
│ • Object matching │ │ • Attribute values │
│ • Type checking │ │ • Element state │
└─────────────────────┘ └─────────────────────┘
│ │
└───────────┬───────────┘
▼
┌─────────────────────┐
│ SoftAssertContext │
│ (Fail-later mode) │
└─────────────────────┘
Implementation
1. AssertUtils - Value-Based Assertions
export class AssertUtils {
private softMode: boolean = false;
private softErrors: Error[] = [];
enableSoftMode(): void {
this.softMode = true;
this.softErrors = [];
}
disableSoftModeAndValidate(): void {
this.softMode = false;
if (this.softErrors.length > 0) {
const messages = this.softErrors.map((e, i) => `${i + 1}. ${e.message}`).join('\n');
this.softErrors = [];
throw new Error(`Soft assertions failed:\n${messages}`);
}
}
getSoftErrors(): Error[] {
return [...this.softErrors];
}
private handleFailure(message: string): void {
const error = new Error(message);
if (this.softMode) {
this.softErrors.push(error);
console.warn(`[Soft Assertion Failed] ${message}`);
} else {
throw error;
}
}
assertEqual<T>(actual: T, expected: T, message?: string): void {
if (actual !== expected) {
this.handleFailure(
message ?? `Expected "${expected}" but got "${actual}"`
);
}
}
assertNotEqual<T>(actual: T, notExpected: T, message?: string): void {
if (actual === notExpected) {
this.handleFailure(
message ?? `Expected value to NOT be "${notExpected}"`
);
}
}
assertStrictEqual<T>(actual: T, expected: T, message?: string): void {
if (actual !== expected) {
this.handleFailure(
message ?? `Expected strict equality: "${expected}" === "${actual}"`
);
}
}
assertDeepEqual<T>(actual: T, expected: T, message?: string): void {
const actualStr = JSON.stringify(actual);
const expectedStr = JSON.stringify(expected);
if (actualStr !== expectedStr) {
this.handleFailure(
message ?? `Objects not deeply equal.\nExpected: ${expectedStr}\nActual: ${actualStr}`
);
}
}
assertTrue(actual: boolean, message?: string): void {
if (actual !== true) {
this.handleFailure(message ?? `Expected true but got ${actual}`);
}
}
assertFalse(actual: boolean, message?: string): void {
if (actual !== false) {
this.handleFailure(message ?? `Expected false but got ${actual}`);
}
}
assertNull(actual: unknown, message?: string): void {
if (actual !== null) {
this.handleFailure(message ?? `Expected null but got ${actual}`);
}
}
assertNotNull(actual: unknown, message?: string): void {
if (actual === null) {
this.handleFailure(message ?? `Expected value to NOT be null`);
}
}
assertUndefined(actual: unknown, message?: string): void {
if (actual !== undefined) {
this.handleFailure(message ?? `Expected undefined but got ${actual}`);
}
}
assertDefined(actual: unknown, message?: string): void {
if (actual === null || actual === undefined) {
this.handleFailure(message ?? `Expected value to be defined`);
}
}
assertGreaterThan(actual: number, expected: number, message?: string): void {
if (actual <= expected) {
this.handleFailure(
message ?? `Expected ${actual} to be greater than ${expected}`
);
}
}
assertGreaterThanOrEqual(actual: number, expected: number, message?: string): void {
if (actual < expected) {
this.handleFailure(
message ?? `Expected ${actual} to be greater than or equal to ${expected}`
);
}
}
assertLessThan(actual: number, expected: number, message?: string): void {
if (actual >= expected) {
this.handleFailure(
message ?? `Expected ${actual} to be less than ${expected}`
);
}
}
assertLessThanOrEqual(actual: number, expected: number, message?: string): void {
if (actual > expected) {
this.handleFailure(
message ?? `Expected ${actual} to be less than or equal to ${expected}`
);
}
}
assertInRange(actual: number, min: number, max: number, message?: string): void {
if (actual < min || actual > max) {
this.handleFailure(
message ?? `Expected ${actual} to be between ${min} and ${max}`
);
}
}
assertApproximately(actual: number, expected: number, tolerance: number, message?: string): void {
if (Math.abs(actual - expected) > tolerance) {
this.handleFailure(
message ?? `Expected ${actual} to be approximately ${expected} (±${tolerance})`
);
}
}
assertContains(actual: string, substring: string, message?: string): void {
if (!actual.includes(substring)) {
this.handleFailure(
message ?? `Expected "${actual}" to contain "${substring}"`
);
}
}
assertNotContains(actual: string, substring: string, message?: string): void {
if (actual.includes(substring)) {
this.handleFailure(
message ?? `Expected "${actual}" to NOT contain "${substring}"`
);
}
}
assertStartsWith(actual: string, prefix: string, message?: string): void {
if (!actual.startsWith(prefix)) {
this.handleFailure(
message ?? `Expected "${actual}" to start with "${prefix}"`
);
}
}
assertEndsWith(actual: string, suffix: string, message?: string): void {
if (!actual.endsWith(suffix)) {
this.handleFailure(
message ?? `Expected "${actual}" to end with "${suffix}"`
);
}
}
assertMatches(actual: string, pattern: RegExp, message?: string): void {
if (!pattern.test(actual)) {
this.handleFailure(
message ?? `Expected "${actual}" to match pattern ${pattern}`
);
}
}
assertEmpty(actual: string, message?: string): void {
if (actual !== '') {
this.handleFailure(message ?? `Expected empty string but got "${actual}"`);
}
}
assertNotEmpty(actual: string, message?: string): void {
if (actual === '') {
this.handleFailure(message ?? `Expected non-empty string`);
}
}
assertEqualIgnoreCase(actual: string, expected: string, message?: string): void {
if (actual.toLowerCase() !== expected.toLowerCase()) {
this.handleFailure(
message ?? `Expected "${actual}" to equal "${expected}" (case-insensitive)`
);
}
}
assertArrayContains<T>(array: T[], element: T, message?: string): void {
if (!array.includes(element)) {
this.handleFailure(
message ?? `Expected array to contain "${element}"`
);
}
}
assertArrayNotContains<T>(array: T[], element: T, message?: string): void {
if (array.includes(element)) {
this.handleFailure(
message ?? `Expected array to NOT contain "${element}"`
);
}
}
assertArrayLength<T>(array: T[], expectedLength: number, message?: string): void {
if (array.length !== expectedLength) {
this.handleFailure(
message ?? `Expected array length ${expectedLength} but got ${array.length}`
);
}
}
assertArrayEmpty<T>(array: T[], message?: string): void {
if (array.length !== 0) {
this.handleFailure(
message ?? `Expected empty array but got ${array.length} elements`
);
}
}
assertArrayNotEmpty<T>(array: T[], message?: string): void {
if (array.length === 0) {
this.handleFailure(message ?? `Expected non-empty array`);
}
}
assertArraysEqual<T>(actual: T[], expected: T[], message?: string): void {
const sortedActual = [...actual].sort();
const sortedExpected = [...expected].sort();
if (JSON.stringify(sortedActual) !== JSON.stringify(sortedExpected)) {
this.handleFailure(
message ?? `Arrays are not equal.\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`
);
}
}
assertType(actual: unknown, expectedType: string, message?: string): void {
if (typeof actual !== expectedType) {
this.handleFailure(
message ?? `Expected type "${expectedType}" but got "${typeof actual}"`
);
}
}
assertInstanceOf<T>(actual: unknown, expectedClass: new (...args: unknown[]) => T, message?: string): void {
if (!(actual instanceof expectedClass)) {
this.handleFailure(
message ?? `Expected instance of ${expectedClass.name}`
);
}
}
}
2. ExpectUtils - DOM-Based Assertions
import { Page, Locator, expect } from '@playwright/test';
export class ExpectUtils {
private page: Page;
private softMode: boolean = false;
private softErrors: Error[] = [];
constructor(page: Page) {
this.page = page;
}
enableSoftMode(): void {
this.softMode = true;
this.softErrors = [];
}
disableSoftModeAndValidate(): void {
this.softMode = false;
if (this.softErrors.length > 0) {
const messages = this.softErrors.map((e, i) => `${i + 1}. ${e.message}`).join('\n');
this.softErrors = [];
throw new Error(`Soft assertions failed:\n${messages}`);
}
}
getSoftErrors(): Error[] {
return [...this.softErrors];
}
private async handleAssertion(assertionFn: () => Promise<void>): Promise<void> {
if (this.softMode) {
try {
await assertionFn();
} catch (error) {
this.softErrors.push(error as Error);
console.warn(`[Soft Assertion Failed] ${(error as Error).message}`);
}
} else {
await assertionFn();
}
}
async toBeVisible(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeVisible();
});
}
async toBeHidden(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeHidden();
});
}
async toBeAttached(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeAttached();
});
}
async toBeDetached(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeAttached({ attached: false });
});
}
async toBeEnabled(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeEnabled();
});
}
async toBeDisabled(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeDisabled();
});
}
async toBeChecked(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeChecked();
});
}
async toBeUnchecked(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeChecked({ checked: false });
});
}
async toBeFocused(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeFocused();
});
}
async toBeEditable(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeEditable();
});
}
async toHaveText(locator: Locator, text: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveText(text);
});
}
async toContainText(locator: Locator, text: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toContainText(text);
});
}
async toBeEmpty(locator: Locator, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toBeEmpty();
});
}
async toHaveValue(locator: Locator, value: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveValue(value);
});
}
async toHaveValues(locator: Locator, values: string[], message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveValues(values);
});
}
async toHaveAttribute(
locator: Locator,
name: string,
value?: string | RegExp,
message?: string
): Promise<void> {
await this.handleAssertion(async () => {
if (value !== undefined) {
await expect(locator, message).toHaveAttribute(name, value);
} else {
await expect(locator, message).toHaveAttribute(name);
}
});
}
async toHaveClass(locator: Locator, className: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveClass(className);
});
}
async toHaveCSS(
locator: Locator,
name: string,
value: string | RegExp,
message?: string
): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveCSS(name, value);
});
}
async toHaveId(locator: Locator, id: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveId(id);
});
}
async toHaveCount(locator: Locator, count: number, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator, message).toHaveCount(count);
});
}
async toHaveTitle(title: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(this.page, message).toHaveTitle(title);
});
}
async toHaveURL(url: string | RegExp, message?: string): Promise<void> {
await this.handleAssertion(async () => {
await expect(this.page, message).toHaveURL(url);
});
}
async toMatchScreenshot(locator: Locator, name: string, options?: object): Promise<void> {
await this.handleAssertion(async () => {
await expect(locator).toHaveScreenshot(name, options);
});
}
async pageToMatchScreenshot(name: string, options?: object): Promise<void> {
await this.handleAssertion(async () => {
await expect(this.page).toHaveScreenshot(name, options);
});
}
}
3. Combined Assertions Class
import { Page } from '@playwright/test';
import { AssertUtils } from './AssertUtils';
import { ExpectUtils } from './ExpectUtils';
export class Assertions {
private _assert: AssertUtils;
private _expect: ExpectUtils;
constructor(page: Page) {
this._assert = new AssertUtils();
this._expect = new ExpectUtils(page);
}
assert(): AssertUtils {
return this._assert;
}
expect(): ExpectUtils {
return this._expect;
}
enableSoftMode(): void {
this._assert.enableSoftMode();
this._expect.enableSoftMode();
}
disableSoftModeAndValidate(): void {
const assertErrors = this._assert.getSoftErrors();
const expectErrors = this._expect.getSoftErrors();
this._assert.disableSoftModeAndValidate();
this._expect.disableSoftModeAndValidate();
const allErrors = [...assertErrors, ...expectErrors];
if (allErrors.length > 0) {
const messages = allErrors.map((e, i) => `${i + 1}. ${e.message}`).join('\n');
throw new Error(`Soft assertions failed:\n${messages}`);
}
}
}
Using Assertions with Fixtures
import { test as base } from '@playwright/test';
import { Assertions } from '../assertions/Assertions';
type AssertionFixtures = {
assertions: Assertions;
};
export const test = base.extend<AssertionFixtures>({
assertions: async ({ page }, use) => {
const assertions = new Assertions(page);
await use(assertions);
},
});
export { expect } from '@playwright/test';
Combined Fixture (UIActions + Assertions)
import { test as base } from '@playwright/test';
import { UIActions } from '../actions/UIActions';
import { Assertions } from '../assertions/Assertions';
type CombinedFixtures = {
ui: UIActions;
assertions: Assertions;
};
export const test = base.extend<CombinedFixtures>({
ui: async ({ page }, use) => {
await use(new UIActions(page));
},
assertions: async ({ page }, use) => {
await use(new Assertions(page));
},
});
export { expect } from '@playwright/test';
Usage Examples
Basic Usage
import { test } from '../fixtures/combined.fixture';
import { LoginPage } from '../pages/LoginPage';
test('user can login', async ({ page, ui, assertions }) => {
const loginPage = new LoginPage(page, ui);
await ui.pageAction().navigate('/login');
await loginPage.login('user@example.com', 'password');
await assertions.expect().toHaveURL(/.*dashboard/);
await assertions.expect().toBeVisible(page.getByText('Welcome'));
const pageTitle = await ui.pageAction().getTitle();
assertions.assert().assertEqual(pageTitle, 'Dashboard');
});
Using Soft Assertions
test('validate form fields', async ({ page, assertions }) => {
assertions.enableSoftMode();
const nameInput = page.getByLabel('Name');
const emailInput = page.getByLabel('Email');
const phoneInput = page.getByLabel('Phone');
await assertions.expect().toBeVisible(nameInput, 'Name field should be visible');
await assertions.expect().toBeVisible(emailInput, 'Email field should be visible');
await assertions.expect().toBeVisible(phoneInput, 'Phone field should be visible');
await assertions.expect().toHaveValue(nameInput, '', 'Name should be empty initially');
await assertions.expect().toHaveValue(emailInput, '', 'Email should be empty initially');
assertions.disableSoftModeAndValidate();
});
In Page Objects
import { Page, Locator } from '@playwright/test';
import { UIActions } from '../actions/UIActions';
import { Assertions } from '../assertions/Assertions';
export class ProductPage {
readonly productTitle: Locator;
readonly productPrice: Locator;
readonly addToCartButton: Locator;
constructor(
private page: Page,
private ui: UIActions,
private assertions: Assertions
) {
this.productTitle = page.getByRole('heading', { level: 1 });
this.productPrice = page.getByTestId('product-price');
this.addToCartButton = page.getByRole('button', { name: 'Add to Cart' });
}
async verifyProductDetails(expectedName: string, expectedPrice: number): Promise<void> {
await this.assertions.expect().toHaveText(this.productTitle, expectedName);
const priceText = await this.ui.element().getText(this.productPrice);
const price = parseFloat(priceText.replace('$', ''));
this.assertions.assert().assertEqual(price, expectedPrice, 'Product price mismatch');
}
async addToCart(): Promise<void> {
await this.ui.button().click(this.addToCartButton);
await this.assertions.expect().toBeVisible(
this.page.getByRole('alert'),
'Cart confirmation should appear'
);
}
}
Folder Structure
your-project/
├── assertions/
│ ├── AssertUtils.ts # Value-based assertions
│ ├── ExpectUtils.ts # DOM-based assertions
│ ├── Assertions.ts # Combined gateway
│ └── index.ts # Barrel exports
├── actions/
│ ├── UIActions.ts
│ └── ...
├── pages/
│ └── ...
├── fixtures/
│ ├── ui.fixture.ts
│ ├── assertions.fixture.ts
│ └── combined.fixture.ts
└── tests/
└── ...
Best Practices
Do's
Don'ts
Custom Assertion Example
import { Locator } from '@playwright/test';
import { ExpectUtils } from './ExpectUtils';
export class CustomAssertions extends ExpectUtils {
async toBeValidProductCard(cardLocator: Locator): Promise<void> {
await this.toBeVisible(cardLocator.getByRole('img'), 'Product image should be visible');
await this.toBeVisible(cardLocator.getByRole('heading'), 'Product title should be visible');
await this.toBeVisible(cardLocator.getByTestId('price'), 'Product price should be visible');
await this.toBeEnabled(cardLocator.getByRole('button', { name: 'Add to Cart' }));
}
async toHaveValidationError(formLocator: Locator, fieldName: string, errorMessage: string): Promise<void> {
const errorLocator = formLocator.getByRole('alert').filter({ hasText: errorMessage });
await this.toBeVisible(errorLocator, `Validation error for ${fieldName} should be visible`);
}
}
Related Resources