---
name: Angry User Simulator
slug: angry-user-simulator
category: Quality
description: Angry User Simulator helps you write and debug tests that mimic rapid clicking, random navigation, form abuse, and other hostile interactions. Use it to find UI resilience issues, crashes, and duplicate actions before release.
github: "https://github.com/PramodDutta/qaskills/tree/main/seed-skills/angry-user-simulator"
language: TypeScript
stars: 216
forks: 23
install: "npx degit https://github.com/PramodDutta/qaskills/tree/main/seed-skills/angry-user-simulator ~/.claude/skills/angry-user-simulator"
installs_to: ~/.claude/skills/angry-user-simulator
source_path: seed-skills/angry-user-simulator/SKILL.md
collection_size: 25
category_size: 1557
collection_url: "https://dirskills.com/collections/PramodDutta/qaskills"
added: 2026-09-04T05:25:36.238Z
last_synced: 2026-09-04T05:25:36.238Z
canonical_url: "https://dirskills.com/skills/angry-user-simulator"
---

# Angry User Simulator

Angry User Simulator helps you write and debug tests that mimic rapid clicking, random navigation, form abuse, and other hostile interactions. Use it to find UI resilience issues, crashes, and duplicate actions before release.

**Install:**

```bash
npx degit https://github.com/PramodDutta/qaskills/tree/main/seed-skills/angry-user-simulator ~/.claude/skills/angry-user-simulator
```

## README

# Angry User Simulator Skill

You are an expert QA automation engineer specializing in chaos testing and adversarial user simulation. When the user asks you to write, review, or debug tests that simulate aggressive, impatient, or unpredictable user behavior, follow these detailed instructions.

## Core Principles

1. **Users are unpredictable** -- Real users do not follow the happy path. They double-click submit buttons, mash the back button, paste enormous strings into text fields, and interact with elements before the page finishes loading. Every application must withstand this behavior without crashing, corrupting data, or displaying broken UI states.
2. **Chaos reveals hidden assumptions** -- Developers make implicit assumptions about interaction timing, input ordering, and event frequency. Angry user simulation systematically violates these assumptions to expose hidden bugs that structured testing cannot find.
3. **Resilience over correctness** -- The goal is not to verify that a feature works correctly, but that the application remains functional and recoverable when subjected to abuse. A button that does nothing when clicked 50 times rapidly is acceptable. A button that submits 50 duplicate orders is not.
4. **No action should crash the application** -- Regardless of how aggressively a user interacts with the UI, the application should never display a blank screen, an unhandled error, or an unresponsive state. Every chaos test should assert that the application remains interactive.
5. **Console errors are bugs** -- Unhandled exceptions, failed network requests, and deprecation warnings that appear during chaos testing indicate code that is not prepared for adversarial input. Monitor the console during every chaos test run.
6. **Reproducibility matters** -- Random testing is valuable but useless if you cannot reproduce a failure. Always seed your random number generators and log every action taken during a chaos run so that failures can be replayed deterministically.
7. **Escalating intensity** -- Start with mild chaos (rapid clicking) and escalate to extreme abuse (simultaneous keyboard, mouse, and navigation events). This helps isolate the threshold at which the application begins to fail.

## Project Structure

Organize angry user simulation tests with this structure:

```
tests/
  chaos/
    rapid-interaction/
      double-click.spec.ts
      rapid-submit.spec.ts
      button-mashing.spec.ts
    navigation-abuse/
      back-forward-spam.spec.ts
      random-navigation.spec.ts
      deep-link-chaos.spec.ts
    form-abuse/
      paste-bombs.spec.ts
      special-characters.spec.ts
      field-overflow.spec.ts
    keyboard-chaos/
      keyboard-mashing.spec.ts
      shortcut-abuse.spec.ts
      tab-cycling.spec.ts
    visual-chaos/
      resize-spam.spec.ts
      scroll-abuse.spec.ts
      zoom-chaos.spec.ts
    monkey-testing/
      configurable-monkey.spec.ts
      targeted-monkey.spec.ts
      full-app-monkey.spec.ts
  fixtures/
    chaos.fixture.ts
    error-monitor.fixture.ts
  helpers/
    chaos-monkey.ts
    action-logger.ts
    random-data.ts
  pages/
    any-page.page.ts
playwright.config.ts
```

