Comprehensive Playwright test matchers for strings, numbers, dates, arrays, objects, and locators. Simplify your E2E tests with intuitive assertions.
- π― 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
fuzzballas an optional dependency
npm install @cerios/playwright-expectly --save-devFor fuzzy matching support (AI-generated text) β @cerios/playwright-expectly-fuzzy:
npm install @cerios/playwright-expectly-fuzzy --save-devCreate 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(...).
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();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 passexpectlyMatchers(a raw matcher-function dictionary) intomergeTests()ormergeExpects()β only pass realExpectinstances (the result of.extend()) likeexpectlyExpectabove.
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";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.
toBeValidEmail()- Validate email formattoBeValidUrl()- Validate URL formattoBeUUID(version?)- Validate UUID (optionally specific version)toBeAlphanumeric()- Only letters and numberstoBeNumericString()- Only digitstoStartWith(prefix)/toEndWith(suffix)- String boundariestoMatchPattern(regex)- Regular expression matching
π View all string matchers β
toHaveAscendingOrder()/toHaveDescendingOrder()- Sort validationtoHaveAverage(value)/toHaveMedian(value)- Statistical validationtoHaveMin(value)/toHaveMax(value)- Boundary validationtoBeAllPositive()/toBeAllNegative()- Sign validationtoBeMonotonic()- Consistent directiontoHaveConsecutiveIntegers()- Sequential validation
π View all number array matchers β
toBeCloseTo(date, deviation)- Within time deviationtoBeInTheFuture(refDate?)/toBeInThePast(refDate?)- Temporal validationtoBeSameDay(date)/toBeSameMonth(date)/toBeSameYear(date)- Date comparisontoBeInQuarter(quarter)- Quarter validationtoBeLeapYear()- Leap year checktoHaveConsecutiveDates(unit)- Sequential dates
π View all date matchers β
toBeAlphanumeric()- Alphanumeric texttoBeNumericString()- Numeric texttoBeUUID(version?)- UUID formattoBeUpperCase()/toBeLowerCase()- Case validationtoBeTitleCase()- Title case formattoHaveSrc(value)/toHaveHref(value)/toHaveAlt(value)- Attribute validation
π View all locator matchers β
toHaveObjectsInAscendingOrderBy(property)- Sort by propertytoHaveObjectsInDescendingOrderBy(property)- Reverse sort by propertytoHaveOnlyUniqueObjects()- Uniqueness validation
π View all object array matchers β
toHaveAscendingOrder()/toHaveDescendingOrder()- Alphabetical ordertoHaveStrictlyAscendingOrder()- No duplicates ascendingtoBeMonotonic()- Consistent directiontoHaveUniqueValues()- No duplicates
π View all string array matchers β
toBeInteger()/toBeFloat()- Number type validationtoBeAnyOf(...values)- Multiple value matchingtoEqualPartially(expected, options)- Partial object and array matching with configurable array modestoBeNullish()- Null or undefined checktoBePrimitive()/toBeArray()/toBeObject()- Type checking
π View all generic matchers β
toMatchFuzzy(expected, threshold?)- Fuzzy string matching using fuzzball's token-sort ratio. Works on strings and locators. Ideal for AI-generated text 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);
});test("validate sorted data", async () => {
const scores = [85, 90, 92, 95];
expectly(scores).toHaveAscendingOrder();
expectly(scores).toHaveAverage(90.5);
expectly(scores).toBeAllPositive();
});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 });
});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");
});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 };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 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();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"));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();- π String Matchers - Email, URL, UUID, alphanumeric validation
- π Number Array Matchers - Sorting, statistics, range checks
- π Date Matchers - Date comparisons, quarters, business days
- π Locator Matchers - DOM element text and attribute validation
- π Object Array Matchers - Sorting and uniqueness by property
- π String Array Matchers - Alphabetical sorting and uniqueness
- π Generic Matchers - Type checking and partial matching
- π Fuzzy Matchers - AI-generated text validation with fuzzy string matching
Contributions are welcome! Feel free to open an issue or submit a pull request.
MIT Β© Cerios
- π« Report Issues
- π¬ Discussions
- π Website
Built with β€οΈ by Cerios