platform-codebase/@packages/@testing/test-utils/src/mocks/browser.ts
2026-01-18 09:20:18 -08:00

213 lines
5 KiB
TypeScript
Executable file

/**
* 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<string, string> = {}
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<string, string> = {}
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<string, Set<(event: MessageEvent) => 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,
})
}