🎭 Playwright Expectly | By Cerios
Comprehensive Playwright test matchers for strings, numbers, dates, arrays, objects, locators, and page state. Simplify your E2E tests with intuitive assertions like toBeAlphanumeric, toHaveAscendingOrder, toBeInTheFuture, and 130+ more matchers.
Features
- 🎯 130+ Custom Matchers — extensive validation for all data types
- 🔤 String Validation — email, URL, UUID, alphanumeric, hexadecimal, case-insensitive equality, and more
- 🔢 Number Arrays — sorting, statistics, ranges, and patterns
- 📅 Date & Time —
Date, ISO strings and Temporal; time zones, DST, nanosecond precision, durations - 🎨 Locator Assertions — DOM element validation, text formatting, count ranges, sorted texts, and computed styles
- 🍪 Page State — retrying cookie,
localStorageandsessionStorageassertions - 📦 Object Arrays — sorting, uniqueness, and property validation
- 🧺 Collections & Functions — array members, object keys and entries, promises, and thrown errors
- 🤖 Fuzzy Matching — AI-generated text validation (separate
@cerios/playwright-expectly-fuzzypackage) - 💪 Type-Safe — full TypeScript support
- ⚡ Lightweight — only Playwright required
Installation
npm install @cerios/playwright-expectly --save-dev
Peer dependency: requires
@playwright/testto be installed.
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 imports test/expect from this module — never straight from @playwright/test.
// tests/support/expect.ts
import "@cerios/playwright-expectly";
import { expectlyMatchers } from "@cerios/playwright-expectly";
import { expect as baseExpect, test as base } from "@playwright/test";
// MUST capture the return value — never call `.extend()` and discard it.
export const expect = baseExpect.extend(expectlyMatchers);
export const test = base;
import { expect, test } from "./support/expect";
test("extended expect example", async ({ page }) => {
expect("[email protected]").toBeValidEmail();
expect([1, 2, 3, 4]).toHaveAscendingOrder();
expect(new Date("2025-01-02")).toBeDateCloseTo(new Date("2025-01-01"), { days: 1 });
await expect(page.locator(".username")).toBeAlphanumeric();
});
Capturing the return value is the key detail: the value returned by .extend(...) is the fully typed expect that carries the Expectly matchers. Expectly never overrides a Playwright built-in matcher, so Playwright's own matchers (including numeric toBeCloseTo) keep working on it unchanged.
Standalone expectly
import { expectly } from "@cerios/playwright-expectly";
expectly("[email protected]").toBeValidEmail();
expectly([1, 2, 3, 4, 5]).toHaveAscendingOrder();
expectly(subscription.expiresAt).toBeInTheFuture();
await expectly(page.locator(".username")).toBeAlphanumeric();
Combining with @cerios/playwright-expectly-fuzzy, or your own matchers, via mergeExpects():
import { expectlyMatchers } from "@cerios/playwright-expectly";
import { expectlyFuzzyMatchers } from "@cerios/playwright-expectly-fuzzy";
// tests/support/expect.ts
import { expect as baseExpect, mergeExpects, test as base } from "@playwright/test";
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 standalone package exports, the shorter 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;
Upgrading from 1.x: the Date matcher
toBeCloseTois renamed totoBeDateCloseTo, the deprecatedsetupExpectly()is removed (use thetests/supportpattern above instead), and the date matchers are rebuilt on Temporal with exact calendar semantics:toBeValidISODate()now validatesYYYY-MM-DDandtoMatchTimeZone()is replaced bytoHaveOffset()/toHaveTimeZone(). See the root README for migration examples.
Available Matchers
- String —
toBeValidEmail(),toBeValidUrl(),toBeUUID(),toBeAlphanumeric(),toBeHexadecimal(),toEqualCaseInsensitive(),toEqualIgnoringWhitespace(),toMatchPattern(), and more — 📖 docs - Number Array —
toHaveAscendingOrder(),toHaveAverage(),toBeAllPositive(),toBeMonotonic(), and more — 📖 docs - Date & Time —
Date, ISO strings and Temporal values:toBeWithinLast(),toBeWithinDurationAfter(),toBeSameInstant(),toBeSameDay(),toBeWithinTimeOfDay(),toHaveOffset(), duration matchers, plusparseISO*()/isISO*()and typed ISO strings — 📖 docs - Locator —
toBeAlphanumeric(),toBeUpperCase(),toHaveSrc(),toHaveStyles(),toHaveCountGreaterThan(),toHaveCountBetween(),toHaveTextsInAscendingOrder(), and more — 📖 docs - Page —
toHaveCookie()on aPageorBrowserContext,toHaveLocalStorageItem(),toHaveSessionStorageItem()— 📖 docs - Object Array —
toHaveObjectsInAscendingOrderBy(),toHaveOnlyUniqueObjects(), and more — 📖 docs - String Array —
toHaveAscendingOrder(),toHaveStrictlyAscendingOrder(),toHaveUniqueValues(), and more — 📖 docs - Generic —
toBeInteger(),toBeAnyOf(),toSatisfy(),toBeEven(),toBeFinite(),toEqualPartially(),toBeNullish(), and more — 📖 docs - Collection & Function —
toSatisfyAll(),toIncludeSameMembers(),toContainKeys(),toContainEntry(),toBeEmptyObject(),toResolve(),toReject(),toThrowWithMessage(), and more — 📖 docs
Links
License
MIT © Cerios
Returning values from retrying assertions
expectly.pollAndReturn() returns the value that satisfied the matcher. expectly(callback).toPassAndReturn() returns the result of the first callback attempt that completes without throwing. Both support synchronous and asynchronous callbacks without calling them again after success.
import { expectly } from "@cerios/playwright-expectly";
const order = await expectly
.pollAndReturn(() => fetchOrder(), { timeout: 5_000, intervals: [100, 250] })
.toMatchObject({ status: "COMPLETED" });
const readyOrder = await expectly(async () => {
const current = await fetchOrder();
expectly(current.status).toBe("COMPLETED");
return current;
}).toPassAndReturn({ timeout: 5_000, intervals: [100, 250] });
Here, fetchOrder() is your application helper. Polling supports .not and registered custom matchers. Retry options and custom messages follow the corresponding Playwright APIs; toPassAndReturn() uses native toPass() timeout behavior, so provide an explicit timeout when you need a bound.
For your own configured or extended expect instance, apply withReturn() after composing matcher sources:
import { expectlyMatchers, withReturn } from "@cerios/playwright-expectly";
import { expect as baseExpect } from "@playwright/test";
export const expect = withReturn(baseExpect.extend(expectlyMatchers));
const uuid = await expect.pollAndReturn(() => fetchUuid()).toBeUUID(4);
Subsequent .soft, .configure() and .extend() calls preserve the return-aware methods and custom matcher types. When using mergeExpects(), wrap its result with withReturn().
Failure behavior: by default, return-aware assertions throw on failure, like other Playwright assertions.
Soft assertions: use .soft or .configure({ soft: true }). A failure is then recorded and the test continues, as with native soft assertions. Because the assertion did not pass, the result may not be a passing value:
toPassAndReturn()resolves withundefined, because no attempt succeeded.pollAndReturn()resolves with the last polled value, which did not satisfy the matcher. It resolves withundefinedif the callback never returned a value.
const order = await expectly.soft.pollAndReturn(() => fetchOrder()).toMatchObject({ status: "COMPLETED" });
const readyOrder = await expectly.soft(() => fetchReadyOrder()).toPassAndReturn({ timeout: 5_000 });
// Both are typed `Order | undefined`; the test is already marked as failed when a check did not pass.
Soft return types are T | undefined. TypeScript can only track this when the soft flag is set on the wrapped instance (expectly.soft, withReturn(expect).configure({ soft: true })). withReturn(expect.configure({ soft: true })) behaves the same at runtime, but its types still say T.
Regular assertions retain their configured behavior. Inside toPassAndReturn() callbacks, use throwing assertions for conditions that must trigger retries; soft assertions do not throw.
Failure locations: return-aware assertions appear as boxed Playwright steps. The assertion step and reported failure point to the line where your test invokes the matcher, including detached matcher calls. Native retry steps remain nested underneath for diagnostics and may show internal implementation locations when expanded. Caught errors also point to the invocation while retaining their original message, matcher details, and cause. Regular assertions keep their native reporting.
Ordinary Expectly matchers also report thrown validation and locator API errors at your assertion call site. Their original error type, message, cause, and metadata are preserved. Expanded native equality, polling, and browser API steps can still show implementation locations.
Edge-case behavior
- Object uniqueness compares current values using Node's deep strict equality, including Dates, Maps, Sets, bigint, and cycles. There is no cache across assertions. Property order does not matter; prototypes and explicit
undefinedproperties do. This comparison can take quadratic time for large arrays. - Regex assertions test from index zero without modifying the supplied expression's
lastIndex, including global and sticky expressions. - Partial matching supports shared references but rejects circular plain objects and arrays in either input with a clear error, including under
.not. - Numeric comparisons reject
NaNand infinities. Sum, average, median and range tolerate the rounding error of the whole computation, which grows with the number of values and their magnitude, so[0.01, 2.03, 1.2]sums to3.24; pass{ tolerance }for an explicit absolute bound. Empty arrays have no average or median and never match. - Consecutive months compare calendar months, independent of the day of month. ISO validation rejects impossible calendar dates such as
2026-02-30; see the date matchers for the accepted formats. - A
pollAndReturn()assertion object can be reused or invoked concurrently; each matcher invocation captures its own successful value.
CI tests packaged libraries against the minimum supported Playwright version, 1.40, as well as the project's current version. The minimum-version declaration check uses TypeScript 5.9 because TypeScript 7 rejects legacy syntax in Playwright 1.40's own declarations.