Skip to content

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

🎭 Playwright Expectly | By Cerios

npm version License: MIT

Comprehensive Playwright test matchers for strings, numbers, dates, arrays, objects, and locators. Simplify your E2E tests with intuitive assertions.

Features

  • 🎯 50+ Custom Matchers - Extensive validation for all data types
  • πŸ”€ String Validation - Email, URL, UUID, alphanumeric, and more
  • πŸ”’ Number Arrays - Sorting, statistics, ranges, and patterns
  • πŸ“… Date Operations - Comparisons, ranges, quarters, and business logic
  • 🎨 Locator Assertions - DOM element validation and text formatting
  • πŸ“¦ Object Arrays - Sorting, uniqueness, and property validation
  • πŸ€– Fuzzy Matching - AI-generated text validation with configurable similarity threshold (separate package)
  • πŸ’ͺ Type-Safe - Full TypeScript support
  • ⚑ Lightweight - Only Playwright required. @cerios/playwright-expectly-fuzzy uses fuzzball as an optional dependency

Installation

npm install @cerios/playwright-expectly --save-dev

For fuzzy matching support (AI-generated text) β€” @cerios/playwright-expectly-fuzzy:

npm install @cerios/playwright-expectly-fuzzy --save-dev

Quick Start

Recommended: a single tests/support module for native expect

Create ONE shared module in your own project that extends Playwright's expect and captures the return value, then re-exports it. Every fixture file and spec file in your project imports test/expect from this module β€” never straight from @playwright/test.

// tests/support/expect.ts
import "@cerios/playwright-expectly";

import { expect as baseExpect, test as base } from "@playwright/test";
import { expectlyMatchers } from "@cerios/playwright-expectly";

// MUST capture the return value β€” never call `.extend()` and discard it.
export const expect = baseExpect.extend(expectlyMatchers);
export const test = base;
// any-spec-file.spec.ts
import { expect, test } from "./support/expect"; // or wherever your support module lives

test("extended expect example", async ({ page }) => {
	expect("john@example.com").toBeValidEmail();
	expect([1, 2, 3, 4]).toHaveAscendingOrder();
	expect(new Date("2025-01-02")).toBeCloseTo(new Date("2025-01-01"), { days: 1 });
	await expect(page.locator(".username")).toBeAlphanumeric();
});

This returned-value pattern is the important part. Playwright only mutates the original expect in place for matcher names that do not collide with built-ins. The Date matcher toBeCloseTo collides with Playwright's native numeric toBeCloseTo, so the Date version only exists on the value returned by .extend(...).

Standalone expectly

Use expectly directly when you do not want to re-export Playwright's expect:

import { expectly } from "@cerios/playwright-expectly";

expectly("user@example.com").toBeValidEmail();
expectly([1, 2, 3, 4, 5]).toHaveAscendingOrder();
expectly(new Date()).toBeInTheFuture(new Date("2020-01-01"));
await expectly(page.locator(".username")).toBeAlphanumeric();

Combining multiple matcher sources

If you also use @cerios/playwright-expectly-fuzzy, your own custom matchers, or multiple fixture files, combine them in that one tests/support/expect.ts module. Two equally valid ways to do this:

mergeExpects() β€” combines already-built Expect instances. Use this when composing 2+ independent matcher sources (recommended for multiple packages):

// tests/support/expect.ts
import { expect as baseExpect, mergeExpects, test as base } from "@playwright/test";
import { expectlyMatchers } from "@cerios/playwright-expectly";
import { expectlyFuzzyMatchers } from "@cerios/playwright-expectly-fuzzy";

// Each matcher set is applied via `.extend()`, capturing the return value, then combined.
const expectlyExpect = baseExpect.extend(expectlyMatchers);
const fuzzyExpect = baseExpect.extend(expectlyFuzzyMatchers);

export const expect = mergeExpects(baseExpect, expectlyExpect, fuzzyExpect);
export const test = base;

If you only need the package-provided standalone expects, the shorter exported-expect form is also valid:

import { expectly } from "@cerios/playwright-expectly";
import { expectlyFuzzy } from "@cerios/playwright-expectly-fuzzy";
import { expect as baseExpect, mergeExpects, test as base } from "@playwright/test";

export const expect = mergeExpects(baseExpect, expectly, expectlyFuzzy);
export const test = base;

