Inject controlled chaos into web applications to test frontend resilience. Works with Playwright, Cypress, WebdriverIO, and Puppeteer with no backend changes.
- Cross-transport matchers: every WebSocket and SSE rule now accepts the same matcher targeting surface as network rules. Use inline
hostname,queryParams, or amatcher: 'name'reference into the existing top-levelmatchersregistry so one matcher can target network, WebSocket, and SSE without per-transport duplication. WebSocket chaos concept and Matchers concept. - Built-in matchers:
graphql,apiRequests, andauthRequestsship preregistered so the most common targets need nomatchersentry. A usermatchersentry of the same name overrides one. Built-in matchers. - Cross-adapter matcher parity coverage: matcher E2E coverage across Playwright, Cypress, WebdriverIO, and Puppeteer is now driven by a single declarative scenario catalog under
e2e-tests/fixtures/parity/. No public API change; published package surface is identical.
Full release notes in CHANGELOG.md.
npm install @chaos-maker/core @chaos-maker/playwright
npm install @chaos-maker/core @chaos-maker/cypress
npm install @chaos-maker/core @chaos-maker/webdriverio
npm install @chaos-maker/core @chaos-maker/puppeteerDrop a named scenario into the config - flaky backend, mobile network, checkout instability - and run. Layer multiple presets for compound scenarios.
import { test, expect } from '@playwright/test';
import { injectChaos } from '@chaos-maker/playwright';
test('checkout works under degraded mobile network', async ({ page }) => {
await injectChaos(page, { presets: ['mobile-3g', 'checkout-degraded'], seed: 42 });
await page.goto('/checkout');
await expect(page.locator('[data-testid="checkout-form"]')).toBeVisible();
});See the full catalog in the Presets docs. When a failure only appears under a generated seed, follow the replay recipe.
When several tests should share the same named scenario, wrap it into a profile. Chaos Maker ships exactly one built-in mobileCheckout demo profile (a wiring proof, not an open catalog) - define your own scenarios via customProfiles. Pass profileOverrides alongside profile to tune one parameter at the call site without forking the profile.
await injectChaos(page, {
profile: 'mobile-checkout',
profileOverrides: {
network: { latencies: [{ urlPattern: '/api/extra', delayMs: 999, probability: 1 }] },
},
seed: 42,
});See the Scenario profiles concept for the resolution rules and runtime override precedence.
Every network, WebSocket, and SSE rule accepts hostname, query parameter, and (network-only) request header / resource-type matchers alongside urlPattern and methods. A separate matchers registry holds reusable named matchers so one matcher can target network, WebSocket, and SSE without per-transport duplication.
await injectChaos(page, {
matchers: {
customers: {
hostname: 'api.example.com',
urlPattern: '/api/customers',
methods: ['GET'],
requestHeaders: { authorization: /^Bearer / },
},
},
network: {
failures: [{ matcher: 'customers', statusCode: 503, probability: 1 }],
latencies: [{ matcher: 'customers', delayMs: 500, probability: 1 }],
},
});See the Advanced matchers concept for the full surface, the four validation issue codes (matcher_not_found, matcher_collision, matcher_inline_conflict, matcher_cycle), and the matcher attribution on debug events.
Three matchers ship built in, so the most common targets need no matchers entry:
await injectChaos(page, {
network: {
latencies: [{ matcher: 'graphql', delayMs: 1200, probability: 1 }],
},
});graphql (/graphql), apiRequests (/api), and authRequests (any request with an Authorization header) resolve by name and behave exactly like a matcher you define. A matchers entry of the same name overrides one. authRequests is meaningful for network rules only: it matches on a request header, which WebSocket and SSE rules cannot target, so a stream rule referencing it matches every connection.
After a chaos run, turn the event log into a structured ChaosReport and serialize it as JSON, Markdown, or a self-contained HTML timeline. The core package returns strings; your test writes them to disk or attaches them as CI artifacts.
import { test } from '@playwright/test';
import {
buildChaosReport,
formatReportHtml,
getChaosLog,
getChaosSeed,
injectChaos,
} from '@chaos-maker/playwright';
test('attach a chaos report on every run', async ({ page }, testInfo) => {
await injectChaos(page, { debug: true, network: { failures: [/* … */] }, seed: 42 });
// run scenario …
const events = await getChaosLog(page);
const seed = await getChaosSeed(page);
const report = buildChaosReport(events, { seed, title: testInfo.title });
await testInfo.attach('chaos-report.html', {
body: formatReportHtml(report),
contentType: 'text/html',
});
});The HTML output is fully self-contained (inline CSS, no <script>, no external URLs). For PR comments, swap formatReportHtml for formatReportMarkdown. See the Timeline and reporting concept for the full report shape, determinism guarantees, and per-rule attribution requirements.
When a preset is too coarse, drop down to explicit rules:
npm install @chaos-maker/core @chaos-maker/playwrightimport { test, expect } from '@playwright/test';
import { injectChaos, getChaosLog } from '@chaos-maker/playwright';
test('shows error state when payment API fails', async ({ page }) => {
await injectChaos(page, {
seed: 42,
network: {
failures: [{ urlPattern: '/api/payments', statusCode: 503, probability: 1.0 }],
},
});
await page.goto('/checkout');
await page.click('#pay-now');
await expect(page.locator('[data-testid="error-banner"]')).toBeVisible();
const log = await getChaosLog(page);
expect(log.some(e => e.type === 'network:failure' && e.applied)).toBe(true);
});| Surface | Playwright | Cypress | WebdriverIO | Puppeteer |
|---|---|---|---|---|
| Network fetch/XHR | Yes | Yes | Yes | Yes |
| UI assaults | Yes | Yes | Yes | Yes |
| WebSocket | Yes | Yes | Yes | Yes |
| Service Worker fetch | Yes | Yes | Yes | Yes |
| Server-Sent Events | Yes | Yes | Yes | Yes |
| GraphQL operation matcher | Yes | Yes | Yes | Yes |
| Rule Groups | Yes | Yes | Yes | Yes |
PWAs and offline-first apps serve fetches from a Service Worker. Those bypass page-side chaos, so add one line to your SW and chaos applies there too:
// classic sw.js
importScripts('/chaos-maker-sw.js');Page-side: injectSWChaos / removeSWChaos / getSWChaosLog in each adapter. See adapter READMEs.
Group related rules so a test can turn a whole failure scenario on or off at runtime without restarting chaos.
import { ChaosConfigBuilder } from '@chaos-maker/core';
const chaos = new ChaosConfigBuilder()
.inGroup("payments")
.failRequests("/api/pay", 503, 1)
.build();Rules without .inGroup() stay in the default group and continue to work as before.
The examples below use page as a generic adapter handle. See each adapter README for exact syntax.
await page.enableGroup("payments");
await page.disableGroup("payments");Browser-side toggles affect rules injected into the page with injectChaos.
await page.enableSWGroup("payments");
await page.disableSWGroup("payments");Service Worker toggles affect rules injected into the active Service Worker with injectSWChaos. Browser-side and SW-side toggles are separate because they run in different JavaScript contexts. If a group has rules in both places, toggle both explicitly.
import { ChaosConfigBuilder } from '@chaos-maker/core';
const chaos = new ChaosConfigBuilder()
.inGroup("payments")
.failRequests("/api/pay", 503, 1)
.inGroup("auth")
.failRequests("/api/session", 401, 1)
.inGroup("analytics")
.addLatency("/api/events", 750, 1)
.build();
await injectChaos(page, chaos);
await page.disableGroup("payments");
await page.enableGroup("auth");
await page.enableGroup("analytics");In this state, payment failures are skipped, auth failures run, and analytics latency runs.
- Group not working: confirm the rule was created with
.inGroup("name")orgroup: "name", and confirm you awaited the toggle before triggering the request. - Group name errors: group names must be strings after trimming. Empty strings, whitespace-only strings, and
nullthrow. - SW toggling issues: call
injectSWChaosafter the page has an active Service Worker controller, and useenableSWGroupordisableSWGroupfor SW rules. Page-sideenableGroupdoes not toggle SW rules.
await injectChaos(page, {
sse: {
drops: [{ urlPattern: '/events', eventType: 'token', probability: 0.1 }],
},
network: {
failures: [{
urlPattern: '/graphql',
graphqlOperation: 'GetUser',
statusCode: 503,
probability: 1,
}],
},
});Getting started | Concepts | Recipes | API
bun install # install all workspace dependencies
bun run build # build all packages
bun run test # unit tests
bun run lint # eslint
bun run dev:docs # local docs dev server
bun run build:docs # build docs for productionPull requests are welcome. See CONTRIBUTING.md for guidelines.
Run the full check before submitting:
bun run lint && bun run test && bun run build
bun run test:playwright -- --project=chromium