event-handler.spec.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. import EventHandler from '../../../src/dom/event-handler.js'
  2. import { noop } from '../../../src/util/index.js'
  3. import { clearFixture, getFixture } from '../../helpers/fixture.js'
  4. describe('EventHandler', () => {
  5. let fixtureEl
  6. beforeAll(() => {
  7. fixtureEl = getFixture()
  8. })
  9. afterEach(() => {
  10. clearFixture()
  11. })
  12. describe('on', () => {
  13. it('should not add event listener if the event is not a string', () => {
  14. fixtureEl.innerHTML = '<div></div>'
  15. const div = fixtureEl.querySelector('div')
  16. EventHandler.on(div, null, noop)
  17. EventHandler.on(null, 'click', noop)
  18. expect().nothing()
  19. })
  20. it('should add event listener', () => {
  21. return new Promise(resolve => {
  22. fixtureEl.innerHTML = '<div></div>'
  23. const div = fixtureEl.querySelector('div')
  24. EventHandler.on(div, 'click', () => {
  25. expect().nothing()
  26. resolve()
  27. })
  28. div.click()
  29. })
  30. })
  31. it('should add namespaced event listener', () => {
  32. return new Promise(resolve => {
  33. fixtureEl.innerHTML = '<div></div>'
  34. const div = fixtureEl.querySelector('div')
  35. EventHandler.on(div, 'bs.namespace', () => {
  36. expect().nothing()
  37. resolve()
  38. })
  39. EventHandler.trigger(div, 'bs.namespace')
  40. })
  41. })
  42. it('should add native namespaced event listener', () => {
  43. return new Promise(resolve => {
  44. fixtureEl.innerHTML = '<div></div>'
  45. const div = fixtureEl.querySelector('div')
  46. EventHandler.on(div, 'click.namespace', () => {
  47. expect().nothing()
  48. resolve()
  49. })
  50. EventHandler.trigger(div, 'click')
  51. })
  52. })
  53. it('should handle event delegation', () => {
  54. return new Promise(resolve => {
  55. EventHandler.on(document, 'click', '.test', () => {
  56. expect().nothing()
  57. resolve()
  58. })
  59. fixtureEl.innerHTML = '<div class="test"></div>'
  60. const div = fixtureEl.querySelector('div')
  61. div.click()
  62. })
  63. })
  64. it('should handle mouseenter/mouseleave like the native counterpart', () => {
  65. return new Promise(resolve => {
  66. fixtureEl.innerHTML = [
  67. '<div class="outer">',
  68. '<div class="inner">',
  69. '<div class="nested">',
  70. '<div class="deep"></div>',
  71. '</div>',
  72. '</div>',
  73. '<div class="sibling"></div>',
  74. '</div>'
  75. ].join('')
  76. const outer = fixtureEl.querySelector('.outer')
  77. const inner = fixtureEl.querySelector('.inner')
  78. const nested = fixtureEl.querySelector('.nested')
  79. const deep = fixtureEl.querySelector('.deep')
  80. const sibling = fixtureEl.querySelector('.sibling')
  81. const enterSpy = jasmine.createSpy('mouseenter')
  82. const leaveSpy = jasmine.createSpy('mouseleave')
  83. const delegateEnterSpy = jasmine.createSpy('mouseenter')
  84. const delegateLeaveSpy = jasmine.createSpy('mouseleave')
  85. EventHandler.on(inner, 'mouseenter', enterSpy)
  86. EventHandler.on(inner, 'mouseleave', leaveSpy)
  87. EventHandler.on(outer, 'mouseenter', '.inner', delegateEnterSpy)
  88. EventHandler.on(outer, 'mouseleave', '.inner', delegateLeaveSpy)
  89. EventHandler.on(sibling, 'mouseenter', () => {
  90. expect(enterSpy.calls.count()).toEqual(2)
  91. expect(leaveSpy.calls.count()).toEqual(2)
  92. expect(delegateEnterSpy.calls.count()).toEqual(2)
  93. expect(delegateLeaveSpy.calls.count()).toEqual(2)
  94. resolve()
  95. })
  96. const moveMouse = (from, to) => {
  97. from.dispatchEvent(new MouseEvent('mouseout', {
  98. bubbles: true,
  99. relatedTarget: to
  100. }))
  101. to.dispatchEvent(new MouseEvent('mouseover', {
  102. bubbles: true,
  103. relatedTarget: from
  104. }))
  105. }
  106. // from outer to deep and back to outer (nested)
  107. moveMouse(outer, inner)
  108. moveMouse(inner, nested)
  109. moveMouse(nested, deep)
  110. moveMouse(deep, nested)
  111. moveMouse(nested, inner)
  112. moveMouse(inner, outer)
  113. setTimeout(() => {
  114. expect(enterSpy.calls.count()).toEqual(1)
  115. expect(leaveSpy.calls.count()).toEqual(1)
  116. expect(delegateEnterSpy.calls.count()).toEqual(1)
  117. expect(delegateLeaveSpy.calls.count()).toEqual(1)
  118. // from outer to inner to sibling (adjacent)
  119. moveMouse(outer, inner)
  120. moveMouse(inner, sibling)
  121. }, 20)
  122. })
  123. })
  124. })
  125. describe('one', () => {
  126. it('should call listener just once', () => {
  127. return new Promise(resolve => {
  128. fixtureEl.innerHTML = '<div></div>'
  129. let called = 0
  130. const div = fixtureEl.querySelector('div')
  131. const obj = {
  132. oneListener() {
  133. called++
  134. }
  135. }
  136. EventHandler.one(div, 'bootstrap', obj.oneListener)
  137. EventHandler.trigger(div, 'bootstrap')
  138. EventHandler.trigger(div, 'bootstrap')
  139. setTimeout(() => {
  140. expect(called).toEqual(1)
  141. resolve()
  142. }, 20)
  143. })
  144. })
  145. it('should call delegated listener just once', () => {
  146. return new Promise(resolve => {
  147. fixtureEl.innerHTML = '<div></div>'
  148. let called = 0
  149. const div = fixtureEl.querySelector('div')
  150. const obj = {
  151. oneListener() {
  152. called++
  153. }
  154. }
  155. EventHandler.one(fixtureEl, 'bootstrap', 'div', obj.oneListener)
  156. EventHandler.trigger(div, 'bootstrap')
  157. EventHandler.trigger(div, 'bootstrap')
  158. setTimeout(() => {
  159. expect(called).toEqual(1)
  160. resolve()
  161. }, 20)
  162. })
  163. })
  164. })
  165. describe('off', () => {
  166. it('should not remove a listener', () => {
  167. fixtureEl.innerHTML = '<div></div>'
  168. const div = fixtureEl.querySelector('div')
  169. EventHandler.off(div, null, noop)
  170. EventHandler.off(null, 'click', noop)
  171. expect().nothing()
  172. })
  173. it('should remove a listener', () => {
  174. return new Promise(resolve => {
  175. fixtureEl.innerHTML = '<div></div>'
  176. const div = fixtureEl.querySelector('div')
  177. let called = 0
  178. const handler = () => {
  179. called++
  180. }
  181. EventHandler.on(div, 'foobar', handler)
  182. EventHandler.trigger(div, 'foobar')
  183. EventHandler.off(div, 'foobar', handler)
  184. EventHandler.trigger(div, 'foobar')
  185. setTimeout(() => {
  186. expect(called).toEqual(1)
  187. resolve()
  188. }, 20)
  189. })
  190. })
  191. it('should remove all the events', () => {
  192. return new Promise(resolve => {
  193. fixtureEl.innerHTML = '<div></div>'
  194. const div = fixtureEl.querySelector('div')
  195. let called = 0
  196. EventHandler.on(div, 'foobar', () => {
  197. called++
  198. })
  199. EventHandler.on(div, 'foobar', () => {
  200. called++
  201. })
  202. EventHandler.trigger(div, 'foobar')
  203. EventHandler.off(div, 'foobar')
  204. EventHandler.trigger(div, 'foobar')
  205. setTimeout(() => {
  206. expect(called).toEqual(2)
  207. resolve()
  208. }, 20)
  209. })
  210. })
  211. it('should remove all the namespaced listeners if namespace is passed', () => {
  212. return new Promise(resolve => {
  213. fixtureEl.innerHTML = '<div></div>'
  214. const div = fixtureEl.querySelector('div')
  215. let called = 0
  216. EventHandler.on(div, 'foobar.namespace', () => {
  217. called++
  218. })
  219. EventHandler.on(div, 'foofoo.namespace', () => {
  220. called++
  221. })
  222. EventHandler.trigger(div, 'foobar.namespace')
  223. EventHandler.trigger(div, 'foofoo.namespace')
  224. EventHandler.off(div, '.namespace')
  225. EventHandler.trigger(div, 'foobar.namespace')
  226. EventHandler.trigger(div, 'foofoo.namespace')
  227. setTimeout(() => {
  228. expect(called).toEqual(2)
  229. resolve()
  230. }, 20)
  231. })
  232. })
  233. it('should remove the namespaced listeners', () => {
  234. return new Promise(resolve => {
  235. fixtureEl.innerHTML = '<div></div>'
  236. const div = fixtureEl.querySelector('div')
  237. let calledCallback1 = 0
  238. let calledCallback2 = 0
  239. EventHandler.on(div, 'foobar.namespace', () => {
  240. calledCallback1++
  241. })
  242. EventHandler.on(div, 'foofoo.namespace', () => {
  243. calledCallback2++
  244. })
  245. EventHandler.trigger(div, 'foobar.namespace')
  246. EventHandler.off(div, 'foobar.namespace')
  247. EventHandler.trigger(div, 'foobar.namespace')
  248. EventHandler.trigger(div, 'foofoo.namespace')
  249. setTimeout(() => {
  250. expect(calledCallback1).toEqual(1)
  251. expect(calledCallback2).toEqual(1)
  252. resolve()
  253. }, 20)
  254. })
  255. })
  256. it('should remove the all the namespaced listeners for native events', () => {
  257. return new Promise(resolve => {
  258. fixtureEl.innerHTML = '<div></div>'
  259. const div = fixtureEl.querySelector('div')
  260. let called = 0
  261. EventHandler.on(div, 'click.namespace', () => {
  262. called++
  263. })
  264. EventHandler.on(div, 'click.namespace2', () => {
  265. called++
  266. })
  267. EventHandler.trigger(div, 'click')
  268. EventHandler.off(div, 'click')
  269. EventHandler.trigger(div, 'click')
  270. setTimeout(() => {
  271. expect(called).toEqual(2)
  272. resolve()
  273. }, 20)
  274. })
  275. })
  276. it('should remove the specified namespaced listeners for native events', () => {
  277. return new Promise(resolve => {
  278. fixtureEl.innerHTML = '<div></div>'
  279. const div = fixtureEl.querySelector('div')
  280. let called1 = 0
  281. let called2 = 0
  282. EventHandler.on(div, 'click.namespace', () => {
  283. called1++
  284. })
  285. EventHandler.on(div, 'click.namespace2', () => {
  286. called2++
  287. })
  288. EventHandler.trigger(div, 'click')
  289. EventHandler.off(div, 'click.namespace')
  290. EventHandler.trigger(div, 'click')
  291. setTimeout(() => {
  292. expect(called1).toEqual(1)
  293. expect(called2).toEqual(2)
  294. resolve()
  295. }, 20)
  296. })
  297. })
  298. it('should remove a listener registered by .one', () => {
  299. return new Promise((resolve, reject) => {
  300. fixtureEl.innerHTML = '<div></div>'
  301. const div = fixtureEl.querySelector('div')
  302. const handler = () => {
  303. reject(new Error('called'))
  304. }
  305. EventHandler.one(div, 'foobar', handler)
  306. EventHandler.off(div, 'foobar', handler)
  307. EventHandler.trigger(div, 'foobar')
  308. setTimeout(() => {
  309. expect().nothing()
  310. resolve()
  311. }, 20)
  312. })
  313. })
  314. it('should remove the correct delegated event listener', () => {
  315. const element = document.createElement('div')
  316. const subelement = document.createElement('span')
  317. element.append(subelement)
  318. const anchor = document.createElement('a')
  319. element.append(anchor)
  320. let i = 0
  321. const handler = () => {
  322. i++
  323. }
  324. EventHandler.on(element, 'click', 'a', handler)
  325. EventHandler.on(element, 'click', 'span', handler)
  326. fixtureEl.append(element)
  327. EventHandler.trigger(anchor, 'click')
  328. EventHandler.trigger(subelement, 'click')
  329. // first listeners called
  330. expect(i).toEqual(2)
  331. EventHandler.off(element, 'click', 'span', handler)
  332. EventHandler.trigger(subelement, 'click')
  333. // removed listener not called
  334. expect(i).toEqual(2)
  335. EventHandler.trigger(anchor, 'click')
  336. // not removed listener called
  337. expect(i).toEqual(3)
  338. EventHandler.on(element, 'click', 'span', handler)
  339. EventHandler.trigger(anchor, 'click')
  340. EventHandler.trigger(subelement, 'click')
  341. // listener re-registered
  342. expect(i).toEqual(5)
  343. EventHandler.off(element, 'click', 'span')
  344. EventHandler.trigger(subelement, 'click')
  345. // listener removed again
  346. expect(i).toEqual(5)
  347. })
  348. })
  349. describe('general functionality', () => {
  350. it('should hydrate properties, and make them configurable', () => {
  351. return new Promise(resolve => {
  352. fixtureEl.innerHTML = [
  353. '<div id="div1">',
  354. ' <div id="div2"></div>',
  355. ' <div id="div3"></div>',
  356. '</div>'
  357. ].join('')
  358. const div1 = fixtureEl.querySelector('#div1')
  359. const div2 = fixtureEl.querySelector('#div2')
  360. EventHandler.on(div1, 'click', event => {
  361. expect(event.currentTarget).toBe(div2)
  362. expect(event.delegateTarget).toBe(div1)
  363. expect(event.originalTarget).toBeNull()
  364. Object.defineProperty(event, 'currentTarget', {
  365. configurable: true,
  366. get() {
  367. return div1
  368. }
  369. })
  370. expect(event.currentTarget).toBe(div1)
  371. resolve()
  372. })
  373. expect(() => {
  374. EventHandler.trigger(div1, 'click', { originalTarget: null, currentTarget: div2 })
  375. }).not.toThrowError(TypeError)
  376. })
  377. })
  378. })
  379. })