ScreenshotNeo

BlogHow-to

Monkey Testing with WebdriverIO: A Practical Guide

Build a bounded, replayable random-action loop with WebdriverIO. Learn how to keep exploratory runs safe, diagnose failures, and turn useful findings into regression tests.

By the ScreenshotNeo team4 October 202612 min read

Monkey testing drives an interface with unpredictable actions—such as clicking visible controls, entering generated text, and scrolling—to expose crashes and broken assumptions. WebdriverIO supplies browser automation primitives for building that loop; the official documentation reviewed does not describe a packaged monkey-testing feature or command. The implementation below is a bounded, seeded example you can adapt to a disposable staging environment.

Use randomized exploration alongside deterministic journey tests. When a run exposes a meaningful defect, preserve its seed and action trace, reproduce and reduce the sequence, then add a repeatable regression test.

1. What monkey testing means here

Classic monkey testing sends randomized or unpredictable inputs to an interface. A vendor describes this as random clicks and keystrokes, in contrast with its own AI-planned interactions; that is the vendor’s distinction, not a neutral definition of every testing approach. In this guide, “monkey” means a small exploratory loop that chooses among explicitly allowed actions and records what happened.

Randomness can reach states that a hand-authored journey misses, but it does not establish correctness by itself. A page changing unexpectedly may be valid behavior. Useful signal comes from explicit invariants—for example, the app remains responsive, a benign form submission produces an allowed outcome, and the browser reports no uncaught application error.

2. Create a WebdriverIO project

Follow the official WebdriverIO getting-started guide to create a project with the starter flow. Its current documentation is for WebdriverIO 9.x and lists Node.js 18.20.0 or later; verify the live requirements when setting up because Node.js support changes. Choose a runner, browser, and framework supported by your project. WebdriverIO Runner supports Mocha, Jasmine, and Cucumber.js directly, with other frameworks possible through adapter packages.

npm init wdio@latest .

The starter prompts configure the project and install dependencies. The sample below is an Mocha-style spec for the WebdriverIO local runner. Put it in the specs directory configured by your generated wdio.conf.js. Set the base URL to a disposable staging app that contains no real customer data.

3. A bounded, replayable random-action spec

This sample intentionally limits interaction to visible, enabled links, buttons, and non-sensitive text-like inputs. It skips submit buttons and password, email, file, and other special input types; tailor the allowlist to your app and data. The seed makes the selection sequence reproducible when the candidate list remains the same. Persisting a trace is still essential because layout and application state can change the available candidates.

// test/specs/monkey.spec.js
const crypto = require('node:crypto');

