pramoddutta/qaskills/angry-user-simulator

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Simulate aggressive user behavior patterns including rapid clicking, random navigation, form abuse, tab spamming, and unexpected interaction sequences to find UI resilience issues

Install

npx vskill@latest install pramoddutta/qaskills/angry-user-simulator

SKILL.md

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
});
}

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 90 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
118
Repository forks
6
7-day trend score
24

Stars and forks are for the whole GitHub repository, not this skill alone. Refreshed 13 May 2026, 5 months ago.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/pramoddutta/qaskills/angry-user-simulator/badge)](https://verified-skill.com/skills/pramoddutta/qaskills/angry-user-simulator)

Something wrong with this skill or its page? Tell us.