## Setting Up the Chaos Test Infrastructure

### Error Monitor

Build an error monitor that captures every console error, unhandled exception, and failed network request during chaos testing:

```typescript
import { Page, ConsoleMessage, Response } from '@playwright/test';

interface ErrorEntry {
  type: 'console-error' | 'unhandled-exception' | 'network-failure' | 'crash';
  message: string;
  timestamp: number;
  url?: string;
  stack?: string;
}

export class ErrorMonitor {
  private errors: ErrorEntry[] = [];
  private readonly page: Page;
  private readonly ignoredPatterns: RegExp[];

  constructor(page: Page, ignoredPatterns: RegExp[] = []) {
    this.page = page;
    this.ignoredPatterns = ignoredPatterns;
  }

  async start(): Promise<void> {
    // Capture console errors
    this.page.on('console', (msg: ConsoleMessage) => {
      if (msg.type() === 'error') {
        const text = msg.text();
        if (!this.isIgnored(text)) {
          this.errors.push({
            type: 'console-error',
            message: text,
            timestamp: Date.now(),
            url: this.page.url(),
          });
        }
      }
    });

    // Capture unhandled exceptions
    this.page.on('pageerror', (error: Error) => {
      if (!this.isIgnored(error.message)) {
        this.errors.push({
          type: 'unhandled-exception',
          message: error.message,
          timestamp: Date.now(),
          stack: error.stack,
          url: this.page.url(),
        });
      }
    });

    // Capture network failures (5xx responses)
    this.page.on('response', (response: Response) => {
      if (response.status() >= 500) {
        this.errors.push({
          type: 'network-failure',
          message: `${response.status()} ${response.statusText()} - ${response.url()}`,
          timestamp: Date.now(),
          url: response.url(),
        });
      }
    });

    // Detect page crashes
    this.page.on('crash', () => {
      this.errors.push({
        type: 'crash',
        message: 'Page crashed',
        timestamp: Date.now(),
        url: this.page.url(),
      });
    });
  }

  private isIgnored(message: string): boolean {
    return this.ignoredPatterns.some((pattern) => pattern.test(message));
  }

  getErrors(): ErrorEntry[] {
    return [...this.errors];
  }

  getErrorsByType(type: ErrorEntry['type']): ErrorEntry[] {
    return this.errors.filter((e) => e.type === type);
  }

  hasErrors(): boolean {
    return this.errors.length > 0;
  }

  clear(): void {
    this.errors = [];
  }

  getReport(): string {
    if (this.errors.length === 0) return 'No errors detected.';

    return this.errors
      .map((e) => `[${e.type}] ${e.message} (at ${e.url || 'unknown'})`)
      .join('\n');
  }
}
```

### Action Logger

Create a logger that records every action taken during chaos testing for reproducibility:

```typescript
interface ActionEntry {
  action: string;
  target?: string;
  data?: unknown;
  timestamp: number;
  seed?: number;
}

export class ActionLogger {
  private actions: ActionEntry[] = [];
  private readonly seed: number;

  constructor(seed?: number) {
    this.seed = seed || Date.now();
  }

  log(action: string, target?: string, data?: unknown): void {
    this.actions.push({
      action,
      target,
      data,
      timestamp: Date.now(),
      seed: this.seed,
    });
  }

  getActions(): ActionEntry[] {
    return [...this.actions];
  }

  getSeed(): number {
    return this.seed;
  }

  getReplayScript(): string {
    return this.actions
      .map((a) => {
        if (a.target) {
          return `// ${a.action} on ${a.target}${a.data ? ` with ${JSON.stringify(a.data)}` : ''}`;
        }
        return `// ${a.action}`;
      })
      .join('\n');
  }

  clear(): void {
    this.actions = [];
  }
}
```

### Seeded Random Number Generator

Reproducible randomness is essential for chaos testing:

```typescript
export class SeededRandom {
  private seed: number;

  constructor(seed: number) {
    this.seed = seed;
  }

  // Mulberry32 PRNG
  next(): number {
    let t = (this.seed += 0x6d2b79f5);
    t = Math.imul(t ^ (t >>> 15), t | 1);
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  }

