import { afterEach, describe, expect, it, vi } from 'vitest' import { act, cleanup, render, screen } from '@testing-library/react' import '@testing-library/jest-dom' import { useMobileViewport } from './useMobileViewport' function Probe() { const isMobile = useMobileViewport() return
{isMobile ? 'mobile' : 'desktop'}
} function createMatchMediaController(initialMatches = false, legacy = false) { let matches = initialMatches const listeners = new Set<(event: MediaQueryListEvent) => void>() const addListener = vi.fn((listener: (event: MediaQueryListEvent) => void) => { listeners.add(listener) }) const removeListener = vi.fn((listener: (event: MediaQueryListEvent) => void) => { listeners.delete(listener) }) const matchMedia = vi.fn().mockImplementation((query: string) => ({ matches, media: query, onchange: null, addEventListener: legacy ? undefined : vi.fn((type: string, listener: (event: MediaQueryListEvent) => void) => { if (type === 'change') listeners.add(listener) }), removeEventListener: legacy ? undefined : vi.fn((type: string, listener: (event: MediaQueryListEvent) => void) => { if (type === 'change') listeners.delete(listener) }), addListener, removeListener, dispatchEvent: vi.fn(), })) return { matchMedia, addListener, removeListener, emit(nextMatches: boolean) { matches = nextMatches const event = { matches: nextMatches, media: '(max-width: 767px)' } as MediaQueryListEvent listeners.forEach((listener) => listener(event)) }, getListenerCount() { return listeners.size }, } } describe('useMobileViewport', () => { const originalMatchMedia = window.matchMedia afterEach(() => { cleanup() if (originalMatchMedia) { window.matchMedia = originalMatchMedia } else { Reflect.deleteProperty(window, 'matchMedia') } }) it('defaults to desktop when matchMedia is unavailable', () => { Reflect.deleteProperty(window, 'matchMedia') render() expect(screen.getByTestId('viewport-state')).toHaveTextContent('desktop') }) it('tracks viewport changes and removes the listener on cleanup', () => { const controller = createMatchMediaController(false) window.matchMedia = controller.matchMedia as typeof window.matchMedia const { unmount } = render() expect(screen.getByTestId('viewport-state')).toHaveTextContent('desktop') expect(controller.matchMedia).toHaveBeenCalledWith('(max-width: 767px)') expect(controller.getListenerCount()).toBe(1) act(() => { controller.emit(true) }) expect(screen.getByTestId('viewport-state')).toHaveTextContent('mobile') unmount() expect(controller.getListenerCount()).toBe(0) }) it('reads the initial media query before first paint', () => { const controller = createMatchMediaController(true) window.matchMedia = controller.matchMedia as typeof window.matchMedia render() expect(screen.getByTestId('viewport-state')).toHaveTextContent('mobile') }) it('falls back to legacy media query listeners', () => { const controller = createMatchMediaController(false, true) window.matchMedia = controller.matchMedia as typeof window.matchMedia const { unmount } = render() expect(controller.addListener).toHaveBeenCalledTimes(1) act(() => { controller.emit(true) }) expect(screen.getByTestId('viewport-state')).toHaveTextContent('mobile') unmount() expect(controller.removeListener).toHaveBeenCalledTimes(1) expect(controller.getListenerCount()).toBe(0) }) })