Simulate aggressive user behavior patterns including rapid clicking, random navigation, form abuse, tab spamming, and unexpected interaction sequences to find UI resilience issues
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.
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
Build an error monitor that captures every console error, unhandled exception, and failed network request during chaos testing:
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');
}
}
Create a logger that records every action taken during chaos testing for reproducibility:
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 = [];
}
}
Reproducible randomness is essential for chaos testing:
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;
}
}
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 };
The most common angry user behavior is rapid, repeated clicking on buttons and interactive elements.
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();
});
});
Test form fields with adversarial input that users may accidentally or intentionally provide.
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();
});
});
Test what happens when users rapidly navigate back and forward, open deep links, or use the browser history aggressively.
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,
assertPageResponsive,
}) => {
await page.goto('/tasks');
// Start filling a form
await page.getByRole('button', { name: /add task/i }).click();
await page.getByLabel('Task Title').fill('Half-completed task');
// Refresh mid-action
await page.reload();
await assertPageResponsive();
// Page should be in a clean state, not a half-broken form
const url = page.url();
expect(url).toContain('/tasks');
});
test('opening the same page in rapid succession', async ({
page,
errorMonitor,
assertPageResponsive,
}) => {
// Rapidly navigate to the same page
for (let i = 0; i < 15; i++) {
await page.goto('/dashboard', { waitUntil: 'commit' }).catch(() => {});
}
await page.waitForLoadState('domcontentloaded');
await assertPageResponsive();
const crashes = errorMonitor.getErrorsByType('crash');
expect(crashes).toHaveLength(0);
});
});
Build a reusable chaos monkey that can be aimed at any page to perform random interactions.
import { Page, Locator } from '@playwright/test';
import { SeededRandom } from './random-data';
import { ActionLogger } from './action-logger';
interface ChaosMonkeyConfig {
duration: number; // milliseconds
actionsPerSecond: number;
enableClicking: boolean;
enableTyping: boolean;
enableNavigation: boolean;
enableScrolling: boolean;
enableResizing: boolean;
enableKeyboard: boolean;
seed: number;
}
const DEFAULT_CONFIG: ChaosMonkeyConfig = {
duration: 30000,
actionsPerSecond: 5,
enableClicking: true,
enableTyping: true,
enableNavigation: true,
enableScrolling: true,
enableResizing: true,
enableKeyboard: true,
seed: Date.now(),
};
export class ChaosMonkey {
private readonly page: Page;
private readonly config: ChaosMonkeyConfig;
private readonly random: SeededRandom;
private readonly logger: ActionLogger;
private running = false;
constructor(page: Page, config: Partial<ChaosMonkeyConfig> = {}) {
this.page = page;
this.config = { ...DEFAULT_CONFIG, ...config };
this.random = new SeededRandom(this.config.seed);
this.logger = new ActionLogger(this.config.seed);
}
async unleash(): Promise<ActionLogger> {
this.running = true;
const startTime = Date.now();
const interval = 1000 / this.config.actionsPerSecond;
while (this.running && Date.now() - startTime < this.config.duration) {
const action = this.pickRandomAction();
try {
await action();
} catch {
// Chaos actions may fail -- that is expected
}
await new Promise((r) => setTimeout(r, interval));
}
this.running = false;
return this.logger;
}
stop(): void {
this.running = false;
}
private pickRandomAction(): () => Promise<void> {
const actions: Array<() => Promise<void>> = [];
if (this.config.enableClicking) {
actions.push(() => this.randomClick());
}
if (this.config.enableTyping) {
actions.push(() => this.randomType());
}
if (this.config.enableScrolling) {
actions.push(() => this.randomScroll());
}
if (this.config.enableKeyboard) {
actions.push(() => this.randomKeyPress());
}
if (this.config.enableResizing) {
actions.push(() => this.randomResize());
}
return this.random.pick(actions);
}
private async randomClick():
<!-- Content truncated for initial SEO render. Open the source file tab for the full file. -->
npx skills add PramodDutta/Angry User Simulator下载完整 Skill 目录,包含 SKILL.md 及所有相关文件
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer
Tags:chaos-testing, user-simulation, stress-testing, ui-resilience, monkey-testing, random-testing, robustness