function seededRandom(seedText) {
  // xmur3 string hash, followed by mulberry32 PRNG.
  let h = 1779033703 ^ seedText.length;
  for (let i = 0; i < seedText.length; i++) {
    h = Math.imul(h ^ seedText.charCodeAt(i), 3432918353);
    h = (h << 13) | (h >>> 19);
  }
  const seed = () => {
    h = Math.imul(h ^ (h >>> 16), 2246822507);
    h = Math.imul(h ^ (h >>> 13), 3266489909);
    return (h ^= h >>> 16) >>> 0;
  };
  let a = seed();
  return function random() {
    a |= 0;
    a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const seedText = process.env.MONKEY_SEED || crypto.randomBytes(8).toString('hex');
const random = seededRandom(seedText);
const actions = [];
const safeText = ['explore', 'sample note', 'check status', 'hello'];

async function candidates() {
  return browser.execute(() => {
    const visible = (el) => {
      const style = getComputedStyle(el);
      const rect = el.getBoundingClientRect();
      return style.display !== 'none' && style.visibility !== 'hidden' &&
        Number(style.opacity) !== 0 && rect.width > 0 && rect.height > 0;
    };
    return [...document.querySelectorAll('a[href], button, input, textarea, select')]
      .filter((el) => visible(el) && !el.disabled && !el.readOnly)
      .filter((el) => {
        if (el.matches('a, button, textarea, select')) return true;
        return el.matches('input') && ['text', 'search', 'url', 'tel'].includes((el.type || 'text').toLowerCase());
      })
      .map((el, index) => ({
        index,
        tag: el.tagName.toLowerCase(),
        id: el.id || '',
        name: el.getAttribute('name') || '',
        type: el.getAttribute('type') || '',
        label: (el.innerText || el.getAttribute('aria-label') || el.getAttribute('placeholder') || '').trim().slice(0, 80),
        href: el.tagName === 'A' ? el.href : '',
        selector: el.id ? `#${CSS.escape(el.id)}` : null
      }));
  });
}

function selectorFor(item) {
  if (item.selector) return item.selector;
  if (item.name) return `${item.tag}[name=${JSON.stringify(item.name)}]`;
  return `${item.tag}:nth-of-type(${item.index + 1})`;
}

describe('bounded monkey exploration', () => {
  it('explores safe controls and records a replay trace', async () => {
    const maxActions = Number(process.env.MONKEY_ACTIONS || 30);
    if (!Number.isInteger(maxActions) || maxActions < 1 || maxActions > 200) {
      throw new Error('MONKEY_ACTIONS must be an integer from 1 to 200');
    }
    const startUrl = process.env.MONKEY_URL;
    if (!startUrl) throw new Error('Set MONKEY_URL to a disposable staging URL');

    await browser.url(startUrl);
    actions.push({ seed: seedText, step: 0, url: await browser.getUrl(), action: 'start' });

    for (let step = 1; step <= maxActions; step++) {
      const options = await candidates();
      if (!options.length) {
        actions.push({ step, url: await browser.getUrl(), action: 'stop', reason: 'no-safe-candidates' });
        break;
      }
      const item = options[Math.floor(random() * options.length)];
      const selector = selectorFor(item);
      const entry = { seed: seedText, step, beforeUrl: await browser.getUrl(), item, selector, at: new Date().toISOString() };
      try {
        if (item.tag === 'a' || item.tag === 'button') {
          await $(selector).click();
          entry.action = 'click';
        } else if (item.tag === 'input' || item.tag === 'textarea') {
          entry.action = 'type';
          entry.value = safeText[Math.floor(random() * safeText.length)];
          await $(selector).setValue(entry.value);
        } else if (item.tag === 'select') {
          entry.action = 'select-first-option';
          const values = await $(selector).$$('option');
          if (values.length > 1) await $(selector).selectByIndex(1);
          else entry.action = 'skip-select-without-choice';
        }
        if (random() < 0.25) {
          await browser.execute(() => window.scrollBy(0, Math.round(window.innerHeight * 0.7)));
          entry.scrolled = true;
        }
        await browser.pause(250);
        entry.afterUrl = await browser.getUrl();
        entry.title = await browser.getTitle();
      } catch (error) {
        entry.error = String(error.message || error);
        entry.afterUrl = await browser.getUrl().catch(() => 'unavailable');
        actions.push(entry);
        await browser.saveScreenshot(`./artifacts/monkey-${seedText}-${step}.png`).catch(() => {});
        throw new Error(`Monkey run failed at step ${step}; seed=${seedText}; trace=${JSON.stringify(actions)}`);
      }
      actions.push(entry);
    }
    console.log(`MONKEY_TRACE ${JSON.stringify(actions)}`);
  });
});

Run it with the environment variables and runner command generated by your project. For example, if the configured script is npm run wdio:

MONKEY_URL=https://staging.example.test MONKEY_SEED=release-42 MONKEY_ACTIONS=40 npm run wdio -- --spec test/specs/monkey.spec.js

Adjust the spec path to your configuration. This is implementation guidance synthesized from WebdriverIO’s automation APIs; it is not an official WebdriverIO monkey-testing recipe, and the code has not been executed as part of this research.

4. Make exploration safer and more useful

Set a hard boundary

  • Run against a disposable test or staging environment with resettable data and test accounts.
  • Set a finite action count and a runner-level test timeout. Stop after the configured bound.
  • Do not include delete, purchase, publish, logout, payment, admin, or other destructive controls in the allowlist.
  • Use network controls or test data to prevent emails, payments, and external side effects.
  • Keep credentials and sensitive data out of generated inputs and logs.

Use explicit selectors and app-specific rules

The sample builds selectors from IDs or names when available, then falls back to an ordinal selector. Ordinal selectors are fragile: the DOM can change between discovery and action, or an element can be replaced after a state update. For higher reliability, add stable test attributes and have candidate discovery return those selectors. Re-discover after every action rather than holding element handles across page changes.

A practical extension is to attach a classification to each candidate, such as safe navigation, benign input, or excluded action. Keep the allowlist close to the application’s test policy. Do not infer that a control is safe merely from its visible label.

Assert invariants, not a guessed journey

After each action, check conditions that should hold across many states: the browser session still responds, a known app shell exists, no fatal error banner appeared, and the page is not unexpectedly blank. For form interactions, define accepted classes of outcome rather than expecting one exact destination for arbitrary input. Add checks for console errors or application-specific health indicators using the hooks supported by the chosen runner and browser.

Record enough to replay

For each step retain the seed, step number, starting URL, action type, stable selector, generated value when applicable, timestamp, ending URL, and error. On failure, save a screenshot and relevant browser logs. Avoid capturing secrets in URLs, fields, screenshots, or CI artifacts. Keep traces compact and apply your normal retention policy.

Turn a finding into a regression test

  1. Replay with the recorded seed and same app build and data.
  2. Confirm whether the behavior violates an invariant or product requirement.
  3. Reduce the action list until the smallest sequence still reproduces the defect.
  4. Write a deterministic test with explicit setup, actions, and assertions.
  5. Keep the exploratory job separate or scheduled so nondeterminism does not obscure release-gate results.

5. Runner and execution choices

Choice What it means Use it when
WebdriverIO local runner Test files run in worker processes, with isolated browser sessions per capability. You want the runner to manage local or configured browser sessions and parallel capabilities.
WebdriverIO browser runner Tests execute inside an actual browser. Your setup benefits from browser-side execution and is configured for the browser runner.

Choose browsers and environments your application supports. The WebdriverIO docs describe browser automation and driver setup; browser-provider coverage, compatibility, and pricing depend on the provider and need current verification. Parallel workers can shorten elapsed time but multiply browser use and may contend for shared test data. Start with one worker until reset and isolation behavior are clear.

6. JavaScript execution and network control

WebdriverIO’s browser.execute runs a function in the current browsing context and returns its value. The sample uses it for DOM candidate discovery and scrolling. Keep page-side code read-only where possible; it can inspect the page but should not silently mutate application state in ways a user could not perform. The documentation recommends execute; avoid the deprecated executeAsync API in new examples.

WebdriverIO’s mock command can help control front-end behavior by changing network responses, but it requires WebDriver BiDi support. Verify that the selected browser or cloud provider supports BiDi before depending on mocks. Without that support, use an application test server, controlled fixtures, or another supported network-stubbing layer.

7. cURL, Python, and Node.js for collecting a screenshot

The browser runner is the right mechanism for driving and observing the live page. If you need an independent image artifact for a URL during triage, ScreenshotNeo offers a screenshot API. Keep the API key in an environment secret and do not commit it. See the ScreenshotNeo API documentation.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

These calls capture the supplied URL; they do not replay the WebdriverIO browser’s cookies or current page state. For screenshots of authenticated or transient test states, capture from the same WebdriverIO session using its screenshot capability. ScreenshotNeo also supports custom headers and cookies for API captures where appropriate; consult the docs for the relevant parameters.

Or skip the browser setup

For a URL screenshot, ScreenshotNeo makes a single request. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed; the response includes page verdict and billing headers. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the API docs and ScreenshotNeo. Create a free account and get 1,000 screenshots a month with no card.

8. Troubleshooting

Symptom Likely cause Fix
WebdriverIO cannot start or install Node.js version or project configuration does not meet the current requirements. Check the current WebdriverIO getting-started requirements, use a supported Node.js release, and rerun the project setup.
“Set MONKEY_URL” error The target environment variable is missing. Set MONKEY_URL to a disposable staging URL before running the spec.
Invalid action-count error MONKEY_ACTIONS is not an integer in the sample’s 1–200 bound. Choose an integer within the bound or change the bound deliberately after assessing runtime and safety.
Element not found or not interactable The DOM changed after discovery, a selector was ambiguous, or an overlay intercepted the click. Use stable test selectors, rediscover each step, filter obscured controls, and capture the failing step’s URL and screenshot.
Run reports a failure on an ordinary navigation The browser or session changed state and the selected control no longer exists, or a navigation outcome was misclassified. Inspect the trace and determine whether it is a product defect; do not classify every changed URL as failure.
Seed does not reproduce the same path Candidate ordering or app state differed, or actions changed which controls were present. Restore the same build and initial data, preserve the full trace, and replay explicit actions. Treat the seed as a selection aid, not a substitute for the trace.
Screenshot is missing on failure The artifact directory may not exist or the browser may already have closed. Create the directory before the run, save artifacts in a runner-supported hook, and retain the trace even if capture fails.
Network mock command is unavailable The active browser/provider does not support WebDriver BiDi. Verify BiDi support or use controlled fixtures and a supported stubbing approach.
Screenshot API returns an error instead of an image The key, URL, request, or target page may be invalid or unreachable. Check the API response and request parameters, keep credentials out of source, and consult the ScreenshotNeo docs for response headers and options.

9. Runtime, reliability, and cost

Runtime grows with action count, browser startup, page navigations, and any waits. Keep a strict action bound, use the shortest wait that still lets the application settle, and avoid parallelizing against shared mutable data. The sample’s pause is a simple settling delay, not a guarantee that all asynchronous work is complete; replace it with app-specific readiness conditions where possible.

Random exploration is inherently sensitive to starting state, browser version, timing, and candidate order. Preserve build identifiers and traces, seed test data, and treat failures as leads to investigate. A failure may indicate a defect, an unstable environment, or an overly broad invariant. No effectiveness or bug-yield percentage is established here.

Local browser execution primarily costs engineering and machine time. Hosted browser environments may add provider charges; check current provider terms. ScreenshotNeo plans are Free for 1,000 shots/month with no card, Starter $5 for 3,000, Growth $15 for 15,000, Pro $39 for 60,000, Scale $99 for 250,000, and Business $249 for 1,000,000; yearly billing gives two months free. Every feature is on every plan. Those API captures are separate from the browser sessions used to run monkey actions.

10. FAQ

Is monkey testing the same as MonkeyTest?

No. “Monkey testing” describes a testing approach; MonkeyTest is a product name used by a vendor. Product capabilities should be judged from that vendor’s current documentation.

Can I execute custom JavaScript with WebdriverIO?

Yes. Use browser.execute for a function in the current browsing context. Keep execution limited to the browser page and prefer observable, user-like actions for the exploration itself.

Should monkey testing replace end-to-end tests?

No. Use it to discover unexpected states, then encode verified bugs and important journeys as deterministic tests.

Where should I run it?

Run against a disposable test or staging environment with safe data and controlled side effects. Do not point an unconstrained random-action loop at production.

References