UNPKG

jsdom-testing-mocks

Version:

A set of tools for emulating browser behavior in jsdom environment

228 lines (197 loc) 6.59 kB
import mediaQuery, { MediaValues } from 'css-mediaquery'; import { mockMediaQueryListEvent } from './MediaQueryListEvent'; import { getConfig } from '../tools'; import { isJsdomEnv, WrongEnvironmentError } from '../helper'; const config = getConfig(); /** * A tool that allows testing components that use js media queries (matchMedia) * `mockViewport` must be called before rendering the component * @example using react testing library * * const viewport = mockViewport({ width: '320px', height: '568px' }) * * const { getByText, queryByText } = render(<TestComponent />) * * expect(getByText('Content visible only in the phone')).toBeInTheDocument() * expect(queryByText('Content visible only on desktop')).not.toBeInTheDocument() * * act(() => { * viewport.set({ width: '1440px', height: '900px' }) * }) * * expect(queryByText('Content visible only on the phone')).not.toBeInTheDocument() * expect(getByText('Content visible only on desktop')).toBeInTheDocument() * * viewport.cleanup() * */ export type ViewportDescription = Partial<MediaValues>; export type MockViewport = { cleanup: () => void; set: (newDesc: ViewportDescription) => void; }; type Listener = (this: MediaQueryList, ev: MediaQueryListEvent) => void; type ListenerObject = { handleEvent: (ev: MediaQueryListEvent) => void; }; type ListenerOrListenerObject = Listener | ListenerObject; function isEventListenerObject( obj: ListenerOrListenerObject ): obj is ListenerObject { return (obj as ListenerObject).handleEvent !== undefined; } function mockViewport(desc: ViewportDescription): MockViewport { if (!isJsdomEnv()) { throw new WrongEnvironmentError(); } mockMediaQueryListEvent(); const state: { currentDesc: ViewportDescription; oldListeners: { listener: Listener; list: MediaQueryList; matches: boolean; }[]; listeners: { listener: ListenerOrListenerObject; list: MediaQueryList; matches: boolean; }[]; } = { currentDesc: desc, oldListeners: [], listeners: [], }; const savedImplementation = window.matchMedia; const addOldListener = ( list: MediaQueryList, matches: boolean, listener: Listener ) => { state.oldListeners.push({ listener, matches, list }); }; const removeOldListener = (listenerToRemove: Listener) => { const index = state.oldListeners.findIndex( ({ listener }) => listener === listenerToRemove ); state.oldListeners.splice(index, 1); }; const addListener = ( list: MediaQueryList, matches: boolean, listener: ListenerOrListenerObject ) => { state.listeners.push({ listener, matches, list }); }; const removeListener = (listenerToRemove: ListenerOrListenerObject) => { const index = state.listeners.findIndex( ({ listener }) => listener === listenerToRemove ); state.listeners.splice(index, 1); }; Object.defineProperty(window, 'matchMedia', { writable: true, value: (query: string): MediaQueryList => ({ get matches() { return mediaQuery.match(query, state.currentDesc); }, media: query, onchange: null, addListener: function (listener) { if (listener) { addOldListener(this, this.matches, listener); } }, // deprecated removeListener: (listener) => { if (listener) { removeOldListener(listener); } }, // deprecated addEventListener: function ( eventType: Parameters<MediaQueryList['addEventListener']>[0], listener: Parameters<MediaQueryList['addEventListener']>[1] ) { if (eventType === 'change') { addListener(this, this.matches, listener); } }, removeEventListener: ( eventType: Parameters<MediaQueryList['removeEventListener']>[0], listener: Parameters<MediaQueryList['removeEventListener']>[1] ) => { if (eventType === 'change') { if (isEventListenerObject(listener)) { removeListener(listener.handleEvent); } else { removeListener(listener); } } }, dispatchEvent: (event: Event) => { if (event.type === 'change') { state.listeners.forEach(({ listener, list }) => { if (isEventListenerObject(listener)) { listener.handleEvent(event as MediaQueryListEvent); } else { listener.call(list, event as MediaQueryListEvent); } }); state.oldListeners.forEach(({ listener, list }) => { listener.call(list, event as MediaQueryListEvent); }); } return true; }, }), }); return { cleanup: () => { window.matchMedia = savedImplementation; }, set: (newDesc: ViewportDescription) => { config.act(() => { state.currentDesc = newDesc; state.listeners.forEach( ({ listener, matches, list }, listenerIndex) => { const newMatches = list.matches; if (newMatches !== matches) { const changeEvent = new MediaQueryListEvent('change', { matches: newMatches, media: list.media, }); if (isEventListenerObject(listener)) { listener.handleEvent(changeEvent); } else { listener.call(list, changeEvent); } state.listeners[listenerIndex].matches = newMatches; } } ); state.oldListeners.forEach( ({ listener, matches, list }, listenerIndex) => { const newMatches = list.matches; if (newMatches !== matches) { const changeEvent = new MediaQueryListEvent('change', { matches: newMatches, media: list.media, }); listener.call(list, changeEvent); state.oldListeners[listenerIndex].matches = newMatches; } } ); }); }, }; } function mockViewportForTestGroup(desc: ViewportDescription) { let viewport: MockViewport; config.beforeAll(() => { viewport = mockViewport(desc); }); config.afterAll(() => { viewport.cleanup(); }); } export { mockViewport, mockViewportForTestGroup };