  nextInt(min: number, max: number): number {
    return Math.floor(this.next() * (max - min + 1)) + min;
  }

  pick<T>(array: T[]): T {
    return array[this.nextInt(0, array.length - 1)];
  }

  shuffle<T>(array: T[]): T[] {
    const result = [...array];
    for (let i = result.length - 1; i > 0; i--) {
      const j = this.nextInt(0, i);
      [result[i], result[j]] = [result[j], result[i]];
    }
    return result;
  }
}
```

### Custom Test Fixture

```typescript
import { test as base, expect } from '@playwright/test';
import { ErrorMonitor } from '../helpers/error-monitor';
import { ActionLogger } from '../helpers/action-logger';
import { SeededRandom } from '../helpers/random-data';

interface ChaosFixtures {
  errorMonitor: ErrorMonitor;
  actionLogger: ActionLogger;
  random: SeededRandom;
  assertNoErrors: () => void;
  assertPageResponsive: () => Promise<void>;
}

export const test = base.extend<ChaosFixtures>({
  errorMonitor: async ({ page }, use) => {
    const monitor = new ErrorMonitor(page, [
      /favicon\.ico/,
      /ResizeObserver loop/,
    ]);
    await monitor.start();
    await use(monitor);
  },

  actionLogger: async ({}, use) => {
    const seed = parseInt(process.env.CHAOS_SEED || '') || Date.now();
    const logger = new ActionLogger(seed);
    await use(logger);
  },

  random: async ({ actionLogger }, use) => {
    const random = new SeededRandom(actionLogger.getSeed());
    await use(random);
  },

  assertNoErrors: async ({ errorMonitor }, use) => {
    const checker = () => {
      const errors = errorMonitor.getErrors();
      if (errors.length > 0) {
        throw new Error(
          `Chaos test produced ${errors.length} errors:\n${errorMonitor.getReport()}`
        );
      }
    };
    await use(checker);
  },

  assertPageResponsive: async ({ page }, use) => {
    const checker = async () => {
      // Verify the page is not frozen by checking if we can evaluate JS
      const isResponsive = await Promise.race([
        page.evaluate(() => true).then(() => true),
        new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 5000)),
      ]);

      if (!isResponsive) {
        throw new Error('Page is unresponsive after chaos testing');
      }

      // Verify the page has visible content (not a blank/error screen)
      const bodyContent = await page.evaluate(
        () => document.body.innerText.trim().length
      );
      if (bodyContent === 0) {
        throw new Error('Page appears blank after chaos testing');
      }
    };
    await use(checker);
  },
});

export { expect };
```

## Rapid Click and Interaction Testing

The most common angry user behavior is rapid, repeated clicking on buttons and interactive elements.

```typescript
import { test, expect } from '../fixtures/chaos.fixture';

