{"id":51351433,"url":"https://github.com/cj-vana/buttonmash","last_synced_at":"2026-07-02T16:07:29.846Z","repository":{"id":361049304,"uuid":"1252914244","full_name":"cj-vana/buttonmash","owner":"cj-vana","description":"🐒 A CI chaos monkey for web apps — presses every button, mashes random keystrokes, breaks things, and reports. Deterministic, safe-by-default, Playwright-powered.","archived":false,"fork":false,"pushed_at":"2026-07-02T04:29:44.000Z","size":346,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-02T06:18:36.224Z","etag":null,"topics":["chaos-monkey","ci","e2e-testing","fuzzing","monkey-testing","playwright","qa","typescript"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cj-vana.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-29T02:00:07.000Z","updated_at":"2026-07-02T04:29:48.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cj-vana/buttonmash","commit_stats":null,"previous_names":["cj-vana/buttonmash"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/cj-vana/buttonmash","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cj-vana%2Fbuttonmash","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cj-vana%2Fbuttonmash/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cj-vana%2Fbuttonmash/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cj-vana%2Fbuttonmash/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cj-vana","download_url":"https://codeload.github.com/cj-vana/buttonmash/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cj-vana%2Fbuttonmash/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35053634,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-02T02:00:06.368Z","response_time":173,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["chaos-monkey","ci","e2e-testing","fuzzing","monkey-testing","playwright","qa","typescript"],"created_at":"2026-07-02T16:07:29.062Z","updated_at":"2026-07-02T16:07:29.823Z","avatar_url":"https://github.com/cj-vana.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🐒 buttonmash\n\n[![CI](https://github.com/cj-vana/buttonmash/actions/workflows/ci.yml/badge.svg)](https://github.com/cj-vana/buttonmash/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/buttonmash.svg)](https://www.npmjs.com/package/buttonmash)\n[![license](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)\n\n**A CI chaos monkey for web apps.** Point it at your site and it **crawls every\npage on its own** — discovering links and in-app (SPA) navigations as it goes —\nthen on each page finds every button/link/input and mashes them: clicking,\ndouble-clicking, typing random keystrokes, selecting, scrolling, resizing,\nnavigating. It even **completes create-flows** — filling forms with valid data\nand submitting them — so empty apps populate themselves and deep editors get\nexercised. When something breaks (an uncaught error, a 500, a crash, a blank\nscreen, a broken image…) it writes a report and **fails your build**.\n\nIt's deterministic (seeded, so any failure replays), bounded (action/time\nbudgets), and **safe by default**: it stays on your origin, skips destructive\ncontrols, refuses to run against live payment keys, and redacts secrets.\n\n```bash\nnpx buttonmash run https://staging.example.com\n```\n\n\u003e [!WARNING]\n\u003e **Point this at a test/staging environment, never production.** A random\n\u003e clicker mutates state. Use **Stripe/PayPal test mode** and test cards. buttonmash\n\u003e tries hard to avoid damage (see [Safety](#-safety)), but those are guardrails,\n\u003e not guarantees — the real safety control is running against a disposable\n\u003e environment with test-mode billing.\n\n---\n\n## Why\n\nExisting in-page monkeys (gremlins.js and friends) inject synthetic events and\n**never actually fail your CI** — they just log to the console. buttonmash flips\nthat around: it drives the page from the **harness** side with Playwright, so it\nowns the verdict and the exit code. It also enumerates real elements (so it hits\nbuttons below the fold, unlike coordinate-based clickers), dispatches **trusted**\ninput, and deduplicates findings into an actionable report with a reproducible\nseed.\n\n## Install\n\n```bash\nnpm install --save-dev buttonmash\nnpx playwright install --with-deps chromium   # one-time browser install\n```\n\nRequires Node 20+.\n\n## Quickstart\n\n```bash\n# 1. (optional) capture an authenticated session — opens a browser, you log in\nnpx buttonmash auth https://staging.example.com/login\n#    → saves cookies/localStorage to playwright/.auth/user.json\n\n# 2. scaffold a config (optional)\nnpx buttonmash init\n\n# 3. run it\nnpx buttonmash run https://staging.example.com --auth playwright/.auth/user.json\n\n# 4. reproduce a failure exactly (the seed is printed on every run)\nnpx buttonmash run https://staging.example.com --seed \u003cseed-from-report\u003e\n```\n\nWhen it finishes you get a `buttonmash-report/` folder with `report.html`\n(self-contained), `results.json`, and `junit.xml`. Exit code is `1` if anything\nbroke at or above your fail threshold.\n\n## Use in CI (GitHub Actions)\n\nThe quickest way is the bundled composite action (installs the browser + runs\nbuttonmash + uploads the report):\n\n```yaml\nname: buttonmash\non: [pull_request]\njobs:\n  buttonmash:\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v5\n      # start your app under test here (e.g. npm ci \u0026\u0026 npm run start \u0026) and wait for it…\n      - uses: cj-vana/buttonmash@v0.1.8\n        with:\n          target: http://localhost:3000\n          args: --seed ci --max-actions 800\n```\n\nOr wire it by hand for full control:\n\n```yaml\nname: buttonmash\non: [pull_request]\njobs:\n  buttonmash:\n    runs-on: ubuntu-latest\n    timeout-minutes: 15\n    steps:\n      - uses: actions/checkout@v5\n      - uses: actions/setup-node@v5\n        with: { node-version: 20, cache: npm }\n      - run: npm ci\n      - run: npx playwright install --with-deps chromium\n      # start your app under test here (e.g. npm run start \u0026) and wait for it…\n      - run: npx buttonmash run http://localhost:3000 --seed ci --fail-on high\n        env:\n          # storageState captured locally and stored as a secret (base64 or file)\n          STORAGE_STATE: ${{ secrets.BUTTONMASH_STORAGE_STATE }}\n      - uses: actions/upload-artifact@v5\n        if: ${{ !cancelled() }}\n        with: { name: buttonmash-report, path: buttonmash-report/ }\n```\n\nbuttonmash auto-detects GitHub Actions and emits inline annotations plus a job-summary\ntable. The non-zero exit code fails the job.\n\n## Crawling the whole site\n\nBy default buttonmash **auto-crawls**: starting from your target, it discovers\nevery same-origin `\u003ca href\u003e` link *and* every client-side route the app\nnavigates to via buttons/`navigate()` (it hooks `pushState`/`popstate`), queues\nthem, and works through them breadth-first. When the link frontier runs dry it\nreturns to the start and keeps clicking — so button-driven SPA shells (where the\nnav isn't `\u003ca href\u003e`) still get fully covered. One run, the whole reachable site:\n\n```bash\nnpx buttonmash run https://staging.example.com   # crawls everything it can reach\n```\n\nControls:\n\n- `budget.maxPages` — cap on distinct pages per run (default 100), so CI stays bounded.\n- `routes` — optional **hints**: pages nothing links to (e.g. a deep editor URL).\n  They seed the frontier; the crawl finds the rest. Also available as `--route \u003curl...\u003e`.\n- `explore.crawl: false` — disable auto-crawl and only sweep `target` + `routes`.\n\nDangerous paths (logout/delete/cancel) and off-origin URLs are never enqueued.\n\nHash-router SPAs are first-class: `#/route` and `#!/route` fragments count as\ndistinct pages in the frontier and stats (plain `#anchor` fragments don't), and\npath guards like `blockedPathPatterns` apply to the hash route too — a\n`#/account/delete` link is guarded exactly like `/account/delete`.\n\nDiscovery also reaches **inside open shadow DOM** (web-component design systems —\nSalesforce LWC, Ionic, Shoelace/Lit/Material Web) and **same-origin iframes**\n(embedded editors, wizards), so component-based apps aren't invisible to it.\n\nIt's built to survive messy real apps on long CI sweeps: it **recovers from\nrenderer crashes** (recreates the page and continues, skipping the page that\ncrashed), opens **custom ARIA dropdowns** and picks an option, **declines file\npickers** so a file input can't hang the run, and you can **scope the crawl**\nwith `guardrails.includePaths` / `excludePaths`.\n\n## Self-populating (form completion)\n\nA fresh app is mostly empty lists — so buttonmash **creates its own data**. When\nit finds a fillable form (or opens a \"New/Add/Create\" flow), it fills every\nrequired field — and a fraction of optional ones — with **valid, deterministic**\nvalues inferred from each field's type/label/pattern/min-max/options (real\nemails, in-range numbers, seeded dates, a chosen `\u003cselect\u003e` option, mirrored\npassword-confirm), clicks the form's **safe** submit, repairs on validation\nerrors, and follows into the created record so deep editors get exercised. No\nper-site config — detection is structural, so it works on any app.\n\nIt stays safe by reusing the same guardrails: it **never submits** a form with a\ncredit-card field, an auth/login/signup form (would mutate your session), or one\nwhose submit is destructive — and the network fence still blocks live payments.\nOne free-text field per form carries a reflected-input canary, so created\nrecords still feed the XSS oracle. Bounded by `explore.forms.maxRecords`;\n`--dry-run` fills but never submits. Turn it off with `explore.forms.enabled: false`.\n\n## Configuration\n\nCreate `buttonmash.config.ts` (or `.js`/`.json`) — `buttonmash init` writes a\nstarter. CLI flags override the file.\n\n```ts\nimport { defineConfig } from 'buttonmash';\n\nexport default defineConfig({\n  target: 'https://staging.example.com',\n  seed: 'ci',\n\n  // Auth: a saved session…\n  auth: { storageState: 'playwright/.auth/user.json' },\n  // …or a scriptable login (CI-friendly; re-authenticates if the session drops\n  // mid-run). Credentials support ${ENV_VAR} so secrets stay out of the file:\n  // auth: {\n  //   loginScript: {\n  //     url: '/login', usernameSelector: '#email', passwordSelector: '#password',\n  //     submitSelector: 'button[type=submit]', username: '${E2E_USER}', password: '${E2E_PASS}',\n  //     successUrl: '/dashboard',\n  //   },\n  // },\n\n  budget: { maxActions: 500, maxDurationMs: 300_000, maxDepth: 12, maxPages: 100 },\n\n  // Point it at any deployment: extra headers (auth proxy / feature flags),\n  // HTTP basic-auth, and a device viewport. ${ENV_VAR} keeps secrets in env.\n  // headers: { 'X-Feature-Flag': 'on', Authorization: 'Bearer ${API_TOKEN}' },\n  // viewport: { width: 390, height: 844 }, // mobile\n  // auth: { basicAuth: { username: '${BASIC_USER}', password: '${BASIC_PASS}' } },\n\n  // Auto-crawl is on by default; `routes` are optional hints for pages nothing\n  // links to (e.g. a deep editor). The crawl discovers everything else.\n  // routes: ['/dashboard', '/settings/billing'],\n  explore: { crawl: true },\n\n  guardrails: {\n    // allowedOrigins: ['https://staging.example.com'], // defaults to target origin\n    // includePaths: ['^/app/'],     // scope the crawl (regex on pathname)\n    // excludePaths: ['/admin'],     // never crawl these\n    billing: { mode: 'refuse' },   // refuse | warn | off\n    // dryRun: true,                // read-only: explore without submitting\n    destructive: { enabled: true, extraVerbs: ['archivar'] },\n  },\n\n  detectors: {\n    a11y: false,                    // opt-in axe-core scan\n    ignoreHttpStatuses: [401, 403],\n    ignorePatterns: ['ResizeObserver loop'], // benign console/network noise (regex)\n    custom: [{ name: 'error-boundary', pattern: 'Something went wrong', severity: 'high', target: 'console' }],\n  },\n\n  failOn: 'high',                   // critical | high | medium | low | info\n});\n```\n\n### Common CLI flags\n\n| Flag | Description |\n|---|---|\n| `--seed \u003cs\u003e` | Reproducibility seed (printed every run) |\n| `--route \u003curl...\u003e` | Extra route hints to sweep in the same run (crawl finds the rest) |\n| `--max-actions \u003cn\u003e` / `--max-duration \u003csec\u003e` | Budget |\n| `--fail-on \u003cseverity\u003e` | Min severity that fails the build (default `high`) |\n| `--dry-run` | Read-only: explore without submitting or mutating |\n| `--auth \u003cpath\u003e` | Playwright storageState JSON |\n| `--billing \u003crefuse\\|warn\\|off\u003e` | Live-payment guard |\n| `--browser \u003cchromium\\|firefox\\|webkit\u003e` | Engine |\n| `--headed` | Show the browser |\n| `--out \u003cdir\u003e` / `--formats json,junit,html,sarif` | Reporting |\n\n## What it detects\n\n- **Uncaught JS errors** and `console.error`\n- **HTTP 4xx/5xx** responses and failed requests\n- **Renderer crashes** and **hangs / unresponsive pages** (wall-clock watchdog)\n- **Framework error overlays** (Next.js/Vite/React, \"Application error\") — caught even when an error boundary swallows the throw\n- **Blank screens** (\"white screen of death\") and **broken images**\n- **Reflected input** — a safe canary probe that flags possible XSS sinks (never injects executing payloads)\n- **Client-exposed secrets** (Stripe/AWS/GitHub/Slack/… keys, gitleaks-derived)\n- **Accessibility** violations via axe-core (opt-in)\n- **Session loss** — if an authed run gets redirected to a login page mid-run (expired session), it flags it and re-authenticates when a login script is configured\n- **Custom signals** — your own console/DOM/url regex rules\n\nFindings are **deduplicated** (the same bug firing 500× becomes one finding with\n`count: 500`) and carry a minimal repro trace. To stay usable on real apps,\nbuttonmash ships a **default allowlist** of benign console noise (ResizeObserver\nloops, React dev warnings, HMR…) and **downgrades third-party `console.error`**\n(analytics/chat/payment SDKs) so they don't redden your build — first-party\nerrors stay high (`detectors.thirdPartyConsole: true` to opt in). State dedup is\n**structural** by default, so live counters/clocks don't explode the state space\non dynamic apps. And if CI cancels or times out mid-run, a **partial report is\nstill written** (SIGTERM-safe) so you never lose the findings collected so far.\n\n## 🛡 Safety\n\nbuttonmash is built to break things without breaking *you*:\n\n- **Stay on origin.** Off-origin navigations and `target=_blank` popups are\n  blocked; the session is fenced to your allowed origins.\n- **Skip destructive controls.** Buttons/links matching a multilingual verb\n  list (delete, pay, logout, cancel subscription, …), or pointing at dangerous\n  paths (`/logout`, `/account/delete`, `/billing/cancel`), are detected and\n  downgraded to a harmless hover.\n- **Refuse live billing.** If live Stripe/Braintree keys or live processor hosts\n  are detected, buttonmash aborts (`billing.mode: 'refuse'`) and tells you to switch\n  to test mode. Publishable test keys are fine.\n- **Redact secrets.** Anything matching a secret pattern is scrubbed before it's\n  written to any report or artifact; auth/cookie headers are never persisted.\n- **Dismiss, never confirm.** Native `confirm()`/`beforeunload` dialogs are\n  always dismissed, so the monkey can't click \"Yes, delete\".\n- **Dry-run mode.** `--dry-run` explores read-only: hover, scroll, navigate\n  links — no form submits, typing, or mutations.\n\n## Reports \u0026 exit codes\n\nEvery run writes `results.json` (the source of truth). Optionally `junit.xml`\n(for CI test rendering), a self-contained `report.html`, and `results.sarif`\n(for GitHub code-scanning). On GitHub Actions it additionally emits inline\n`::error` annotations for the top findings and a markdown job summary — no\nsetup needed. Exit codes follow the pytest/ESLint convention:\n\n| Code | Meaning |\n|---|---|\n| `0` | No findings at/above the fail threshold |\n| `1` | Findings at/above the threshold — **the build-failing signal** |\n| `2` | buttonmash itself errored (bad config/usage/internal) |\n\n## Reproducibility\n\nEvery choice — which element, which action, which input — flows through a single\nseeded PRNG, and the in-page `Math.random` is seeded identically. The seed is\nprinted at startup and embedded in every report, and replaying it makes the\nmonkey take the **same decisions**.\n\nCaveat worth knowing: the page clock is **deliberately not frozen** (freezing\ntime breaks many real apps). So replay is reliable for apps whose rendered DOM\nis stable given the same inputs; apps with heavy async-loaded content, polling,\nor wall-clock/`Math.random`-driven rendering can still diverge, because a\ndifferent DOM at a step changes what the monkey sees and therefore what it picks\nnext. Pinning the seed in CI plus a stable build gets you most of the way.\n\n## Programmatic API\n\n```ts\nimport { buttonmash } from 'buttonmash';\n\nconst result = await buttonmash({ target: 'http://localhost:3000', failOn: 'high' });\nconsole.log(result.stats, result.findings);\nif (result.run.exitCode !== 0) process.exit(1);\n```\n\n## How it works\n\n```\nlaunch (Playwright) → auth (storageState) → fence (origin/dialogs/popups)\n  → crawl frontier (target + routes; grows with discovered links + SPA navs)\n  → per page: discover interactive elements → fingerprint state (coverage)\n          → choose element (epsilon-greedy) → gate (safety) → perform action\n          → log trace → capture artifacts on signal\n          → when exhausted, move to the next page in the frontier\n  → aggregate + dedupe → report (json/junit/html/sarif) → exit code\n```\n\n## Limitations\n\n- Reaches open shadow roots and same-origin iframes, but not **closed** shadow\n  roots or **cross-origin** iframes (payment iframes are intentionally left\n  alone).\n- Pure random/coverage exploration can under-explore deep multi-step flows.\n- Heuristic destructive detection covers English, Spanish, German, French,\n  Japanese, Chinese, Korean, Russian, and Arabic verbs; extend\n  `destructive.extraVerbs` for your UI. **Sandbox + test mode is the real safety\n  net.**\n\n## Development\n\n```bash\nnpm install\nnpm run build        # tsup → dist/\nnpm test             # vitest (unit + e2e against the bundled buggy app)\nnpm run typecheck \u0026\u0026 npm run lint\n```\n\nThe `examples/buggy-app/` is a deliberately broken page used to dogfood the tool\nin CI.\n\n## License\n\n[MIT](./LICENSE) © cj-vana\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcj-vana%2Fbuttonmash","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcj-vana%2Fbuttonmash","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcj-vana%2Fbuttonmash/lists"}