.extend() directly β€” layer multiple matcher sets (or your own custom matchers) in one call:

// tests/support/expect.ts
import { expect as baseExpect, test as base } from "@playwright/test";
import { expectlyMatchers } from "@cerios/playwright-expectly";
import { expectlyFuzzyMatchers } from "@cerios/playwright-expectly-fuzzy"; // optional

// MUST capture the return value β€” never call `.extend()` and discard it.
export const expect = baseExpect.extend({
	...expectlyMatchers,
	...expectlyFuzzyMatchers,
	toBeMyCustomThing(received) {
		/* ... */
	},
});
export const test = base;

Common mistake: mergeTests() combines Playwright fixture (test) objects, not matchers. Don't pass expectlyMatchers (a raw matcher-function dictionary) into mergeTests() or mergeExpects() β€” only pass real Expect instances (the result of .extend()) like expectlyExpect above.

If you have several fixture files (page objects, auth, API clients, etc.), give each its own test.extend() module, then combine them with mergeTests() in the same tests/support index alongside the merged expect:

// tests/support/index.ts
import { mergeTests } from "@playwright/test";

import { expect } from "./expect";
import { test as authTest } from "./fixtures/auth.fixture";
import { test as apiTest } from "./fixtures/api.fixture";

export const test = mergeTests(authTest, apiTest);
export { expect };

Then every spec file imports test/expect from tests/support β€” never straight from @playwright/test.

If your playwright.config is JavaScript or is not included in your tsconfig.json, add one ambient import in a .d.ts or shared test file so IntelliSense picks up the matcher types:

import "@cerios/playwright-expectly";

Why not just call a setup function?

You may see a setupExpectly()/setupExpectlyFuzzy() function in older docs or code. These are deprecated. Playwright's expect.extend() only mutates the original expect object in place for matcher names that don't collide with a Playwright built-in. toBeCloseTo collides with Playwright's own built-in numeric matcher, so setupExpectly() (which discards the return value of .extend()) can never make the Date toBeCloseTo matcher work β€” regardless of where you call it, including playwright.config.ts or a worker fixtures file. The tests/support pattern above always works because it captures the return value of .extend()/mergeExpects() correctly.

Available Matchers

String Matchers

  • toBeValidEmail() - Validate email format
  • toBeValidUrl() - Validate URL format
  • toBeUUID(version?) - Validate UUID (optionally specific version)
  • toBeAlphanumeric() - Only letters and numbers
  • toBeNumericString() - Only digits
  • toStartWith(prefix) / toEndWith(suffix) - String boundaries
  • toMatchPattern(regex) - Regular expression matching

πŸ“– View all string matchers β†’

Number Array Matchers

  • toHaveAscendingOrder() / toHaveDescendingOrder() - Sort validation
  • toHaveAverage(value) / toHaveMedian(value) - Statistical validation
  • toHaveMin(value) / toHaveMax(value) - Boundary validation
  • toBeAllPositive() / toBeAllNegative() - Sign validation
  • toBeMonotonic() - Consistent direction
  • toHaveConsecutiveIntegers() - Sequential validation

πŸ“– View all number array matchers β†’

Date Matchers

  • toBeCloseTo(date, deviation) - Within time deviation
  • toBeInTheFuture(refDate?) / toBeInThePast(refDate?) - Temporal validation
  • toBeSameDay(date) / toBeSameMonth(date) / toBeSameYear(date) - Date comparison
  • toBeInQuarter(quarter) - Quarter validation
  • toBeLeapYear() - Leap year check
  • toHaveConsecutiveDates(unit) - Sequential dates

πŸ“– View all date matchers β†’

Locator Matchers

  • toBeAlphanumeric() - Alphanumeric text
  • toBeNumericString() - Numeric text
  • toBeUUID(version?) - UUID format
  • toBeUpperCase() / toBeLowerCase() - Case validation
  • toBeTitleCase() - Title case format
  • toHaveSrc(value) / toHaveHref(value) / toHaveAlt(value) - Attribute validation

πŸ“– View all locator matchers β†’

Object Array Matchers

  • toHaveObjectsInAscendingOrderBy(property) - Sort by property
  • toHaveObjectsInDescendingOrderBy(property) - Reverse sort by property
  • toHaveOnlyUniqueObjects() - Uniqueness validation

πŸ“– View all object array matchers β†’

