CommonSelect.spec.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { ref } from 'vue'
  3. import { renderComponent } from '#tests/support/components/index.ts'
  4. import { i18n } from '#shared/i18n.ts'
  5. import CommonSelect, { type Props } from '../CommonSelect.vue'
  6. import type { Ref } from 'vue'
  7. const options = [
  8. {
  9. value: 0,
  10. label: 'Item A',
  11. },
  12. {
  13. value: 1,
  14. label: 'Item B',
  15. },
  16. {
  17. value: 2,
  18. label: 'Item C',
  19. },
  20. ]
  21. const html = String.raw
  22. const renderSelect = (props: Props, modelValue?: Ref) => {
  23. return renderComponent(CommonSelect, {
  24. props: {
  25. isTargetVisible: true,
  26. ...props,
  27. },
  28. slots: {
  29. default: html` <template #default="{ open, focus }">
  30. <button @click="open()">Open Select</button>
  31. <button @click="focus()">Move Focus</button>
  32. </template>`,
  33. },
  34. vModel: {
  35. modelValue,
  36. },
  37. })
  38. }
  39. beforeEach(() => {
  40. i18n.setTranslationMap(new Map([]))
  41. })
  42. describe('CommonSelect.vue', () => {
  43. it('can select and unselect value', async () => {
  44. const modelValue = ref()
  45. const view = renderSelect({ options }, modelValue)
  46. await view.events.click(view.getByText('Open Select'))
  47. await view.events.click(view.getByText('Item A'))
  48. expect(view.emitted().select).toEqual([[options[0]]])
  49. expect(
  50. view.queryByRole('menu'),
  51. 'dropdown is hidden',
  52. ).not.toBeInTheDocument()
  53. expect(modelValue.value).toBe(0)
  54. await view.events.click(view.getByText('Open Select'))
  55. expect(
  56. view.getByIconName((name, node) => {
  57. return (
  58. name === '#icon-check2' &&
  59. !node?.parentElement?.classList.contains('invisible')
  60. )
  61. }),
  62. ).toBeInTheDocument()
  63. await view.events.click(view.getByText('Item A'))
  64. expect(view.emitted().select).toEqual([[options[0]], [options[0]]])
  65. expect(modelValue.value).toBe(undefined)
  66. })
  67. it('does not close select with noClose prop', async () => {
  68. const view = renderSelect({ options, noClose: true })
  69. await view.events.click(view.getByText('Open Select'))
  70. await view.events.click(view.getByRole('option', { name: 'Item A' }))
  71. expect(view.getByRole('menu')).toBeInTheDocument()
  72. })
  73. it('can select and unselect multiple values', async () => {
  74. const modelValue = ref()
  75. const view = renderSelect({ options, multiple: true }, modelValue)
  76. await view.events.click(view.getByText('Open Select'))
  77. await view.events.click(view.getByText('Item A'))
  78. expect(modelValue.value).toEqual([0])
  79. expect(view.queryAllByIconName('check-square')).toHaveLength(1)
  80. await view.events.click(view.getByText('Item A'))
  81. expect(modelValue.value).toEqual([])
  82. await view.events.click(view.getByText('Item A'))
  83. await view.events.click(view.getByText('Item B'))
  84. expect(modelValue.value).toEqual([0, 1])
  85. expect(view.queryAllByIconName('check-square')).toHaveLength(2)
  86. })
  87. it('can use select all action with active multiple', async () => {
  88. const modelValue = ref()
  89. const view = renderSelect({ options, multiple: true }, modelValue)
  90. await view.events.click(view.getByText('Open Select'))
  91. await view.events.click(view.getByText('select all options'))
  92. expect(modelValue.value).toEqual([0, 1, 2])
  93. expect(view.queryAllByIconName('check-square')).toHaveLength(3)
  94. })
  95. it('can add additional actions', async () => {
  96. const modelValue = ref()
  97. const actionCallbackSpy = vi.fn()
  98. const view = renderSelect(
  99. {
  100. options,
  101. multiple: true,
  102. actions: [
  103. {
  104. label: 'example action',
  105. key: 'example',
  106. onClick: actionCallbackSpy,
  107. },
  108. ],
  109. },
  110. modelValue,
  111. )
  112. await view.events.click(view.getByText('Open Select'))
  113. await view.events.click(view.getByText('example action'))
  114. expect(actionCallbackSpy).toHaveBeenCalledTimes(1)
  115. })
  116. it('passive mode does not change local value, but emits select', async () => {
  117. const modelValue = ref()
  118. const view = renderSelect({ options, passive: true }, modelValue)
  119. await view.events.click(view.getByText('Open Select'))
  120. await view.events.click(view.getByText('Item A'))
  121. expect(view.emitted().select).toBeDefined()
  122. expect(modelValue.value).toBeUndefined()
  123. })
  124. it('cannot select disabled values', async () => {
  125. const modelValue = ref()
  126. const view = renderSelect(
  127. { options: [{ ...options[0], disabled: true }] },
  128. modelValue,
  129. )
  130. await view.events.click(view.getByText('Open Select'))
  131. expect(view.getByRole('option')).toHaveAttribute('aria-disabled', 'true')
  132. await view.events.click(view.getByText('Item A'))
  133. expect(view.emitted().select).toBeUndefined()
  134. expect(modelValue.value).toBeUndefined()
  135. })
  136. it('translates labels', async () => {
  137. i18n.setTranslationMap(new Map([[options[0].label, 'Translated Item A']]))
  138. const view = renderSelect({ options })
  139. await view.events.click(view.getByText('Open Select'))
  140. expect(view.getByText('Translated Item A')).toBeInTheDocument()
  141. })
  142. it('does not translate with noOptionsLabelTranslation prop', async () => {
  143. i18n.setTranslationMap(new Map([[options[0].label, 'Translated Item A']]))
  144. const view = renderSelect({ options, noOptionsLabelTranslation: true })
  145. await view.events.click(view.getByText('Open Select'))
  146. expect(view.getByText(/^Item A$/)).toBeInTheDocument()
  147. })
  148. it('forces translation if placeholder is provided', async () => {
  149. const options = [
  150. {
  151. value: 0,
  152. label: 'Label (%s)',
  153. labelPlaceholder: ['A'],
  154. heading: 'Heading (%s)',
  155. headingPlaceholder: ['B'],
  156. },
  157. ]
  158. const view = renderSelect({ options, noOptionsLabelTranslation: true })
  159. await view.events.click(view.getByText('Open Select'))
  160. expect(
  161. view.getByRole('option', { name: 'Label (A) – Heading (B)' }),
  162. ).toBeInTheDocument()
  163. })
  164. it('can use boolean as value', async () => {
  165. const modelValue = ref()
  166. const view = renderSelect(
  167. {
  168. options: [
  169. { value: true, label: 'Yes' },
  170. { value: false, label: 'No' },
  171. ],
  172. },
  173. modelValue,
  174. )
  175. await view.events.click(view.getByText('Open Select'))
  176. await view.events.click(view.getByText('Yes'))
  177. expect(modelValue.value).toBe(true)
  178. })
  179. it('has an accessible name', async () => {
  180. const view = renderSelect({ options })
  181. await view.events.click(view.getByText('Open Select'))
  182. expect(view.getByRole('listbox')).toHaveAccessibleName('Select…')
  183. })
  184. it('supports optional headings', async () => {
  185. const view = renderSelect({
  186. options: [
  187. {
  188. value: 0,
  189. label: 'foo (%s)',
  190. labelPlaceholder: ['1'],
  191. heading: 'bar (%s)',
  192. headingPlaceholder: ['2'],
  193. },
  194. ],
  195. })
  196. await view.events.click(view.getByText('Open Select'))
  197. const option = view.getByRole('option')
  198. expect(option).toHaveTextContent('foo (1) – bar (2)')
  199. expect(option.children[1]).toHaveAttribute(
  200. 'aria-label',
  201. 'foo (1) – bar (2)',
  202. )
  203. })
  204. it('supports navigating options with children', async () => {
  205. const testChildOption = {
  206. value: 1,
  207. label: 'child',
  208. }
  209. const testParentOption = {
  210. value: 1,
  211. label: 'parent',
  212. disabled: true,
  213. children: [testChildOption],
  214. }
  215. const view = renderSelect({
  216. options: [testParentOption],
  217. })
  218. await view.events.click(view.getByText('Open Select'))
  219. expect(
  220. view.queryByRole('button', { name: 'Back to previous page' }),
  221. ).not.toBeInTheDocument()
  222. expect(view.getByRole('option')).toHaveTextContent('parent')
  223. expect(view.getByRole('option')).toHaveAttribute('aria-disabled', 'true')
  224. await view.events.click(view.getByRole('button', { name: 'Has submenu' }))
  225. expect(view.emitted().push).toEqual([[testParentOption]])
  226. await view.rerender({
  227. options: [testChildOption],
  228. isChildPage: true,
  229. })
  230. expect(view.getByRole('option')).toHaveTextContent('child')
  231. await view.events.click(
  232. view.getByRole('button', { name: 'Back to previous page' }),
  233. )
  234. expect(view.emitted().pop).toEqual([[]])
  235. await view.rerender({
  236. options: [testParentOption],
  237. isChildPage: false,
  238. })
  239. expect(
  240. view.queryByRole('button', { name: 'Back to previous page' }),
  241. ).not.toBeInTheDocument()
  242. expect(view.getByRole('option')).toHaveTextContent('parent')
  243. expect(view.getByRole('option')).toHaveAttribute('aria-disabled', 'true')
  244. await view.events.click(view.getByRole('button', { name: 'Has submenu' }))
  245. await view.rerender({
  246. options: [testChildOption],
  247. isChildPage: true,
  248. })
  249. await view.events.click(view.getByText('child'))
  250. expect(view.emitted().select).toEqual([[testChildOption]])
  251. })
  252. })