11import mediaQuery , { MediaValues } from 'css-mediaquery' ;
2+ import './MediaQueryListEvent' ;
3+ import { MockedMediaQueryListEvent } from './MediaQueryListEvent' ;
24
35/**
46 * A tool that allows testing components that use js media queries (matchMedia)
@@ -29,51 +31,111 @@ export type MockViewport = {
2931 set : ( newDesc : ViewportDescription ) => void ;
3032} ;
3133
32- type Handler = ( ) => void ;
34+ type Listener = ( this : MediaQueryList , ev : MockedMediaQueryListEvent ) => void ;
35+ type ListenerObject = {
36+ handleEvent : ( ev : MockedMediaQueryListEvent ) => void ;
37+ } ;
38+ type ListenerOrListenerObject = Listener | ListenerObject ;
39+
40+ function isEventListenerObject (
41+ obj : ListenerOrListenerObject
42+ ) : obj is ListenerObject {
43+ return ( obj as any ) . handleEvent !== undefined ;
44+ }
3345
3446function mockViewport ( desc : ViewportDescription ) : MockViewport {
3547 const state : {
3648 currentDesc : ViewportDescription ;
37- listenerHandlers : Handler [ ] ;
49+ oldListeners : {
50+ listener : Listener ;
51+ list : MediaQueryList ;
52+ matches : boolean ;
53+ } [ ] ;
54+ listeners : {
55+ listener : ListenerOrListenerObject ;
56+ list : MediaQueryList ;
57+ matches : boolean ;
58+ } [ ] ;
3859 } = {
3960 currentDesc : desc ,
40- listenerHandlers : [ ] ,
61+ oldListeners : [ ] ,
62+ listeners : [ ] ,
4163 } ;
4264
4365 const savedImplementation = window . matchMedia ;
4466
45- const addListener = ( handler : Handler ) => {
46- state . listenerHandlers . push ( handler ) ;
67+ const addOldListener = (
68+ list : MediaQueryList ,
69+ matches : boolean ,
70+ listener : Listener
71+ ) => {
72+ state . oldListeners . push ( { listener, matches, list } ) ;
73+ } ;
74+
75+ const removeOldListener = ( listenerToRemove : Listener ) => {
76+ const index = state . oldListeners . findIndex (
77+ ( { listener } ) => listener === listenerToRemove
78+ ) ;
79+
80+ state . oldListeners . splice ( index , 1 ) ;
81+ } ;
82+
83+ const addListener = (
84+ list : MediaQueryList ,
85+ matches : boolean ,
86+ listener : ListenerOrListenerObject
87+ ) => {
88+ state . listeners . push ( { listener, matches, list } ) ;
4789 } ;
4890
49- const removeListener = ( handler : Handler ) => {
50- const index = state . listenerHandlers . findIndex ( value => value === handler ) ;
91+ const removeListener = ( listenerToRemove : ListenerOrListenerObject ) => {
92+ const index = state . listeners . findIndex (
93+ ( { listener } ) => listener === listenerToRemove
94+ ) ;
5195
52- state . listenerHandlers . splice ( index , 1 ) ;
96+ state . listeners . splice ( index , 1 ) ;
5397 } ;
5498
5599 Object . defineProperty ( window , 'matchMedia' , {
56100 writable : true ,
57- value : jest . fn ( ) . mockImplementation ( query => ( {
101+ value : ( query : string ) : MediaQueryList => ( {
58102 get matches ( ) {
59103 return mediaQuery . match ( query , state . currentDesc ) ;
60104 } ,
61105 media : query ,
62106 onchange : null ,
63- addListener, // deprecated
64- removeListener, // deprecated
65- addEventListener : ( eventType : string , handler : Handler ) => {
107+ addListener : function ( listener ) {
108+ if ( listener ) {
109+ addOldListener ( this , this . matches , listener ) ;
110+ }
111+ } , // deprecated
112+ removeListener : listener => {
113+ if ( listener ) {
114+ removeOldListener ( listener ) ;
115+ }
116+ } , // deprecated
117+ addEventListener : function (
118+ eventType : Parameters < MediaQueryList [ 'addEventListener' ] > [ 0 ] ,
119+ listener : Parameters < MediaQueryList [ 'addEventListener' ] > [ 1 ]
120+ ) {
66121 if ( eventType === 'change' ) {
67- addListener ( handler ) ;
122+ addListener ( this , this . matches , listener ) ;
68123 }
69124 } ,
70- removeEventListener : ( eventType : string , handler : Handler ) => {
125+ removeEventListener : (
126+ eventType : Parameters < MediaQueryList [ 'removeEventListener' ] > [ 0 ] ,
127+ listener : Parameters < MediaQueryList [ 'removeEventListener' ] > [ 1 ]
128+ ) => {
71129 if ( eventType === 'change' ) {
72- removeListener ( handler ) ;
130+ if ( isEventListenerObject ( listener ) ) {
131+ removeListener ( listener . handleEvent ) ;
132+ } else {
133+ removeListener ( listener ) ;
134+ }
73135 }
74136 } ,
75137 dispatchEvent : jest . fn ( ) ,
76- } ) ) ,
138+ } ) ,
77139 } ) ;
78140
79141 return {
@@ -82,7 +144,41 @@ function mockViewport(desc: ViewportDescription): MockViewport {
82144 } ,
83145 set : ( newDesc : ViewportDescription ) => {
84146 state . currentDesc = newDesc ;
85- state . listenerHandlers . forEach ( handler => handler ( ) ) ;
147+ state . listeners . forEach ( ( { listener, matches, list } , listenerIndex ) => {
148+ const newMatches = list . matches ;
149+
150+ if ( newMatches !== matches ) {
151+ const changeEvent = new MediaQueryListEvent ( 'change' , {
152+ matches : newMatches ,
153+ media : list . media ,
154+ } ) ;
155+
156+ if ( isEventListenerObject ( listener ) ) {
157+ listener . handleEvent ( changeEvent ) ;
158+ } else {
159+ listener . call ( list , changeEvent ) ;
160+ }
161+
162+ state . listeners [ listenerIndex ] . matches = newMatches ;
163+ }
164+ } ) ;
165+
166+ state . oldListeners . forEach (
167+ ( { listener, matches, list } , listenerIndex ) => {
168+ const newMatches = list . matches ;
169+
170+ if ( newMatches !== matches ) {
171+ const changeEvent = new MediaQueryListEvent ( 'change' , {
172+ matches : newMatches ,
173+ media : list . media ,
174+ } ) ;
175+
176+ listener . call ( list , changeEvent ) ;
177+
178+ state . oldListeners [ listenerIndex ] . matches = newMatches ;
179+ }
180+ }
181+ ) ;
86182 } ,
87183 } ;
88184}
0 commit comments