Build typed templates with webhookTemplate() and compose matchers using matchField, matchPartial, and matchPredicate. All matchers on a template use AND semantics — every matcher must pass for a webhook to be considered a match. Templates are immutable value objects produced by a fluent builder.
Define template factories as pure functions that accept a test-scoped ID. This is the key pattern for parallel isolation — each factory call produces a template bound to a specific entity:
import { webhookTemplate } from '@seontechnologies/playwright-utils/webhook';
// Template factories for movie webhooks
// 15s timeout: the Kafka → HTTP webhook delivery pipeline can back up under
// high CI concurrency (burn-in with many parallel workers). 10s was occasionally
// not enough; 15s gives the pipeline headroom without slowing normal runs.
const movieCreated = (movieId: number) =>
webhookTemplate<{ event: string; data: { id: number } }>('movie.created')
.matchField('event', 'movie.created')
.matchField('data.id', movieId)
.withTimeout(15_000)
.withInterval(500)
.build();
const movieDeleted = (movieId: number) =>
webhookTemplate<{ event: string; data: { id: number } }>('movie.deleted')
.matchField('event', 'movie.deleted')
.matchField('data.id', movieId)
.withTimeout(15_000)
.withInterval(500)
.build();
The ID parameter scopes each template to a specific entity, preventing parallel workers from matching each other’s webhooks.
Traverses dot-notation paths into the payload. Never throws if the path is missing — a missing path evaluates as non-matching.
webhookTemplate('order.created')
.matchField('event', 'order.created') // top-level field
.matchField('data.id', orderId) // nested path
.matchField('data.status', 'pending') // nested string value
.build();
Matcher detail output: field(data.id=42)
Checks that the expected object is a subset of the received payload. Extra fields in the payload are ignored. Arrays use strict length matching.
const partialTemplate = webhookTemplate<{
event: string;
data: { id: number; name: string };
}>('movie.created.partial')
.matchPartial({ event: 'movie.created', data: { id: movieId } })
.withTimeout(10_000)
.withInterval(500)
.build();
Matcher detail output: partial({"event":"movie.created","data":{"id":42}})
Accepts any (payload: T) => boolean function. Always requires a human-readable description string — this appears in WebhookTimeoutError.matcherDetails for debugging.
ID-scoped parallel isolation (prevents cross-worker contamination in waitForCount):
const batchTemplate = webhookTemplate<{
event: string;
data: { id: number };
}>('movie.created.batch')
.matchField('event', 'movie.created')
.matchPredicate(`data.id is ${id1} or ${id2}`, (p) => p.data.id === id1 || p.data.id === id2)
.withTimeout(15_000)
.withInterval(500)
.build();
Business data filtering:
const highRatingTemplate = webhookTemplate<{
event: string;
data: { id: number; rating: number };
}>('movie.created.high-rating')
.matchField('event', 'movie.created')
.matchPredicate(`data.id is ${movieId} and data.rating >= 9`, (p) => p.data.id === movieId && p.data.rating >= 9)
.withTimeout(10_000)
.withInterval(500)
.build();
Matcher detail output: predicate(data.id is 42 and data.rating >= 9)
All matchers use AND semantics — all must pass for the webhook to match:
// Combined field + partial: both matchers must pass
const updateTemplate = webhookTemplate<{
event: string;
data: { id: number; name: string };
}>('movie.updated')
.matchField('event', 'movie.updated')
.matchPartial({ data: { id: movieId, name: nameUpdate.name } })
.withTimeout(10_000)
.withInterval(500)
.build();
Override the registry defaults on a per-template basis:
webhookTemplate('slow.pipeline.event')
.matchField('event', 'slow.pipeline.event')
.withTimeout(60_000) // 60s for slow delivery pipelines
.withInterval(2_000) // poll every 2s
.build();
Note:
clone()is available on the builder but is not used in the playwright-utils E2E suite. Use it when multiple tests share the same base template with slight field variations.
const base = webhookTemplate<OrderPayload>('order').matchField('event', 'order.completed');
const forOrderA = base.clone().matchField('data.orderId', 'A').build();
const forOrderB = base.clone().matchField('data.orderId', 'B').build();
| Method | Description |
|---|---|
webhookTemplate<T>(name) |
Create a new builder with the given template name |
.matchField(path, value) |
Add dot-path exact-match matcher |
.matchPartial(expected) |
Add deep-subset matcher |
.matchPredicate(desc, fn) |
Add arbitrary predicate matcher (description required) |
.withTimeout(ms) |
Override registry default timeout |
.withInterval(ms) |
Override registry default poll interval |
.clone() |
Copy current builder state for variation |
.build() |
Produce the immutable WebhookTemplate<T> object |
webhook-waiting-querying.md — waitFor, waitForCount, drain patternwebhook-timeout-error.md — Reading matcherDetails in error output