x-on.spec.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. import { beChecked, notBeChecked, haveAttribute, haveData, haveText, test, beVisible, notBeVisible, html } from '../../utils'
  2. test('data modified in event listener updates affected attribute bindings',
  3. html`
  4. <div x-data="{ foo: 'bar' }">
  5. <button x-on:click="foo = 'baz'"></button>
  6. <span x-bind:foo="foo"></span>
  7. </div>
  8. `,
  9. ({ get }) => {
  10. get('span').should(haveAttribute('foo', 'bar'))
  11. get('button').click()
  12. get('span').should(haveAttribute('foo', 'baz'))
  13. }
  14. )
  15. test('can call a method without parenthesis',
  16. html`
  17. <div x-data="{ foo: 'bar', baz($event) { this.foo = $event.target.dataset.bob } }">
  18. <button x-on:click="baz" data-bob="lob"></button>
  19. <span x-text="foo"></span>
  20. </div>
  21. `,
  22. ({ get }) => {
  23. get('span').should(haveText('bar'))
  24. get('button').click()
  25. get('span').should(haveText('lob'))
  26. }
  27. )
  28. test('event object is not passed if other params are present',
  29. html`
  30. <div x-data="{ foo: 'bar', baz(word) { this.foo = word } }">
  31. <button x-on:click="baz('foo')" data-bob="lob"></button>
  32. <span x-text="foo"></span>
  33. </div>
  34. `,
  35. ({ get }) => {
  36. get('span').should(haveText('bar'))
  37. get('button').click()
  38. get('span').should(haveText('foo'))
  39. }
  40. )
  41. test('nested data modified in event listener updates affected attribute bindings',
  42. html`
  43. <div x-data="{ nested: { foo: 'bar' } }">
  44. <button x-on:click="nested.foo = 'baz'"></button>
  45. <span x-bind:foo="nested.foo"></span>
  46. </div>
  47. `,
  48. ({ get }) => {
  49. get('span').should(haveAttribute('foo', 'bar'))
  50. get('button').click()
  51. get('span').should(haveAttribute('foo', 'baz'))
  52. }
  53. )
  54. test('.passive modifier should disable e.preventDefault()',
  55. html`
  56. <div x-data="{ defaultPrevented: null }">
  57. <button
  58. x-on:mousedown.passive="
  59. $event.preventDefault();
  60. defaultPrevented = $event.defaultPrevented;
  61. "
  62. >
  63. <span></span>
  64. </button>
  65. </div>
  66. `,
  67. ({ get }) => {
  68. get('button').click()
  69. get('div').should(haveData('defaultPrevented', false))
  70. }
  71. )
  72. test('.stop modifier',
  73. html`
  74. <div x-data="{ foo: 'bar' }">
  75. <button x-on:click="foo = 'baz'">
  76. <h1>h1</h1>
  77. <h2 @click.stop>h2</h2>
  78. </button>
  79. </div>
  80. `,
  81. ({ get }) => {
  82. get('div').should(haveData('foo', 'bar'))
  83. get('h2').click()
  84. get('div').should(haveData('foo', 'bar'))
  85. get('h1').click()
  86. get('div').should(haveData('foo', 'baz'))
  87. }
  88. )
  89. test('.capture modifier',
  90. html`
  91. <div x-data="{ foo: 'bar', count: 0 }">
  92. <button @click.capture="count = count + 1; foo = 'baz'">
  93. <h1>h1</h1>
  94. <h2 @click="foo = 'bob'">h2</h2>
  95. </button>
  96. </div>
  97. `,
  98. ({ get }) => {
  99. get('div').should(haveData('foo', 'bar'))
  100. get('h2').click()
  101. get('div').should(haveData('foo', 'bob'))
  102. get('div').should(haveData('count', 1))
  103. }
  104. )
  105. test('.capture modifier with @keyup',
  106. html`
  107. <div x-data="{ foo: 'bar', count: 0 }">
  108. <span @keyup.capture="count = count + 1; foo = 'span'">
  109. <input type="text" @keyup="foo = 'input'">
  110. </span>
  111. </div>
  112. `,
  113. ({ get }) => {
  114. get('div').should(haveData('foo', 'bar'))
  115. get('input').type('f')
  116. get('div').should(haveData('foo', 'input'))
  117. get('div').should(haveData('count', 1))
  118. }
  119. )
  120. test('.capture modifier with @keyup and specified key',
  121. html`
  122. <div x-data="{ foo: 'bar', count: 0 }">
  123. <span @keyup.enter.capture="count = count + 1; foo = 'span'">
  124. <input type="text" @keyup.enter="foo = 'input'">
  125. </span>
  126. </div>
  127. `,
  128. ({ get }) => {
  129. get('div').should(haveData('foo', 'bar'))
  130. get('input').type('{enter}')
  131. get('div').should(haveData('foo', 'input'))
  132. get('div').should(haveData('count', 1))
  133. }
  134. )
  135. test('.self modifier',
  136. html`
  137. <div x-data="{ foo: 'bar' }">
  138. <h1 x-on:click.self="foo = 'baz'" id="selfTarget">
  139. content
  140. <button>click</button>
  141. content
  142. </h1>
  143. <span x-text="foo"></span>
  144. </div>
  145. `,
  146. ({ get }) => {
  147. get('span').should(haveText('bar'))
  148. get('button').click()
  149. get('span').should(haveText('bar'))
  150. get('h1').click()
  151. get('span').should(haveText('baz'))
  152. }
  153. )
  154. test('.prevent modifier',
  155. html`
  156. <div x-data="{}">
  157. <input type="checkbox" x-on:click.prevent>
  158. </div>
  159. `,
  160. ({ get }) => {
  161. get('input').check()
  162. get('input').should(notBeChecked())
  163. }
  164. )
  165. test('.window modifier',
  166. html`
  167. <div x-data="{ foo: 'bar' }">
  168. <div x-on:click.window="foo = 'baz'"></div>
  169. <span x-text="foo"></span>
  170. </div>
  171. `,
  172. ({ get }) => {
  173. get('span').should(haveText('bar'))
  174. get('span').click()
  175. get('span').should(haveText('baz'))
  176. }
  177. )
  178. test('expressions can start with if',
  179. html`
  180. <div x-data="{ foo: 'bar' }">
  181. <button @click="if (foo === 'bar') foo = 'baz'">click</button>
  182. <span x-text="foo"></span>
  183. </div>
  184. `,
  185. ({ get }) => {
  186. get('span').should(haveText('bar'))
  187. get('button').click()
  188. get('span').should(haveText('baz'))
  189. }
  190. )
  191. test('unbind global event handler when element is removed',
  192. html`
  193. <div x-data="{ count: 0 }">
  194. <div x-on:click.window="count++" x-ref="rmMe"></div>
  195. <button @click="$refs.rmMe.remove()">click</button>
  196. <span x-text="count"></span>
  197. </div>
  198. `,
  199. ({ get }) => {
  200. get('button').click()
  201. get('span').click()
  202. get('span').should(haveText('1'))
  203. }
  204. )
  205. test('.document modifier',
  206. html`
  207. <div x-data="{ foo: 'bar' }">
  208. <div x-on:click.document="foo = 'baz'"></div>
  209. <span x-text="foo"></span>
  210. </div>
  211. `,
  212. ({ get }) => {
  213. get('span').should(haveText('bar'))
  214. get('span').click()
  215. get('span').should(haveText('baz'))
  216. }
  217. )
  218. test('.once modifier',
  219. html`
  220. <div x-data="{ count: 0 }">
  221. <button x-on:click.once="count = count+1"></button>
  222. <span x-text="count"></span>
  223. </div>
  224. `,
  225. ({ get }) => {
  226. get('span').should(haveText('0'))
  227. get('button').click()
  228. get('span').should(haveText('1'))
  229. get('button').click()
  230. get('span').should(haveText('1'))
  231. }
  232. )
  233. test('.once modifier with @keyup',
  234. html`
  235. <div x-data="{ count: 0 }">
  236. <input type="text" x-on:keyup.once="count = count+1">
  237. <span x-text="count"></span>
  238. </div>
  239. `,
  240. ({ get }) => {
  241. get('span').should(haveText('0'))
  242. get('input').type('f')
  243. get('span').should(haveText('1'))
  244. get('input').type('o')
  245. get('span').should(haveText('1'))
  246. }
  247. )
  248. test('.once modifier with @keyup and specified key',
  249. html`
  250. <div x-data="{ count: 0 }">
  251. <input type="text" x-on:keyup.enter.once="count = count+1">
  252. <span x-text="count"></span>
  253. </div>
  254. `,
  255. ({ get }) => {
  256. get('span').should(haveText('0'))
  257. get('input').type('f')
  258. get('span').should(haveText('0'))
  259. get('input').type('{enter}')
  260. get('span').should(haveText('1'))
  261. get('input').type('{enter}')
  262. get('span').should(haveText('1'))
  263. }
  264. )
  265. test('.debounce modifier',
  266. html`
  267. <div x-data="{ count: 0 }">
  268. <input x-on:input.debounce="count = count+1">
  269. <span x-text="count"></span>
  270. </div>
  271. `,
  272. ({ get }) => {
  273. get('span').should(haveText('0'))
  274. get('input').type('f')
  275. get('span').should(haveText('1'))
  276. get('input').type('ffffffffffff')
  277. get('span').should(haveText('2'))
  278. }
  279. )
  280. test('.throttle modifier',
  281. html`
  282. <div x-data="{ count: 0 }">
  283. <input x-on:keyup.throttle.504ms="count = count+1">
  284. <span x-text="count"></span>
  285. </div>
  286. `,
  287. ({ get }) => {
  288. get('span').should(haveText('0'))
  289. get('input').type('f')
  290. get('span').should(haveText('1'))
  291. get('input').type('ffffffffffff')
  292. get('span').should(haveText('1'))
  293. }
  294. )
  295. test('keydown modifiers',
  296. html`
  297. <div x-data="{ count: 0 }">
  298. <input type="text"
  299. x-on:keydown="count++"
  300. x-on:keydown.enter="count++"
  301. x-on:keydown.space="count++"
  302. x-on:keydown.up="count++"
  303. x-on:keydown.down="count++"
  304. x-on:keydown.right="count++"
  305. x-on:keydown.left="count++"
  306. x-on:keydown.cmd="count++"
  307. x-on:keydown.meta="count++"
  308. x-on:keydown.escape="count++"
  309. x-on:keydown.esc="count++"
  310. x-on:keydown.ctrl="count++"
  311. x-on:keydown.slash="count++"
  312. x-on:keydown.period="count++"
  313. x-on:keydown.equal="count++"
  314. >
  315. <span x-text="count"></span>
  316. </div>
  317. `,
  318. ({ get }) => {
  319. get('span').should(haveText('0'))
  320. get('input').type('f')
  321. get('span').should(haveText('1'))
  322. get('input').type('{enter}')
  323. get('span').should(haveText('3'))
  324. get('input').type(' ')
  325. get('span').should(haveText('5'))
  326. get('input').type('{leftarrow}')
  327. get('span').should(haveText('7'))
  328. get('input').type('{rightarrow}')
  329. get('span').should(haveText('9'))
  330. get('input').type('{uparrow}')
  331. get('span').should(haveText('11'))
  332. get('input').type('{downarrow}')
  333. get('span').should(haveText('13'))
  334. get('input').type('{meta}')
  335. get('span').should(haveText('16'))
  336. get('input').type('{esc}')
  337. get('span').should(haveText('19'))
  338. get('input').type('{ctrl}')
  339. get('span').should(haveText('21'))
  340. get('input').type('/')
  341. get('span').should(haveText('23'))
  342. get('input').type('=')
  343. get('span').should(haveText('25'))
  344. get('input').type('.')
  345. get('span').should(haveText('27'))
  346. }
  347. )
  348. test('discerns between space minus underscore',
  349. html`
  350. <div x-data="{ count: 0 }">
  351. <input id="space" type="text" x-on:keydown.space="count++" />
  352. <input id="minus" type="text" x-on:keydown.-="count++" />
  353. <input id="underscore" type="text" x-on:keydown._="count++" />
  354. <span x-text="count"></span>
  355. </div>
  356. `,
  357. ({get}) => {
  358. get('span').should(haveText('0'))
  359. get('#space').type(' ')
  360. get('span').should(haveText('1'))
  361. get('#space').type('-')
  362. get('span').should(haveText('1'))
  363. get('#minus').type('-')
  364. get('span').should(haveText('2'))
  365. get('#minus').type(' ')
  366. get('span').should(haveText('2'))
  367. get('#underscore').type('_')
  368. get('span').should(haveText('3'))
  369. get('#underscore').type(' ')
  370. get('span').should(haveText('3'))
  371. })
  372. test('keydown combo modifiers',
  373. html`
  374. <div x-data="{ count: 0 }">
  375. <input type="text" x-on:keydown.cmd.enter="count++">
  376. <span x-text="count"></span>
  377. </div>
  378. `,
  379. ({ get }) => {
  380. get('span').should(haveText('0'))
  381. get('input').type('f')
  382. get('span').should(haveText('0'))
  383. get('input').type('{cmd}{enter}')
  384. get('span').should(haveText('1'))
  385. }
  386. )
  387. test('keydown with specified key and stop modifier only stops for specified key',
  388. html`
  389. <div x-data="{ count: 0 }">
  390. <article x-on:keydown="count++">
  391. <input type="text" x-on:keydown.enter.stop>
  392. </article>
  393. <span x-text="count"></span>
  394. </div>
  395. `,
  396. ({ get }) => {
  397. get('span').should(haveText('0'))
  398. get('input').type('f')
  399. get('span').should(haveText('1'))
  400. get('input').type('{enter}')
  401. get('span').should(haveText('1'))
  402. }
  403. )
  404. test('@click.away',
  405. html`
  406. <div x-data="{ foo: 'bar' }">
  407. <h1 @click.away="foo = 'baz'">h1</h1>
  408. <h2>h2</h2>
  409. <span x-text="foo"></span>
  410. </div>
  411. `,
  412. ({ get }) => {
  413. get('span').should(haveText('bar'))
  414. get('h1').click()
  415. get('span').should(haveText('bar'))
  416. get('h2').click()
  417. get('span').should(haveText('baz'))
  418. }
  419. )
  420. test('@click.away with x-show (prevent race condition)',
  421. html`
  422. <div x-data="{ show: false }">
  423. <button @click="show = true">Show</button>
  424. <h1 x-show="show" @click.away="show = false">h1</h1>
  425. <h2>h2</h2>
  426. </div>
  427. `,
  428. ({ get }) => {
  429. get('h1').should(notBeVisible())
  430. get('button').click()
  431. get('h1').should(beVisible())
  432. }
  433. )
  434. test('event with colon',
  435. html`
  436. <div x-data="{ foo: 'bar' }">
  437. <div x-on:my:event.document="foo = 'baz'"></div>
  438. <button @click="document.dispatchEvent(new CustomEvent('my:event', { bubbles: true }))">click</button>
  439. <span x-text="foo"></span>
  440. </div>
  441. `,
  442. ({ get }) => {
  443. get('span').should(haveText('bar'))
  444. get('button').click()
  445. get('span').should(haveText('baz'))
  446. }
  447. )
  448. test('event instance can be passed to method reference',
  449. html`
  450. <div x-data="{ foo: 'bar', changeFoo(e) { this.foo = e.target.id } }">
  451. <button x-on:click="changeFoo" id="baz"></button>
  452. <span x-text="foo"></span>
  453. </div>
  454. `,
  455. ({ get }) => {
  456. get('span').should(haveText('bar'))
  457. get('button').click()
  458. get('span').should(haveText('baz'))
  459. }
  460. )
  461. test('.camel modifier correctly binds event listener',
  462. html`
  463. <div x-data="{ foo: 'bar' }" x-on:event-name.camel="foo = 'baz'">
  464. <button x-on:click="$dispatch('eventName')"></button>
  465. <span x-text="foo"></span>
  466. </div>
  467. `,
  468. ({ get }) => {
  469. get('span').should(haveText('bar'))
  470. get('button').click()
  471. get('span').should(haveText('baz'))
  472. }
  473. )
  474. test('.camel modifier correctly binds event listener with namespace',
  475. html`
  476. <div x-data="{ foo: 'bar' }" x-on:ns:event-name.camel="foo = 'baz'">
  477. <button x-on:click="$dispatch('ns:eventName')"></button>
  478. <span x-text="foo"></span>
  479. </div>
  480. `,
  481. ({ get }) => {
  482. get('span').should(haveText('bar'))
  483. get('button').click()
  484. get('span').should(haveText('baz'))
  485. }
  486. )
  487. test('.dot modifier correctly binds event listener',
  488. html`
  489. <div x-data="{ foo: 'bar' }" x-on:event-name.dot="foo = 'baz'">
  490. <button x-on:click="$dispatch('event.name')"></button>
  491. <span x-text="foo"></span>
  492. </div>
  493. `,
  494. ({ get }) => {
  495. get('span').should(haveText('bar'))
  496. get('button').click()
  497. get('span').should(haveText('baz'))
  498. }
  499. )
  500. test('.dot modifier correctly binds event listener with namespace',
  501. html`
  502. <div x-data="{ foo: 'bar' }" x-on:ns:event-name.dot="foo = 'baz'">
  503. <button x-on:click="$dispatch('ns:event.name')"></button>
  504. <span x-text="foo"></span>
  505. </div>
  506. `,
  507. ({ get }) => {
  508. get('span').should(haveText('bar'))
  509. get('button').click()
  510. get('span').should(haveText('baz'))
  511. }
  512. )
  513. test('handles await in handlers with invalid right hand expressions',
  514. html`
  515. <div x-data="{ text: 'original' }">
  516. <button @click="let value = 'new string'; text = await Promise.resolve(value)"></button>
  517. <span x-text="text"></span>
  518. </div>
  519. `,
  520. ({ get }) => {
  521. get('span').should(haveText('original'))
  522. get('button').click()
  523. get('span').should(haveText('new string'))
  524. }
  525. )