Skip to content

chaos-maker-dev/chaos-maker

Repository files navigation

Chaos Maker

Build Status npm License: MIT

Inject controlled chaos into web applications to test frontend resilience. Works with Playwright, Cypress, WebdriverIO, and Puppeteer with no backend changes.

What's new in v0.8.0

  • Cross-transport matchers: every WebSocket and SSE rule now accepts the same matcher targeting surface as network rules. Use inline hostname, queryParams, or a matcher: 'name' reference into the existing top-level matchers registry so one matcher can target network, WebSocket, and SSE without per-transport duplication. WebSocket chaos concept and Matchers concept.
  • Built-in matchers: graphql, apiRequests, and authRequests ship preregistered so the most common targets need no matchers entry. A user matchers entry 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.

Install

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/puppeteer

Quick start with presets

Drop 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.

Scenario profiles

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.

Advanced matchers

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.

Built-in matchers

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.

Reporting and timeline

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.

30-second Playwright quickstart

When a preset is too coarse, drop down to explicit rules:

npm install @chaos-maker/core @chaos-maker/playwright
import { 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);
});

Adapter coverage

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

Service Worker chaos

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.

Rule Groups

Group related rules so a test can turn a whole failure scenario on or off at runtime without restarting chaos.

Creating Groups

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.

Runtime Toggle

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.

Service Worker Toggle

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.

Multiple Groups Example

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.

Troubleshooting

  • Group not working: confirm the rule was created with .inGroup("name") or group: "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 null throw.
  • SW toggling issues: call injectSWChaos after the page has an active Service Worker controller, and use enableSWGroup or disableSWGroup for SW rules. Page-side enableGroup does not toggle SW rules.

SSE and GraphQL

await injectChaos(page, {
  sse: {
    drops: [{ urlPattern: '/events', eventType: 'token', probability: 0.1 }],
  },
  network: {
    failures: [{
      urlPattern: '/graphql',
      graphqlOperation: 'GetUser',
      statusCode: 503,
      probability: 1,
    }],
  },
});

Full docs

Getting started | Concepts | Recipes | API

Development

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 production

Contributing

Pull 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

License

MIT

Packages

 
 
 

Contributors