1
0

menu.spec.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. import { haveClasses, beVisible, haveAttribute, haveText, html, notBeVisible, notExist, test, haveFocus, notHaveClasses, notHaveAttribute } from '../../../utils'
  2. test('it works',
  3. [html`
  4. <div x-data x-menu>
  5. <span>
  6. <button x-menu:button trigger>
  7. <span>Options</span>
  8. </button>
  9. </span>
  10. <div x-menu:items items>
  11. <div>
  12. <p>Signed in as</p>
  13. <p>tom@example.com</p>
  14. </div>
  15. <div>
  16. <a x-menu:item href="#account-settings">
  17. Account settings
  18. </a>
  19. <a x-menu:item href="#support">
  20. Support
  21. </a>
  22. <a x-menu:item disabled href="#new-feature">
  23. New feature (soon)
  24. </a>
  25. <a x-menu:item href="#license">
  26. License
  27. </a>
  28. </div>
  29. <div>
  30. <a x-menu:item href="#sign-out">
  31. Sign out
  32. </a>
  33. </div>
  34. </div>
  35. </div>`],
  36. ({ get }) => {
  37. get('[items]').should(notBeVisible())
  38. get('[trigger]').click()
  39. get('[items]').should(beVisible())
  40. },
  41. )
  42. test('focusing away closes menu',
  43. [html`
  44. <div>
  45. <div x-data x-menu>
  46. <span>
  47. <button x-menu:button trigger>
  48. <span>Options</span>
  49. </button>
  50. </span>
  51. <div x-menu:items items>
  52. <div>
  53. <p>Signed in as</p>
  54. <p>tom@example.com</p>
  55. </div>
  56. <div>
  57. <a x-menu:item href="#account-settings">
  58. Account settings
  59. </a>
  60. <a x-menu:item href="#support">
  61. Support
  62. </a>
  63. <a x-menu:item href="#license">
  64. License
  65. </a>
  66. </div>
  67. <div>
  68. <a x-menu:item href="#sign-out">
  69. Sign out
  70. </a>
  71. </div>
  72. </div>
  73. </div>
  74. <button>Focus away</button>
  75. </div>
  76. `],
  77. ({ get }) => {
  78. get('[items]').should(notBeVisible())
  79. get('[trigger]').click()
  80. get('[items]').should(beVisible())
  81. cy.focused().tab()
  82. get('[items]').should(notBeVisible())
  83. },
  84. )
  85. test('it works with x-model',
  86. [html`
  87. <div x-data="{ open: false }" x-menu x-model="open">
  88. <button trigger @click="open = !open">
  89. <span>Options</span>
  90. </button>
  91. <button x-menu:button>
  92. <span>Options</span>
  93. </button>
  94. <div x-menu:items items>
  95. <div>
  96. <p>Signed in as</p>
  97. <p>tom@example.com</p>
  98. </div>
  99. <div>
  100. <a x-menu:item href="#account-settings">
  101. Account settings
  102. </a>
  103. <a x-menu:item href="#support">
  104. Support
  105. </a>
  106. <a x-menu:item disabled href="#new-feature">
  107. New feature (soon)
  108. </a>
  109. <a x-menu:item href="#license">
  110. License
  111. </a>
  112. </div>
  113. <div>
  114. <a x-menu:item href="#sign-out">
  115. Sign out
  116. </a>
  117. </div>
  118. </div>
  119. </div>`],
  120. ({ get }) => {
  121. get('[items]').should(notBeVisible())
  122. get('[trigger]').click()
  123. get('[items]').should(beVisible())
  124. get('[trigger]').click()
  125. get('[items]').should(notBeVisible())
  126. },
  127. )
  128. test('keyboard controls',
  129. [html`
  130. <div x-data x-menu>
  131. <span>
  132. <button x-menu:button trigger>
  133. <span>Options</span>
  134. </button>
  135. </span>
  136. <div x-menu:items items>
  137. <div>
  138. <p>Signed in as</p>
  139. <p>tom@example.com</p>
  140. </div>
  141. <div>
  142. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  143. Account settings
  144. </a>
  145. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  146. Support
  147. </a>
  148. <a x-menu:item disabled href="#new-feature" :class="$menuItem.isActive && 'active'">
  149. New feature (soon)
  150. </a>
  151. <a x-menu:item href="#license" :class="$menuItem.isActive && 'active'">
  152. License
  153. </a>
  154. </div>
  155. <div>
  156. <a x-menu:item href="#sign-out" :class="$menuItem.isActive && 'active'">
  157. Sign out
  158. </a>
  159. </div>
  160. </div>
  161. </div>`],
  162. ({ get }) => {
  163. get('.active').should(notExist())
  164. get('[trigger]').type(' ')
  165. get('[items]')
  166. .should(beVisible())
  167. .should(haveFocus())
  168. .type('{downarrow}')
  169. get('[href="#account-settings"]')
  170. .should(haveClasses(['active']))
  171. get('[items]')
  172. .type('{downarrow}')
  173. get('[href="#support"]')
  174. .should(haveClasses(['active']))
  175. .type('{downarrow}')
  176. get('[href="#license"]')
  177. .should(haveClasses(['active']))
  178. get('[items]')
  179. .type('{uparrow}')
  180. get('[href="#support"]')
  181. .should(haveClasses(['active']))
  182. get('[items]')
  183. .type('{home}')
  184. get('[href="#account-settings"]')
  185. .should(haveClasses(['active']))
  186. get('[items]')
  187. .type('{end}')
  188. get('[href="#sign-out"]')
  189. .should(haveClasses(['active']))
  190. get('[items]')
  191. .type('{pageUp}')
  192. get('[href="#account-settings"]')
  193. .should(haveClasses(['active']))
  194. get('[items]')
  195. .type('{pageDown}')
  196. get('[href="#sign-out"]')
  197. .should(haveClasses(['active']))
  198. get('[items]')
  199. .tab()
  200. .should(haveFocus())
  201. .should(beVisible())
  202. .tab({ shift: true})
  203. .should(haveFocus())
  204. .should(beVisible())
  205. .type('{esc}')
  206. .should(notBeVisible())
  207. },
  208. )
  209. test('search',
  210. [html`
  211. <div x-data x-menu>
  212. <span>
  213. <button x-menu:button trigger>
  214. <span>Options</span>
  215. </button>
  216. </span>
  217. <div x-menu:items items>
  218. <div>
  219. <p>Signed in as</p>
  220. <p>tom@example.com</p>
  221. </div>
  222. <div>
  223. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  224. Account settings
  225. </a>
  226. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  227. Support
  228. </a>
  229. <a x-menu:item disabled href="#new-feature" :class="$menuItem.isActive && 'active'">
  230. New feature (soon)
  231. </a>
  232. <a x-menu:item href="#license" :class="$menuItem.isActive && 'active'">
  233. License
  234. </a>
  235. </div>
  236. <div>
  237. <a x-menu:item href="#sign-out" :class="$menuItem.isActive && 'active'">
  238. Sign out
  239. </a>
  240. </div>
  241. </div>
  242. </div>`],
  243. ({ get, wait }) => {
  244. get('.active').should(notExist())
  245. get('[trigger]').click()
  246. get('[items]')
  247. .type('ac')
  248. get('[href="#account-settings"]')
  249. .should(haveClasses(['active']))
  250. wait(500)
  251. get('[items]')
  252. .type('si')
  253. get('[href="#sign-out"]')
  254. .should(haveClasses(['active']))
  255. },
  256. )
  257. test('has accessibility attributes',
  258. [html`
  259. <div x-data x-menu>
  260. <label x-menu:label>Options label</label>
  261. <span>
  262. <button x-menu:button trigger>
  263. <span>Options</span>
  264. </button>
  265. </span>
  266. <div x-menu:items items>
  267. <div>
  268. <p>Signed in as</p>
  269. <p>tom@example.com</p>
  270. </div>
  271. <div>
  272. <a x-menu:item href="#account-settings" :class="$menuItem.isActive && 'active'">
  273. Account settings
  274. </a>
  275. <a x-menu:item href="#support" :class="$menuItem.isActive && 'active'">
  276. Support
  277. </a>
  278. <a x-menu:item disabled href="#new-feature" :class="$menuItem.isActive && 'active'">
  279. New feature (soon)
  280. </a>
  281. <a x-menu:item href="#license" :class="$menuItem.isActive && 'active'">
  282. License
  283. </a>
  284. </div>
  285. <div>
  286. <a x-menu:item href="#sign-out" :class="$menuItem.isActive && 'active'">
  287. Sign out
  288. </a>
  289. </div>
  290. </div>
  291. </div>`],
  292. ({ get }) => {
  293. get('[trigger]')
  294. .should(haveAttribute('aria-haspopup', 'true'))
  295. .should(haveAttribute('aria-labelledby', 'alpine-menu-label-1'))
  296. .should(haveAttribute('aria-expanded', 'false'))
  297. .should(notHaveAttribute('aria-controls'))
  298. .should(haveAttribute('id', 'alpine-menu-button-1'))
  299. .click()
  300. .should(haveAttribute('aria-expanded', 'true'))
  301. .should(haveAttribute('aria-controls', 'alpine-menu-items-1'))
  302. get('[items]')
  303. .should(haveAttribute('aria-orientation', 'vertical'))
  304. .should(haveAttribute('role', 'menu'))
  305. .should(haveAttribute('id', 'alpine-menu-items-1'))
  306. .should(haveAttribute('aria-labelledby', 'alpine-menu-button-1'))
  307. .should(notHaveAttribute('aria-activedescendant'))
  308. .should(haveAttribute('tabindex', '0'))
  309. .type('{downarrow}')
  310. .should(haveAttribute('aria-activedescendant', 'alpine-menu-item-1'))
  311. get('[href="#account-settings"]')
  312. .should(haveAttribute('role', 'menuitem'))
  313. .should(haveAttribute('id', 'alpine-menu-item-1'))
  314. .should(haveAttribute('tabindex', '-1'))
  315. get('[href="#support"]')
  316. .should(haveAttribute('role', 'menuitem'))
  317. .should(haveAttribute('id', 'alpine-menu-item-2'))
  318. .should(haveAttribute('tabindex', '-1'))
  319. get('[items]')
  320. .type('{downarrow}')
  321. .should(haveAttribute('aria-activedescendant', 'alpine-menu-item-2'))
  322. },
  323. )
  324. test('$menuItem.isDisabled',
  325. [html`
  326. <div x-data x-menu>
  327. <label x-menu:label>Options label</label>
  328. <span>
  329. <button x-menu:button trigger>
  330. <span>Options</span>
  331. </button>
  332. </span>
  333. <div x-menu:items items>
  334. <div>
  335. <p>Signed in as</p>
  336. <p>tom@example.com</p>
  337. </div>
  338. <div>
  339. <a x-menu:item href="#account-settings" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  340. Account settings
  341. </a>
  342. <a x-menu:item href="#support" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  343. Support
  344. </a>
  345. <a x-menu:item disabled href="#new-feature" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  346. New feature (soon)
  347. </a>
  348. <a x-menu:item href="#license" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  349. License
  350. </a>
  351. </div>
  352. <div>
  353. <a x-menu:item href="#sign-out" :class="{ 'active': $menuItem.isActive, 'disabled': $menuItem.isDisabled }">
  354. Sign out
  355. </a>
  356. </div>
  357. </div>
  358. </div>`],
  359. ({ get }) => {
  360. get('[trigger]').click()
  361. get('[href="#account-settings"]').should(notHaveClasses(['disabled']))
  362. get('[href="#support"]').should(notHaveClasses(['disabled']))
  363. get('[href="#new-feature"]').should(haveClasses(['disabled']))
  364. },
  365. )