Angry User Simulator — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Angry User Simulator (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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.tsBuild 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('[email protected]');
// 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('[email protected]');
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(): Promise<void> {
const viewport = this.page.viewportSize();
if (!viewport) return;
const x = this.random.nextInt(0, viewport.width);
const y = this.random.nextInt(0, viewport.height);
this.logger.log('click', `(${x}, ${y})`);
await this.page.mouse.click(x, y);
}
private async randomType(): Promise<void> {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*() ';
const length = this.random.nextInt(1, 20);
let text = '';
for (let i = 0; i < length; i++) {
text += chars[this.random.nextInt(0, chars.length - 1)];
}
this.logger.log('type', undefined, text);
await this.page.keyboard.type(text, { delay: 10 });
}
private async randomScroll(): Promise<void> {
const deltaX = this.random.nextInt(-500, 500);
const deltaY = this.random.nextInt(-1000, 1000);
this.logger.log('scroll', `(${deltaX}, ${deltaY})`);
await this.page.mouse.wheel(deltaX, deltaY);
}
private async randomKeyPress(): Promise<void> {
const keys = [
'Enter',
'Escape',
'Tab',
'Backspace',
'Delete',
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'Home',
'End',
'PageUp',
'PageDown',
'F5',
];
const key = this.random.pick(keys);
this.logger.log('keypress', key);
await this.page.keyboard.press(key);
}
private async randomResize(): Promise<void> {
const width = this.random.nextInt(320, 1920);
const height = this.random.nextInt(480, 1080);
this.logger.log('resize', `${width}x${height}`);
await this.page.setViewportSize({ width, height });
}
}import { test, expect } from '../fixtures/chaos.fixture';
import { ChaosMonkey } from '../helpers/chaos-monkey';
test.describe('Chaos Monkey Testing', () => {
test('dashboard survives 30 seconds of chaos', async ({
page,
errorMonitor,
assertPageResponsive,
}) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
const monkey = new ChaosMonkey(page, {
duration: 30000,
actionsPerSecond: 5,
seed: 12345, // Fixed seed for reproducibility
});
const actionLog = await monkey.unleash();
// After chaos, page should still be responsive
await assertPageResponsive();
const crashes = errorMonitor.getErrorsByType('crash');
const unhandled = errorMonitor.getErrorsByType('unhandled-exception');
if (crashes.length > 0 || unhandled.length > 0) {
console.log('Chaos seed:', actionLog.getSeed());
console.log('Action replay:\n', actionLog.getReplayScript());
}
expect(crashes).toHaveLength(0);
expect(unhandled).toHaveLength(0);
});
test('form page survives typing-focused chaos', async ({
page,
errorMonitor,
assertPageResponsive,
}) => {
await page.goto('/profile/edit');
const monkey = new ChaosMonkey(page, {
duration: 15000,
actionsPerSecond: 10,
enableClicking: true,
enableTyping: true,
enableNavigation: false, // Stay on the form page
enableScrolling: false,
enableResizing: false,
enableKeyboard: true,
seed: 67890,
});
await monkey.unleash();
await assertPageResponsive();
const crashes = errorMonitor.getErrorsByType('crash');
expect(crashes).toHaveLength(0);
});
test('targeted monkey testing on modal dialogs', async ({
page,
errorMonitor,
assertPageResponsive,
}) => {
await page.goto('/dashboard');
// Open a modal
await page.getByRole('button', { name: /create project/i }).click();
await expect(page.getByRole('dialog')).toBeVisible();
const monkey = new ChaosMonkey(page, {
duration: 10000,
actionsPerSecond: 8,
enableNavigation: false,
enableResizing: false,
seed: 11111,
});
await monkey.unleash();
await assertPageResponsive();
// Modal should either still be open or have closed gracefully
// It should NOT have left orphaned overlays or broken z-index
const overlays = page.locator('[data-overlay], .modal-backdrop, [role="dialog"]');
const overlayCount = await overlays.count();
expect(overlayCount).toBeLessThanOrEqual(1);
const crashes = errorMonitor.getErrorsByType('crash');
expect(crashes).toHaveLength(0);
});
});Test what happens when the viewport is resized rapidly, simulating users dragging browser windows aggressively.
import { test, expect } from '../fixtures/chaos.fixture';
test.describe('Resize and Orientation Spam', () => {
test('rapid resizing does not break layout', async ({
page,
errorMonitor,
assertPageResponsive,
}) => {
await page.goto('/dashboard');
const sizes = [
{ width: 1920, height: 1080 },
{ width: 1024, height: 768 },
{ width: 768, height: 1024 },
{ width: 375, height: 667 },
{ width: 320, height: 480 },
{ width: 2560, height: 1440 },
{ width: 500, height: 300 },
];
for (let cycle = 0; cycle < 3; cycle++) {
for (const size of sizes) {
await page.setViewportSize(size);
await new Promise((r) => setTimeout(r, 100));
}
}
await new Promise((r) => setTimeout(r, 1000));
await assertPageResponsive();
// Check for overflow issues
const hasHorizontalOverflow = await page.evaluate(() => {
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
});
// Some overflow may be expected on very small viewports, but check for gross overflow
if (hasHorizontalOverflow) {
const overflowAmount = await page.evaluate(() => {
return (
document.documentElement.scrollWidth - document.documentElement.clientWidth
);
});
expect(overflowAmount).toBeLessThan(100);
}
const crashes = errorMonitor.getErrorsByType('crash');
expect(crashes).toHaveLength(0);
});
test('orientation switching simulation does not break responsive design', async ({
page,
assertPageResponsive,
}) => {
await page.goto('/dashboard');
// Simulate rapid orientation switches (portrait <-> landscape)
for (let i = 0; i < 20; i++) {
if (i % 2 === 0) {
await page.setViewportSize({ width: 375, height: 812 });
} else {
await page.setViewportSize({ width: 812, height: 375 });
}
await new Promise((r) => setTimeout(r, 100));
}
await assertPageResponsive();
});
});Simulate users who open the application in multiple tabs and interact with them simultaneously.
import { test, expect } from '../fixtures/chaos.fixture';
test.describe('Multi-Tab Interactions', () => {
test('opening same page in multiple tabs does not corrupt state', async ({
browser,
}) => {
const context = await browser.newContext();
const pages = await Promise.all(
Array.from({ length: 5 }, () => context.newPage())
);
// Navigate all tabs to the same page
await Promise.all(pages.map((page) => page.goto('/dashboard')));
// Perform actions in different tabs
await pages[0].getByRole('button', { name: /create/i }).click().catch(() => {});
await pages[1].getByRole('button', { name: /create/i }).click().catch(() => {});
// All tabs should still be responsive
for (const page of pages) {
const isResponsive = await Promise.race([
page.evaluate(() => true).then(() => true),
new Promise<boolean>((resolve) => setTimeout(() => resolve(false), 5000)),
]);
expect(isResponsive).toBe(true);
}
await context.close();
});
test('logging out in one tab reflects in other tabs', async ({ browser }) => {
const context = await browser.newContext();
const page1 = await context.newPage();
const page2 = await context.newPage();
await page1.goto('/dashboard');
await page2.goto('/dashboard');
// Log out in page1
await page1.getByRole('button', { name: /logout|sign out/i }).click().catch(() => {});
await new Promise((r) => setTimeout(r, 2000));
// Page2 should detect the logout (via storage events or polling)
await page2.reload();
// Should redirect to login or show logged-out state
const url = page2.url();
const isLoggedOut =
url.includes('login') ||
url.includes('signin') ||
(await page2
.getByRole('button', { name: /login|sign in/i })
.isVisible()
.catch(() => false));
expect(isLoggedOut).toBe(true);
await context.close();
});
});import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests/chaos',
timeout: 120000, // Long timeout for chaos tests
retries: 0, // Do not retry chaos tests -- failures should be investigated
workers: 1, // Sequential to avoid resource contention
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on', // Always capture traces for chaos tests
screenshot: 'on', // Always capture screenshots
video: 'on', // Record video for visual debugging
},
projects: [
{
name: 'chaos-desktop',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'chaos-mobile',
use: { ...devices['Pixel 5'] },
},
],
});# .env.test
BASE_URL=http://localhost:3000
CHAOS_SEED=12345
CHAOS_DURATION_MS=30000
CHAOS_ACTIONS_PER_SECOND=5
CHAOS_ENABLE_VIDEO=truegetEventListeners() to check for accumulated event listeners on DOM elements. Listener leaks are a common cause of post-chaos sluggishness.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.