test.describe('Rapid Click Testing', () => {
  test('double-clicking submit button does not create duplicate submissions', async ({
    page,
    errorMonitor,
    assertNoErrors,
  }) => {
    await page.goto('/checkout');

    // Fill in required fields
    await page.getByLabel('Name').fill('Test User');
    await page.getByLabel('Email').fill('test@example.com');

    // Track form submissions
    const submissions: unknown[] = [];
    await page.route('**/api/orders', async (route) => {
      submissions.push(route.request().postDataJSON());
      await route.continue();
    });

    // Double-click the submit button
    const submitButton = page.getByRole('button', { name: /place order/i });
    await submitButton.dblclick();

    await new Promise((r) => setTimeout(r, 2000));

    // Should only submit once despite double-click
    expect(submissions.length).toBeLessThanOrEqual(1);
    assertNoErrors();
  });

  test('rapid clicking submit 20 times creates at most one submission', async ({
    page,
    errorMonitor,
    assertNoErrors,
    assertPageResponsive,
  }) => {
    await page.goto('/checkout');

    await page.getByLabel('Name').fill('Rapid Clicker');
    await page.getByLabel('Email').fill('rapid@example.com');

    const submissions: unknown[] = [];
    await page.route('**/api/orders', async (route) => {
      submissions.push(route.request().postDataJSON());
      await route.continue();
    });

    const submitButton = page.getByRole('button', { name: /place order/i });

    // Click 20 times as fast as possible
    for (let i = 0; i < 20; i++) {
      await submitButton.click({ force: true, delay: 0 }).catch(() => {
        // Button may become disabled or hidden
      });
    }

    await new Promise((r) => setTimeout(r, 3000));

    expect(submissions.length).toBeLessThanOrEqual(1);
    await assertPageResponsive();
    assertNoErrors();
  });

  test('rapid clicking on navigation links does not break routing', async ({
    page,
    errorMonitor,
    assertPageResponsive,
  }) => {
    await page.goto('/dashboard');

    const navLinks = page.getByRole('navigation').getByRole('link');
    const linkCount = await navLinks.count();

    // Rapidly click different navigation links
    for (let i = 0; i < Math.min(linkCount * 3, 30); i++) {
      const index = i % linkCount;
      await navLinks.nth(index).click({ force: true }).catch(() => {});
      // No wait between clicks -- simulating an impatient user
    }

    // Allow navigation to settle
    await new Promise((r) => setTimeout(r, 2000));

    await assertPageResponsive();

    // Page should be on a valid route
    const url = page.url();
    expect(url).not.toContain('undefined');
    expect(url).not.toContain('null');
  });

  test('clicking disabled button does not trigger action', async ({
    page,
    assertNoErrors,
  }) => {
    await page.goto('/checkout');

    // Do not fill required fields, so the button should be disabled
    const submitButton = page.getByRole('button', { name: /place order/i });

    const submissions: unknown[] = [];
    await page.route('**/api/orders', async (route) => {
      submissions.push(route.request().postDataJSON());
      await route.continue();
    });

    // Force-click the disabled button multiple times
    for (let i = 0; i < 10; i++) {
      await submitButton.click({ force: true }).catch(() => {});
    }

    await new Promise((r) => setTimeout(r, 2000));

    expect(submissions).toHaveLength(0);
    assertNoErrors();
  });
});
```

## Form Abuse Testing

Test form fields with adversarial input that users may accidentally or intentionally provide.

```typescript
import { test, expect } from '../fixtures/chaos.fixture';

