listbox.spec.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557
  1. import { beVisible, haveAttribute, haveClasses, haveText, html, notBeVisible, notHaveAttribute, notExist, haveFocus, test } from '../../../utils'
  2. test('it works with x-model',
  3. [html`
  4. <div
  5. x-data="{ active: null, people: [
  6. { id: 1, name: 'Wade Cooper' },
  7. { id: 2, name: 'Arlene Mccoy' },
  8. { id: 3, name: 'Devon Webb' },
  9. { id: 4, name: 'Tom Cook' },
  10. { id: 5, name: 'Tanya Fox', disabled: true },
  11. { id: 6, name: 'Hellen Schmidt' },
  12. { id: 7, name: 'Caroline Schultz' },
  13. { id: 8, name: 'Mason Heaney' },
  14. { id: 9, name: 'Claudie Smitham' },
  15. { id: 10, name: 'Emil Schaefer' },
  16. ]}"
  17. x-listbox
  18. x-model="active"
  19. >
  20. <label x-listbox:label>Assigned to</label>
  21. <button x-listbox:button x-text="active ? active.name : 'Select Person'"></button>
  22. <ul x-listbox:options>
  23. <template x-for="person in people" :key="person.id">
  24. <li
  25. :option="person.id"
  26. x-listbox:option
  27. :value="person"
  28. :disabled="person.disabled"
  29. >
  30. <span x-text="person.name"></span>
  31. </li>
  32. </template>
  33. </ul>
  34. </div>
  35. `],
  36. ({ get }) => {
  37. get('ul').should(notBeVisible())
  38. get('button')
  39. .should(haveText('Select Person'))
  40. .click()
  41. get('ul').should(beVisible())
  42. get('button').click()
  43. get('ul').should(notBeVisible())
  44. get('button').click()
  45. get('[option="2"]').click()
  46. get('ul').should(notBeVisible())
  47. get('button').should(haveText('Arlene Mccoy'))
  48. },
  49. )
  50. test('it works with internal state',
  51. [html`
  52. <div
  53. x-data="{ people: [
  54. { id: 1, name: 'Wade Cooper' },
  55. { id: 2, name: 'Arlene Mccoy' },
  56. { id: 3, name: 'Devon Webb' },
  57. { id: 4, name: 'Tom Cook' },
  58. { id: 5, name: 'Tanya Fox', disabled: true },
  59. { id: 6, name: 'Hellen Schmidt' },
  60. { id: 7, name: 'Caroline Schultz' },
  61. { id: 8, name: 'Mason Heaney' },
  62. { id: 9, name: 'Claudie Smitham' },
  63. { id: 10, name: 'Emil Schaefer' },
  64. ]}"
  65. x-listbox
  66. >
  67. <label x-listbox:label>Assigned to</label>
  68. <button x-listbox:button x-text="$listbox.selected ? $listbox.selected.name : 'Select Person'"></button>
  69. <ul x-listbox:options>
  70. <template x-for="person in people" :key="person.id">
  71. <li
  72. :option="person.id"
  73. x-listbox:option
  74. :value="person"
  75. :disabled="person.disabled"
  76. >
  77. <span x-text="person.name"></span>
  78. </li>
  79. </template>
  80. </ul>
  81. </div>
  82. `],
  83. ({ get }) => {
  84. get('ul').should(notBeVisible())
  85. get('button')
  86. .should(haveText('Select Person'))
  87. .click()
  88. get('ul').should(beVisible())
  89. get('button').click()
  90. get('ul').should(notBeVisible())
  91. get('button').click()
  92. get('[option="2"]').click()
  93. get('ul').should(notBeVisible())
  94. get('button').should(haveText('Arlene Mccoy'))
  95. },
  96. )
  97. test('$listbox/$listboxOption',
  98. [html`
  99. <div
  100. x-data="{ people: [
  101. { id: 1, name: 'Wade Cooper' },
  102. { id: 2, name: 'Arlene Mccoy' },
  103. { id: 3, name: 'Devon Webb' },
  104. { id: 4, name: 'Tom Cook' },
  105. { id: 5, name: 'Tanya Fox', disabled: true },
  106. { id: 6, name: 'Hellen Schmidt' },
  107. { id: 7, name: 'Caroline Schultz' },
  108. { id: 8, name: 'Mason Heaney' },
  109. { id: 9, name: 'Claudie Smitham' },
  110. { id: 10, name: 'Emil Schaefer' },
  111. ]}"
  112. x-listbox
  113. >
  114. <label x-listbox:label>Assigned to</label>
  115. <button x-listbox:button x-text="$listbox.selected ? $listbox.selected.name : 'Select Person'"></button>
  116. <article x-text="$listbox.active?.name"></article>
  117. <ul x-listbox:options>
  118. <template x-for="person in people" :key="person.id">
  119. <li
  120. :option="person.id"
  121. x-listbox:option
  122. :value="person"
  123. :disabled="person.disabled"
  124. :class="{
  125. 'selected': $listboxOption.isSelected,
  126. 'active': $listboxOption.isActive,
  127. 'disabled': $listboxOption.isDisabled,
  128. }"
  129. >
  130. <span x-text="person.name"></span>
  131. </li>
  132. </template>
  133. </ul>
  134. </div>
  135. `],
  136. ({ get }) => {
  137. get('article').should(haveText(''))
  138. get('[option="5"]').should(haveClasses(['disabled']))
  139. get('button')
  140. .should(haveText('Select Person'))
  141. .click()
  142. get('article').should(haveText('Wade Cooper'))
  143. get('[option="1"]').should(haveClasses(['active']))
  144. get('ul').type('{downarrow}')
  145. get('article').should(haveText('Arlene Mccoy'))
  146. get('[option="2"]').should(haveClasses(['active']))
  147. get('button').should(haveText('Select Person'))
  148. get('[option="2"]').click()
  149. get('button').should(haveText('Arlene Mccoy'))
  150. get('[option="2"]').should(haveClasses(['selected']))
  151. },
  152. )
  153. // @todo support "name" prop
  154. test('"name" prop',
  155. [html`
  156. <div
  157. x-data="{ people: [
  158. { id: 1, name: 'Wade Cooper' },
  159. { id: 2, name: 'Arlene Mccoy' },
  160. { id: 3, name: 'Devon Webb' },
  161. { id: 4, name: 'Tom Cook' },
  162. { id: 5, name: 'Tanya Fox', disabled: true },
  163. { id: 6, name: 'Hellen Schmidt' },
  164. { id: 7, name: 'Caroline Schultz' },
  165. { id: 8, name: 'Mason Heaney' },
  166. { id: 9, name: 'Claudie Smitham' },
  167. { id: 10, name: 'Emil Schaefer' },
  168. ]}"
  169. x-listbox
  170. name="person"
  171. >
  172. <label x-listbox:label>Assigned to</label>
  173. <button x-listbox:button x-text="$listbox.selected ? $listbox.selected : 'Select Person'"></button>
  174. <ul x-listbox:options>
  175. <template x-for="person in people" :key="person.id">
  176. <li
  177. :option="person.id"
  178. x-listbox:option
  179. :value="person.id"
  180. :disabled="person.disabled"
  181. :class="{
  182. 'selected': $listboxOption.isSelected,
  183. 'active': $listboxOption.isActive,
  184. }"
  185. >
  186. <span x-text="person.name"></span>
  187. </li>
  188. </template>
  189. </ul>
  190. </div>
  191. `],
  192. ({ get }) => {
  193. get('input').should(notExist())
  194. get('input').should(beHidden())
  195. .should(haveAttribute('name', 'person'))
  196. .should(haveAttribute('value', '2'))
  197. .should(haveAttribute('type', 'hidden'))
  198. get('[option="2"]').click()
  199. get('[option="4"]').click()
  200. get('input').should(beHidden())
  201. .should(haveAttribute('name', 'person'))
  202. .should(haveAttribute('value', '4'))
  203. .should(haveAttribute('type', 'hidden'))
  204. },
  205. );
  206. // @todo support "default-value" prop
  207. test('"default-value" prop',
  208. [html`
  209. <div
  210. x-data="{ people: [
  211. { id: 1, name: 'Wade Cooper' },
  212. { id: 2, name: 'Arlene Mccoy' },
  213. { id: 3, name: 'Devon Webb' },
  214. { id: 4, name: 'Tom Cook' },
  215. { id: 5, name: 'Tanya Fox', disabled: true },
  216. { id: 6, name: 'Hellen Schmidt' },
  217. { id: 7, name: 'Caroline Schultz' },
  218. { id: 8, name: 'Mason Heaney' },
  219. { id: 9, name: 'Claudie Smitham' },
  220. { id: 10, name: 'Emil Schaefer' },
  221. ]}"
  222. x-listbox
  223. name="person"
  224. default-value="2"
  225. >
  226. <label x-listbox:label>Assigned to</label>
  227. <button x-listbox:button x-text="$listbox.selected ? $listbox.selected : 'Select Person'"></button>
  228. <ul x-listbox:options>
  229. <template x-for="person in people" :key="person.id">
  230. <li
  231. :option="person.id"
  232. x-listbox:option
  233. :value="person.id"
  234. :disabled="person.disabled"
  235. :class="{
  236. 'selected': $listboxOption.isSelected,
  237. 'active': $listboxOption.isActive,
  238. }"
  239. >
  240. <span x-text="person.name"></span>
  241. </li>
  242. </template>
  243. </ul>
  244. </div>
  245. `],
  246. ({ get }) => {
  247. get('input').should(beHidden())
  248. .should(haveAttribute('name', 'person'))
  249. .should(haveAttribute('value', '2'))
  250. .should(haveAttribute('type', 'hidden'))
  251. },
  252. );
  253. // @todo support "multiple" prop
  254. test.only('"multiple" prop',
  255. [html`
  256. <div
  257. x-data="{
  258. value: [],
  259. people: [
  260. { id: 1, name: 'Wade Cooper' },
  261. { id: 2, name: 'Arlene Mccoy' },
  262. { id: 3, name: 'Devon Webb' },
  263. { id: 4, name: 'Tom Cook' },
  264. { id: 5, name: 'Tanya Fox', disabled: true },
  265. { id: 6, name: 'Hellen Schmidt' },
  266. { id: 7, name: 'Caroline Schultz' },
  267. { id: 8, name: 'Mason Heaney' },
  268. { id: 9, name: 'Claudie Smitham' },
  269. { id: 10, name: 'Emil Schaefer' },
  270. ]
  271. }"
  272. x-listbox
  273. multiple
  274. >
  275. <label x-listbox:label>Assigned to</label>
  276. <button x-listbox:button x-text="$listbox.selected ? $listbox.selected.join(',') : 'Select People'"></button>
  277. <ul x-listbox:options>
  278. <template x-for="person in people" :key="person.id">
  279. <li
  280. :option="person.id"
  281. x-listbox:option
  282. :value="person.id"
  283. :disabled="person.disabled"
  284. :class="{
  285. 'selected': $listboxOption.isSelected,
  286. 'active': $listboxOption.isActive,
  287. }"
  288. >
  289. <span x-text="person.name"></span>
  290. </li>
  291. </template>
  292. </ul>
  293. </div>
  294. `],
  295. ({ get }) => {
  296. get('button').click()
  297. get('[option="2"]').click()
  298. get('ul').should(beVisible())
  299. get('button').should(haveText('2'))
  300. get('[option="4"]').click()
  301. get('button').should(haveText('2,4'))
  302. get('ul').should(beVisible())
  303. get('[option="4"]').click()
  304. get('button').should(haveText('2'))
  305. get('ul').should(beVisible())
  306. },
  307. );
  308. // @todo support "static" prop
  309. test('keyboard controls',
  310. [html`
  311. <div
  312. x-data="{ active: null, people: [
  313. { id: 1, name: 'Wade Cooper' },
  314. { id: 2, name: 'Arlene Mccoy' },
  315. { id: 3, name: 'Devon Webb', disabled: true },
  316. { id: 4, name: 'Tom Cook' },
  317. { id: 5, name: 'Tanya Fox', disabled: true },
  318. { id: 6, name: 'Hellen Schmidt' },
  319. { id: 7, name: 'Caroline Schultz' },
  320. { id: 8, name: 'Mason Heaney' },
  321. { id: 9, name: 'Claudie Smitham' },
  322. { id: 10, name: 'Emil Schaefer' },
  323. ]}"
  324. x-listbox
  325. x-model="active"
  326. >
  327. <label x-listbox:label>Assigned to</label>
  328. <button x-listbox:button x-text="active ? active.name : 'Select Person'"></button>
  329. <ul x-listbox:options options>
  330. <template x-for="person in people" :key="person.id">
  331. <li
  332. :option="person.id"
  333. x-listbox:option
  334. :value="person"
  335. :disabled="person.disabled"
  336. :class="{
  337. 'selected': $listboxOption.isSelected,
  338. 'active': $listboxOption.isActive,
  339. }"
  340. >
  341. <span x-text="person.name"></span>
  342. </li>
  343. </template>
  344. </ul>
  345. </div>
  346. `],
  347. ({ get }) => {
  348. get('.active').should(notExist())
  349. get('button').focus().type(' ')
  350. get('[options]')
  351. .should(beVisible())
  352. .should(haveFocus())
  353. .type('{downarrow}')
  354. get('[option="1"')
  355. .should(haveClasses(['active']))
  356. get('[options]')
  357. .type('{downarrow}')
  358. get('[option="2"]')
  359. .should(haveClasses(['active']))
  360. .type('{downarrow}')
  361. get('[option="4"]')
  362. .should(haveClasses(['active']))
  363. get('[options]')
  364. .type('{uparrow}')
  365. get('[option="2"]')
  366. .should(haveClasses(['active']))
  367. get('[options]')
  368. .type('{home}')
  369. get('[option="1"')
  370. .should(haveClasses(['active']))
  371. get('[options]')
  372. .type('{end}')
  373. get('[option="10"]')
  374. .should(haveClasses(['active']))
  375. get('[options]')
  376. .type('{pageUp}')
  377. get('[option="1"')
  378. .should(haveClasses(['active']))
  379. get('[options]')
  380. .type('{pageDown}')
  381. get('[option="10"]')
  382. .should(haveClasses(['active']))
  383. get('[options]')
  384. .tab()
  385. .should(haveFocus())
  386. .should(beVisible())
  387. .tab({ shift: true })
  388. .should(haveFocus())
  389. .should(beVisible())
  390. .type('{esc}')
  391. .should(notBeVisible())
  392. },
  393. )
  394. // @todo support horizontal prop and add tests for it
  395. test('search',
  396. [html`
  397. <div
  398. x-data="{ active: null, people: [
  399. { id: 1, name: 'Wade Cooper' },
  400. { id: 2, name: 'Arlene Mccoy' },
  401. { id: 3, name: 'Devon Webb', disabled: true },
  402. { id: 4, name: 'Tom Cook' },
  403. { id: 5, name: 'Tanya Fox', disabled: true },
  404. { id: 6, name: 'Hellen Schmidt' },
  405. { id: 7, name: 'Caroline Schultz' },
  406. { id: 8, name: 'Mason Heaney' },
  407. { id: 9, name: 'Claudie Smitham' },
  408. { id: 10, name: 'Emil Schaefer' },
  409. ]}"
  410. x-listbox
  411. x-model="active"
  412. >
  413. <label x-listbox:label>Assigned to</label>
  414. <button x-listbox:button x-text="active ? active.name : 'Select Person'"></button>
  415. <ul x-listbox:options options>
  416. <template x-for="person in people" :key="person.id">
  417. <li
  418. :option="person.id"
  419. x-listbox:option
  420. :value="person"
  421. :disabled="person.disabled"
  422. :class="{
  423. 'selected': $listboxOption.isSelected,
  424. 'active': $listboxOption.isActive,
  425. }"
  426. >
  427. <span x-text="person.name"></span>
  428. </li>
  429. </template>
  430. </ul>
  431. </div>
  432. `],
  433. ({ get, wait }) => {
  434. get('.active').should(notExist())
  435. get('button').click()
  436. get('[options]')
  437. .type('ar')
  438. get('[option="2"')
  439. .should(haveClasses(['active']))
  440. wait(500)
  441. get('[options]')
  442. .type('ma')
  443. get('[option="8"]')
  444. .should(haveClasses(['active']))
  445. },
  446. )
  447. // @todo update these assertions to be accurate
  448. test('has accessibility attributes',
  449. [html`
  450. <div
  451. x-data="{ active: null, people: [
  452. { id: 1, name: 'Wade Cooper' },
  453. { id: 2, name: 'Arlene Mccoy' },
  454. { id: 3, name: 'Devon Webb', disabled: true },
  455. { id: 4, name: 'Tom Cook' },
  456. { id: 5, name: 'Tanya Fox', disabled: true },
  457. { id: 6, name: 'Hellen Schmidt' },
  458. { id: 7, name: 'Caroline Schultz' },
  459. { id: 8, name: 'Mason Heaney' },
  460. { id: 9, name: 'Claudie Smitham' },
  461. { id: 10, name: 'Emil Schaefer' },
  462. ]}"
  463. x-listbox
  464. x-model="active"
  465. >
  466. <label x-listbox:label>Assigned to</label>
  467. <button x-listbox:button x-text="active ? active.name : 'Select Person'"></button>
  468. <ul x-listbox:options options>
  469. <template x-for="person in people" :key="person.id">
  470. <li
  471. :option="person.id"
  472. x-listbox:option
  473. :value="person"
  474. :disabled="person.disabled"
  475. :class="{
  476. 'selected': $listboxOption.isSelected,
  477. 'active': $listboxOption.isActive,
  478. }"
  479. >
  480. <span x-text="person.name"></span>
  481. </li>
  482. </template>
  483. </ul>
  484. </div>
  485. `],
  486. ({ get }) => {
  487. get('button')
  488. .should(haveAttribute('aria-haspopup', 'true'))
  489. .should(haveAttribute('aria-labelledby', 'alpine-listbox-label-1'))
  490. .should(haveAttribute('aria-expanded', 'false'))
  491. .should(notHaveAttribute('aria-controls'))
  492. .should(haveAttribute('id', 'alpine-listbox-button-1'))
  493. .click()
  494. .should(haveAttribute('aria-expanded', 'true'))
  495. .should(haveAttribute('aria-controls', 'alpine-listbox-items-1'))
  496. get('[options]')
  497. .should(haveAttribute('aria-orientation', 'vertical'))
  498. .should(haveAttribute('role', 'menu'))
  499. .should(haveAttribute('id', 'alpine-listbox-items-1'))
  500. .should(haveAttribute('aria-labelledby', 'alpine-listbox-button-1'))
  501. .should(notHaveAttribute('aria-activedescendant'))
  502. .should(haveAttribute('tabindex', '0'))
  503. .type('{downarrow}')
  504. .should(haveAttribute('aria-activedescendant', 'alpine-listbox-item-1'))
  505. get('[option="1"')
  506. .should(haveAttribute('role', 'menuitem'))
  507. .should(haveAttribute('id', 'alpine-listbox-item-1'))
  508. .should(haveAttribute('tabindex', '-1'))
  509. get('[option="2"]')
  510. .should(haveAttribute('role', 'menuitem'))
  511. .should(haveAttribute('id', 'alpine-listbox-item-2'))
  512. .should(haveAttribute('tabindex', '-1'))
  513. get('[options]')
  514. .type('{downarrow}')
  515. .should(haveAttribute('aria-activedescendant', 'alpine-listbox-item-2'))
  516. },
  517. )
  518. // test multiple (selecting multiple)
  519. // test keyboard navigation (arrows and searching)
  520. // test static
  521. // test skipping disabled
  522. // test "by" attribute
  523. // test changing value manually changes selected items