# 🐒 buttonmash
[](https://github.com/cj-vana/buttonmash/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/buttonmash)
[](./LICENSE)
**A CI chaos monkey for web apps.** Point it at your site and it **crawls every
page on its own** — discovering links and in-app (SPA) navigations as it goes —
then on each page finds every button/link/input and mashes them: clicking,
double-clicking, typing random keystrokes, selecting, scrolling, resizing,
navigating. It even **completes create-flows** — filling forms with valid data
and submitting them — so empty apps populate themselves and deep editors get
exercised. When something breaks (an uncaught error, a 500, a crash, a blank
screen, a broken image…) it writes a report and **fails your build**.
It's deterministic (seeded, so any failure replays), bounded (action/time
budgets), and **safe by default**: it stays on your origin, skips destructive
controls, refuses to run against live payment keys, and redacts secrets.
npx buttonmash run https://staging.example.com
## Why
Existing in-page monkeys (gremlins.js and friends) inject synthetic events and
**never actually fail your CI** — they just log to the console. buttonmash flips
that around: it drives the page from the **harness** side with Playwright, so it
owns the verdict and the exit code. It also enumerates real elements (so it hits
buttons below the fold, unlike coordinate-based clickers), dispatches **trusted**
input, and deduplicates findings into an actionable report with a reproducible
seed.
## Install
npm install --save-dev buttonmash
npx playwright install --with-deps chromium # one-time browser install
Requires Node 20+.
## Quickstart
# 1. (optional) capture an authenticated session — opens a browser, you log in
npx buttonmash auth https://staging.example.com/login
# → saves cookies/localStorage to playwright/.auth/user.json
# 2. scaffold a config (optional)
npx buttonmash init
# 3. run it
npx buttonmash run https://staging.example.com --auth playwright/.auth/user.json
# 4. reproduce a failure exactly (the seed is printed on every run)
npx buttonmash run https://staging.example.com --seed
When it finishes you get a `buttonmash-report/` folder with `report.html`
(self-contained), `results.json`, and `junit.xml`. Exit code is `1` if anything
broke at or above your fail threshold.
## Use in CI (GitHub Actions)
The quickest way is the bundled composite action (installs the browser + runs
buttonmash + uploads the report):
name: buttonmash
on: [pull_request]
jobs:
buttonmash:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
# start your app under test here (e.g. npm ci && npm run start &) and wait for it…
- uses: cj-vana/buttonmash@v0.1.8
with:
target: http://localhost:3000
args: --seed ci --max-actions 800
Or wire it by hand for full control:
name: buttonmash
on: [pull_request]
jobs:
buttonmash:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npx playwright install --with-deps chromium
# start your app under test here (e.g. npm run start &) and wait for it…
- run: npx buttonmash run http://localhost:3000 --seed ci --fail-on high
env:
# storageState captured locally and stored as a secret (base64 or file)
STORAGE_STATE: ${{ secrets.BUTTONMASH_STORAGE_STATE }}
- uses: actions/upload-artifact@v5
if: ${{ !cancelled() }}
with: { name: buttonmash-report, path: buttonmash-report/ }
buttonmash auto-detects GitHub Actions and emits inline annotations plus a job-summary
table. The non-zero exit code fails the job.
## Crawling the whole site
By default buttonmash **auto-crawls**: starting from your target, it discovers
every same-origin `` link *and* every client-side route the app
navigates to via buttons/`navigate()` (it hooks `pushState`/`popstate`), queues
them, and works through them breadth-first. When the link frontier runs dry it
returns to the start and keeps clicking — so button-driven SPA shells (where the
nav isn't ``) still get fully covered. One run, the whole reachable site:
npx buttonmash run https://staging.example.com # crawls everything it can reach
Controls:
- `budget.maxPages` — cap on distinct pages per run (default 100), so CI stays bounded.
- `routes` — optional **hints**: pages nothing links to (e.g. a deep editor URL).
They seed the frontier; the crawl finds the rest. Also available as `--route `.
- `explore.crawl: false` — disable auto-crawl and only sweep `target` + `routes`.
Dangerous paths (logout/delete/cancel) and off-origin URLs are never enqueued.
Discovery also reaches **inside open shadow DOM** (web-component design systems —
Salesforce LWC, Ionic, Shoelace/Lit/Material Web) and **same-origin iframes**
(embedded editors, wizards), so component-based apps aren't invisible to it.
It's built to survive messy real apps on long CI sweeps: it **recovers from
renderer crashes** (recreates the page and continues, skipping the page that
crashed), opens **custom ARIA dropdowns** and picks an option, **declines file
pickers** so a file input can't hang the run, and you can **scope the crawl**
with `guardrails.includePaths` / `excludePaths`.
## Self-populating (form completion)
A fresh app is mostly empty lists — so buttonmash **creates its own data**. When
it finds a fillable form (or opens a "New/Add/Create" flow), it fills every
required field — and a fraction of optional ones — with **valid, deterministic**
values inferred from each field's type/label/pattern/min-max/options (real
emails, in-range numbers, seeded dates, a chosen `