/** * Chaos injector — occasionally fires unexpected UI events while a * game is playing, to hunt race conditions and recovery bugs. * * Called from the stress scenario's background chaos loop. Each call * has `probability` of firing; when it fires it picks one random * event type and runs it against one session. */ import type { Session, Logger } from '../../core/types'; export type ChaosEvent = 'rapid_clicks' | 'tab_blur' | 'brief_offline'; const ALL_EVENTS: ChaosEvent[] = ['rapid_clicks', 'tab_blur', 'brief_offline']; function pickEvent(): ChaosEvent { return ALL_EVENTS[Math.floor(Math.random() * ALL_EVENTS.length)]; } export async function maybeInjectChaos( session: Session, probability: number, logger: Logger, roomId: string, ): Promise { if (Math.random() >= probability) return null; const event = pickEvent(); logger.info('chaos_injected', { room: roomId, session: session.key, event }); try { switch (event) { case 'rapid_clicks': { // Fire 5 rapid clicks at the player's own cards for (let i = 0; i < 5; i++) { await session.page .locator(`#player-cards .card:nth-child(${(i % 6) + 1})`) .click({ timeout: 300 }) .catch(() => {}); } break; } case 'tab_blur': { // Briefly dispatch blur then focus — simulates user tabbing away await session.page.evaluate(() => { window.dispatchEvent(new Event('blur')); setTimeout(() => window.dispatchEvent(new Event('focus')), 200); }); break; } case 'brief_offline': { // 300ms network outage — should trigger client reconnect logic await session.context.setOffline(true); await new Promise((r) => setTimeout(r, 300)); await session.context.setOffline(false); break; } } } catch (err) { logger.warn('chaos_error', { event, error: err instanceof Error ? err.message : String(err), }); } return event; }