String Array Matchers

  • toHaveAscendingOrder() / toHaveDescendingOrder() - Alphabetical order
  • toHaveStrictlyAscendingOrder() - No duplicates ascending
  • toBeMonotonic() - Consistent direction
  • toHaveUniqueValues() - No duplicates

πŸ“– View all string array matchers β†’

Generic Matchers

  • toBeInteger() / toBeFloat() - Number type validation
  • toBeAnyOf(...values) - Multiple value matching
  • toEqualPartially(expected, options) - Partial object and array matching with configurable array modes
  • toBeNullish() - Null or undefined check
  • toBePrimitive() / toBeArray() / toBeObject() - Type checking

πŸ“– View all generic matchers β†’

Fuzzy Matchers (@cerios/playwright-expectly-fuzzy)

  • toMatchFuzzy(expected, threshold?) - Fuzzy string matching using fuzzball's token-sort ratio. Works on strings and locators. Ideal for AI-generated text validation.

πŸ“– View fuzzy matchers β†’

Usage Examples

Basic String Validation

import { expectly } from "@cerios/playwright-expectly";

test("validate user input", async () => {
	expectly("john.doe@example.com").toBeValidEmail();
	expectly("https://example.com").toBeValidUrl();
	expectly("550e8400-e29b-41d4-a716-446655440000").toBeUUID(4);
});

Number Array Assertions

test("validate sorted data", async () => {
	const scores = [85, 90, 92, 95];

	expectly(scores).toHaveAscendingOrder();
	expectly(scores).toHaveAverage(90.5);
	expectly(scores).toBeAllPositive();
});

Date Comparisons

test("validate dates", async () => {
	const now = new Date();
	const tomorrow = new Date(now.getTime() + 86400000);

	expectly(tomorrow).toBeInTheFuture(now);
	expectly(tomorrow).toBeCloseTo(now, { hours: 24 });
});

DOM Element Validation

test("validate form elements", async ({ page }) => {
	await expectly(page.locator(".email")).toBeValidEmail();
	await expectly(page.locator(".username")).toBeAlphanumeric();
	await expectly(page.locator("img")).toHaveAlt("Company Logo");
});

Advanced Usage

Extend in one place (global)

Use this when you want all tests to use the extra matchers without importing expectly everywhere.

// e.g. tests/support/expect.ts
import { expect as baseExpect, test } from "@playwright/test";
import { expectlyMatchers } from "@cerios/playwright-expectly";

export const expect = baseExpect.extend(expectlyMatchers);
export { test };

Exported matcher objects

All matcher objects are exported, so you can extend expect with everything or only specific families.

import { expect as baseExpect } from "@playwright/test";
import { expectlyDateMatchers, expectlyMatchers, expectlyStringMatchers } from "@cerios/playwright-expectly";

// All matchers
const expect = baseExpect.extend(expectlyMatchers);

// Or only selected matcher families
const expectStringsAndDates = baseExpect.extend({
	...expectlyStringMatchers,
	...expectlyDateMatchers,
});

Use expectly families directly

Use this when you want a smaller, explicit API per matcher family.

import { expectlyDate, expectlyLocator, expectlyNumberArray, expectlyString } from "@cerios/playwright-expectly";

expectlyString("ABC123").toBeAlphanumeric();
expectlyNumberArray([10, 20, 30]).toHaveAscendingOrder();
expectlyDate(new Date()).toBeSameYear(new Date("2026-01-01"));
await expectlyLocator(page.locator(".email")).toBeValidEmail();

Individual Matcher Imports

For tree-shaking optimization, import only what you need:

import { expectlyString } from "@cerios/playwright-expectly";
import { expectlyNumberArray } from "@cerios/playwright-expectly";
import { expectlyDate } from "@cerios/playwright-expectly";

expectlyString("test@example.com").toBeValidEmail();
expectlyNumberArray([1, 2, 3]).toHaveAscendingOrder();
expectlyDate(new Date()).toBeInTheFuture(new Date("2020-01-01"));

Negation

All matchers support .not for inverse assertions:

expectly("not-an-email").not.toBeValidEmail();
expectly([5, 3, 1]).not.toHaveAscendingOrder();
await expectly(page.locator(".text")).not.toBeNumericString();

Documentation

Contributing

Contributions are welcome! Feel free to open an issue or submit a pull request.

License

MIT Β© Cerios

Support


Built with ❀️ by Cerios

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages