/** * Browser API Mocks * * Common browser API mocks for testing React components * in a jsdom environment. */ import { vi } from 'vitest' /** * Mock localStorage with in-memory storage */ export function mockLocalStorage(): Storage { const store: Record = {} const storageMock: Storage = { getItem: vi.fn((key: string) => store[key] || null), setItem: vi.fn((key: string, value: string) => { store[key] = value }), removeItem: vi.fn((key: string) => { delete store[key] }), clear: vi.fn(() => { for (const key in store) { delete store[key] } }), key: vi.fn((index: number) => Object.keys(store)[index] || null), get length() { return Object.keys(store).length }, } Object.defineProperty(window, 'localStorage', { value: storageMock, writable: true, }) return storageMock } /** * Mock sessionStorage with in-memory storage */ export function mockSessionStorage(): Storage { const store: Record = {} const storageMock: Storage = { getItem: vi.fn((key: string) => store[key] || null), setItem: vi.fn((key: string, value: string) => { store[key] = value }), removeItem: vi.fn((key: string) => { delete store[key] }), clear: vi.fn(() => { for (const key in store) { delete store[key] } }), key: vi.fn((index: number) => Object.keys(store)[index] || null), get length() { return Object.keys(store).length }, } Object.defineProperty(window, 'sessionStorage', { value: storageMock, writable: true, }) return storageMock } /** * Mock window.matchMedia for responsive components */ export function mockMatchMedia(defaultMatches = false): void { Object.defineProperty(window, 'matchMedia', { writable: true, value: vi.fn().mockImplementation((query: string) => ({ matches: defaultMatches, media: query, onchange: null, addListener: vi.fn(), removeListener: vi.fn(), addEventListener: vi.fn(), removeEventListener: vi.fn(), dispatchEvent: vi.fn(), })), }) } /** * Mock IntersectionObserver for lazy-loading components * Sets on both window and global for maximum compatibility */ export function mockIntersectionObserver(): void { const mockIntersectionObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), takeRecords: vi.fn(() => []), })) Object.defineProperty(window, 'IntersectionObserver', { writable: true, value: mockIntersectionObserver, }) Object.defineProperty(global, 'IntersectionObserver', { writable: true, value: mockIntersectionObserver, }) } /** * Mock ResizeObserver for responsive components * Sets on both window and global for maximum compatibility */ export function mockResizeObserver(): void { const mockResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn(), })) Object.defineProperty(window, 'ResizeObserver', { writable: true, value: mockResizeObserver, }) Object.defineProperty(global, 'ResizeObserver', { writable: true, value: mockResizeObserver, }) } /** * Mock window.scrollTo for scroll behavior * Sets on both window and global for maximum compatibility */ export function mockScrollTo(): void { const mockScrollToFn = vi.fn() Object.defineProperty(window, 'scrollTo', { writable: true, value: mockScrollToFn, }) Object.defineProperty(global, 'scrollTo', { writable: true, value: mockScrollToFn, }) } /** * Mock BroadcastChannel for cross-tab communication */ export function mockBroadcastChannel(): void { const channels: Map void>> = new Map() class MockBroadcastChannel { name: string onmessage: ((event: MessageEvent) => void) | null = null constructor(name: string) { this.name = name if (!channels.has(name)) { channels.set(name, new Set()) } } postMessage(data: unknown): void { const listeners = channels.get(this.name) if (listeners) { const event = new MessageEvent('message', { data }) listeners.forEach((listener) => { if (listener !== this.onmessage) { listener(event) } }) } } addEventListener(_type: string, listener: (event: MessageEvent) => void): void { const listeners = channels.get(this.name) if (listeners) { listeners.add(listener) } } removeEventListener(_type: string, listener: (event: MessageEvent) => void): void { const listeners = channels.get(this.name) if (listeners) { listeners.delete(listener) } } close(): void { const listeners = channels.get(this.name) if (listeners) { listeners.clear() } } } Object.defineProperty(window, 'BroadcastChannel', { writable: true, value: MockBroadcastChannel, }) }