CommonActionMenu.spec.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import renderComponent from '#tests/support/components/renderComponent.ts'
  3. import type { ObjectLike } from '#shared/types/utils.ts'
  4. import CommonActionMenu from '#desktop/components/CommonActionMenu/CommonActionMenu.vue'
  5. import type { MenuItem } from '#desktop/components/CommonPopoverMenu/types.ts'
  6. import type { Props } from '../CommonActionMenu.vue'
  7. const fn = vi.fn()
  8. describe('CommonActionMenu', () => {
  9. const actions: MenuItem[] = [
  10. {
  11. key: 'delete-foo',
  12. label: 'Delete Foo',
  13. icon: 'trash3',
  14. show: () => true,
  15. onClick: (entity?: ObjectLike) => {
  16. fn(entity?.id)
  17. },
  18. },
  19. {
  20. key: 'change-foo',
  21. label: 'Change Foo',
  22. icon: 'person-gear',
  23. show: () => true,
  24. onClick: (entity?: ObjectLike) => {
  25. fn(entity?.id)
  26. },
  27. },
  28. ]
  29. const renderActionMenu = async (
  30. actions: MenuItem[],
  31. props?: Partial<Props>,
  32. ) => {
  33. return renderComponent(CommonActionMenu, {
  34. router: true,
  35. props: {
  36. ...props,
  37. entity: {
  38. id: 'foo-test-action',
  39. },
  40. actions,
  41. },
  42. })
  43. }
  44. describe('Multiple Actions', () => {
  45. it('shows action menu button by default', async () => {
  46. const view = await renderActionMenu(actions)
  47. expect(view.getByIconName('three-dots-vertical')).toBeInTheDocument()
  48. })
  49. it('show not content when no item exists', async () => {
  50. const view = await renderActionMenu([
  51. {
  52. ...actions[0],
  53. show: () => false,
  54. },
  55. {
  56. ...actions[1],
  57. show: () => false,
  58. },
  59. {
  60. key: 'example',
  61. label: 'Example',
  62. show: () => false,
  63. },
  64. ])
  65. expect(
  66. view.queryByIconName('three-dots-vertical'),
  67. ).not.toBeInTheDocument()
  68. })
  69. it('calls onClick handler when action is clicked', async () => {
  70. const view = await renderActionMenu(actions)
  71. await view.events.click(view.getByIconName('three-dots-vertical'))
  72. expect(view.getByIconName('trash3')).toBeInTheDocument()
  73. expect(view.getByIconName('person-gear')).toBeInTheDocument()
  74. await view.events.click(view.getByText('Change Foo'))
  75. expect(fn).toHaveBeenCalledWith('foo-test-action')
  76. })
  77. it('finds corresponding a11y controls', async () => {
  78. const view = await renderActionMenu(actions)
  79. await view.events.click(view.getByIconName('three-dots-vertical'))
  80. const id = view
  81. .getByLabelText('Action menu button')
  82. .getAttribute('aria-controls')
  83. const popover = document.getElementById(id as string)
  84. expect(popover?.getAttribute('id')).toEqual(id)
  85. })
  86. it('sets a custom aria label on single action button', async () => {
  87. const view = await renderActionMenu(actions, {
  88. customMenuButtonLabel: 'Custom Action Menu Label',
  89. })
  90. await view.rerender({
  91. customMenuButtonLabel: 'Custom Action Menu Label',
  92. })
  93. expect(
  94. view.getByLabelText('Custom Action Menu Label'),
  95. ).toBeInTheDocument()
  96. })
  97. })
  98. describe('single action mode', () => {
  99. it('adds aria label on single action button', async () => {
  100. const view = await renderActionMenu([actions[0]])
  101. expect(view.getByLabelText('Delete Foo')).toBeInTheDocument()
  102. })
  103. it('supports single action mode', async () => {
  104. const view = await renderActionMenu([actions[0]])
  105. expect(
  106. view.queryByIconName('three-dots-vertical'),
  107. ).not.toBeInTheDocument()
  108. expect(view.getByIconName('trash3')).toBeInTheDocument()
  109. })
  110. it('calls onClick handler when action is clicked', async () => {
  111. const view = await renderActionMenu([actions[0]])
  112. await view.events.click(view.getByIconName('trash3'))
  113. expect(fn).toHaveBeenCalledWith('foo-test-action')
  114. })
  115. it('renders single action if prop is set', async () => {
  116. const view = await renderActionMenu([actions[0]], {
  117. noSingleActionMode: true,
  118. })
  119. expect(view.queryByIconName('trash3')).not.toBeInTheDocument()
  120. await view.events.click(view.getByIconName('three-dots-vertical'))
  121. expect(view.getByIconName('trash3')).toBeInTheDocument()
  122. })
  123. it('sets a custom aria label on single action', async () => {
  124. const view = await renderActionMenu([
  125. {
  126. key: 'delete-foo',
  127. label: 'Delete Foo',
  128. ariaLabel: 'Custom Delete Foo',
  129. icon: 'trash3',
  130. onClick: (entity?: ObjectLike) => {
  131. fn(entity?.id)
  132. },
  133. },
  134. ])
  135. expect(view.getByLabelText('Custom Delete Foo')).toBeInTheDocument()
  136. await view.rerender({
  137. actions: [
  138. {
  139. key: 'delete-foo',
  140. label: 'Delete Foo',
  141. ariaLabel: (entity: ObjectLike) => `label ${entity.id}`,
  142. icon: 'trash3',
  143. onClick: (entity?: ObjectLike) => {
  144. fn(entity?.id)
  145. },
  146. },
  147. ],
  148. })
  149. expect(view.getByLabelText('label foo-test-action')).toBeInTheDocument()
  150. })
  151. it('supports disabling action menu', async () => {
  152. const actions = [
  153. {
  154. key: 'delete-foo',
  155. label: 'Delete Foo',
  156. icon: 'trash3',
  157. show: () => true,
  158. onClick: (entity?: ObjectLike) => {
  159. fn(entity?.id)
  160. },
  161. },
  162. {
  163. key: 'change-foo',
  164. label: 'Change Foo',
  165. icon: 'person-gear',
  166. show: () => true,
  167. onClick: (entity?: ObjectLike) => {
  168. fn(entity?.id)
  169. },
  170. },
  171. ]
  172. // Testing if single action is presented
  173. const wrapper = await renderActionMenu(actions, {
  174. disabled: true,
  175. })
  176. expect(
  177. wrapper.getByRole('button', { name: 'Action menu button' }),
  178. ).toBeDisabled()
  179. await wrapper.rerender({
  180. actions: [actions[0]],
  181. disabled: true,
  182. })
  183. expect(wrapper.getByRole('button', { name: 'Delete Foo' })).toBeDisabled()
  184. })
  185. })
  186. })