test.describe('Form Field Abuse', () => {
  const PASTE_BOMBS = {
    longString: 'A'.repeat(100000),
    unicodeMadness: '\u202E\u200B\u200C\u200D\uFEFF'.repeat(1000),
    sqlInjection: "'; DROP TABLE users; --",
    xssPayload: '<script>alert("xss")</script><img src=x onerror=alert(1)>',
    controlCharacters: '\x00\x01\x02\x03\x04\x05\x06\x07\x08'.repeat(100),
    emojiFlood: String.fromCodePoint(0x1f4a9).repeat(10000),
    rtlOverride: '\u202Ethis text is reversed\u202C'.repeat(500),
    nullBytes: 'normal\x00hidden\x00data'.repeat(1000),
    nestedHtml: '<div>'.repeat(1000) + 'content' + '</div>'.repeat(1000),
    jsonPayload: '{"__proto__":{"admin":true}}'.repeat(100),
  };

  for (const [name, value] of Object.entries(PASTE_BOMBS)) {
    test(`form handles paste bomb: ${name}`, async ({
      page,
      errorMonitor,
      assertPageResponsive,
    }) => {
      await page.goto('/profile/edit');

      const nameField = page.getByLabel('Display Name');

      // Paste the adversarial content
      await nameField.fill(value);

      // Try to submit
      await page.getByRole('button', { name: /save/i }).click();

      await new Promise((r) => setTimeout(r, 2000));

      // Application should either reject the input or handle it gracefully
      await assertPageResponsive();

      // Should not have unhandled errors
      const criticalErrors = errorMonitor
        .getErrors()
        .filter((e) => e.type === 'unhandled-exception' || e.type === 'crash');
      expect(criticalErrors).toHaveLength(0);
    });
  }

  test('pasting into every field on a form does not crash', async ({
    page,
    assertPageResponsive,
  }) => {
    await page.goto('/settings');

    // Find all input fields
    const inputs = page.locator('input:visible, textarea:visible, select:visible');
    const inputCount = await inputs.count();

    for (let i = 0; i < inputCount; i++) {
      const input = inputs.nth(i);
      const tagName = await input.evaluate((el) => el.tagName.toLowerCase());
      const inputType = await input.getAttribute('type');

      if (tagName === 'select') {
        // Select a random option
        const options = await input.locator('option').allTextContents();
        if (options.length > 0) {
          await input.selectOption({ index: 0 }).catch(() => {});
        }
      } else if (inputType === 'checkbox' || inputType === 'radio') {
        await input.click({ force: true }).catch(() => {});
      } else {
        await input.fill('A'.repeat(50000)).catch(() => {});
      }
    }

    await assertPageResponsive();
  });

  test('special characters in search field do not cause errors', async ({
    page,
    errorMonitor,
    assertPageResponsive,
  }) => {
    await page.goto('/search');

    const searchInput = page.getByRole('searchbox').or(page.getByPlaceholder(/search/i));
    const specialInputs = [
      '((((((',
      '))))))))',
      '[[[[]]]]]',
      '****???+++',
      '\\\\\\\\',
      '//////',
      '<<<>>>',
      '${process.env.SECRET}',
      '{{constructor.constructor("return this")()}}',
      '%00%0d%0a',
      '../../../etc/passwd',
      'AAAA%08%08%08%08',
    ];

    for (const input of specialInputs) {
      await searchInput.fill(input);
      await page.keyboard.press('Enter');
      await new Promise((r) => setTimeout(r, 500));

      await assertPageResponsive();
    }

    const criticalErrors = errorMonitor
      .getErrors()
      .filter((e) => e.type !== 'network-failure');
    expect(criticalErrors).toHaveLength(0);
  });

  test('rapid field focus cycling does not cause layout thrashing', async ({
    page,
    assertPageResponsive,
  }) => {
    await page.goto('/profile/edit');

    const inputs = page.locator('input:visible, textarea:visible');
    const inputCount = await inputs.count();

    // Rapidly Tab through all fields multiple times
    for (let cycle = 0; cycle < 5; cycle++) {
      for (let i = 0; i < inputCount; i++) {
        await page.keyboard.press('Tab');
      }
    }

    await assertPageResponsive();
  });
});
```

## Navigation Abuse Testing

Test what happens when users rapidly navigate back and forward, open deep links, or use the browser history aggressively.

```typescript
import { test, expect } from '../fixtures/chaos.fixture';

test.describe('Navigation Abuse', () => {
  test('back/forward button mashing does not break routing', async ({
    page,
    errorMonitor,
    assertPageResponsive,
  }) => {
    // Build up some navigation history
    await page.goto('/dashboard');
    await page.goto('/profile');
    await page.goto('/settings');
    await page.goto('/tasks');
    await page.goto('/dashboard');

    // Mash back and forward buttons
    for (let i = 0; i < 20; i++) {
      if (i % 3 === 0) {
        await page.goForward().catch(() => {});
      } else {
        await page.goBack().catch(() => {});
      }
      // No delay between navigations
    }

    await new Promise((r) => setTimeout(r, 2000));

    await assertPageResponsive();

    const criticalErrors = errorMonitor
      .getErrors()
      .filter((e) => e.type === 'unhandled-exception' || e.type === 'crash');
    expect(criticalErrors).toHaveLength(0);
  });

  test('random navigation across all app routes', async ({
    page,
    random,
    actionLogger,
    errorMonitor,
    assertPageResponsive,
  }) => {
    const routes = [
      '/dashboard',
      '/profile',
      '/settings',
      '/tasks',
      '/tasks/new',
      '/search',
      '/notifications',
      '/help',
      '/about',
    ];

    await page.goto('/dashboard');

    for (let i = 0; i < 30; i++) {
      const route = random.pick(routes);
      actionLogger.log('navigate', route);

      await page.goto(route).catch(() => {});
      await new Promise((r) => setTimeout(r, 200));
    }

    await assertPageResponsive();

    const crashes = errorMonitor.getErrorsByType('crash');
    expect(crashes).toHaveLength(0);
  });

  test('refreshing mid-navigation does not corrupt state', async ({
    page,
    assertPageRes
