CommonActionMenu.spec.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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/CommonPopover/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. props: {
  35. ...props,
  36. entity: {
  37. id: 'foo-test-action',
  38. },
  39. actions,
  40. },
  41. })
  42. }
  43. describe('Multiple Actions', () => {
  44. it('shows action menu button by default', async () => {
  45. const view = await renderActionMenu(actions)
  46. expect(view.getByIconName('three-dots-vertical')).toBeInTheDocument()
  47. })
  48. it('show not content when no item exists', async () => {
  49. const view = await renderActionMenu([
  50. {
  51. ...actions[0],
  52. show: () => false,
  53. },
  54. {
  55. ...actions[1],
  56. show: () => false,
  57. },
  58. {
  59. key: 'example',
  60. label: 'Example',
  61. show: () => false,
  62. },
  63. ])
  64. expect(
  65. view.queryByIconName('three-dots-vertical'),
  66. ).not.toBeInTheDocument()
  67. })
  68. it('calls onClick handler when action is clicked', async () => {
  69. const view = await renderActionMenu(actions)
  70. await view.events.click(view.getByIconName('three-dots-vertical'))
  71. expect(view.getByIconName('trash3')).toBeInTheDocument()
  72. expect(view.getByIconName('person-gear')).toBeInTheDocument()
  73. await view.events.click(view.getByText('Change Foo'))
  74. expect(fn).toHaveBeenCalledWith('foo-test-action')
  75. })
  76. it('finds corresponding a11y controls', async () => {
  77. const view = await renderActionMenu(actions)
  78. await view.events.click(view.getByIconName('three-dots-vertical'))
  79. const id = view
  80. .getByLabelText('Action menu button')
  81. .getAttribute('aria-controls')
  82. const popover = document.getElementById(id as string)
  83. expect(popover?.getAttribute('id')).toEqual(id)
  84. })
  85. it('sets a custom aria label on single action button', async () => {
  86. const view = await renderActionMenu(actions, {
  87. customMenuButtonLabel: 'Custom Action Menu Label',
  88. })
  89. await view.rerender({
  90. customMenuButtonLabel: 'Custom Action Menu Label',
  91. })
  92. expect(
  93. view.getByLabelText('Custom Action Menu Label'),
  94. ).toBeInTheDocument()
  95. })
  96. })
  97. describe('single action mode', () => {
  98. it('adds aria label on single action button', async () => {
  99. const view = await renderActionMenu([actions[0]])
  100. expect(view.getByLabelText('Delete Foo')).toBeInTheDocument()
  101. })
  102. it('supports single action mode', async () => {
  103. const view = await renderActionMenu([actions[0]])
  104. expect(
  105. view.queryByIconName('three-dots-vertical'),
  106. ).not.toBeInTheDocument()
  107. expect(view.getByIconName('trash3')).toBeInTheDocument()
  108. })
  109. it('calls onClick handler when action is clicked', async () => {
  110. const view = await renderActionMenu([actions[0]])
  111. await view.events.click(view.getByIconName('trash3'))
  112. expect(fn).toHaveBeenCalledWith('foo-test-action')
  113. })
  114. it('renders single action if prop is set', async () => {
  115. const view = await renderActionMenu([actions[0]], {
  116. noSingleActionMode: true,
  117. })
  118. expect(view.queryByIconName('trash3')).not.toBeInTheDocument()
  119. await view.events.click(view.getByIconName('three-dots-vertical'))
  120. expect(view.getByIconName('trash3')).toBeInTheDocument()
  121. })
  122. it('sets a custom aria label on single action', async () => {
  123. const view = await renderActionMenu([
  124. {
  125. key: 'delete-foo',
  126. label: 'Delete Foo',
  127. ariaLabel: 'Custom Delete Foo',
  128. icon: 'trash3',
  129. onClick: (entity?: ObjectLike) => {
  130. fn(entity?.id)
  131. },
  132. },
  133. ])
  134. expect(view.getByLabelText('Custom Delete Foo')).toBeInTheDocument()
  135. await view.rerender({
  136. actions: [
  137. {
  138. key: 'delete-foo',
  139. label: 'Delete Foo',
  140. ariaLabel: (entity: ObjectLike) => `label ${entity.id}`,
  141. icon: 'trash3',
  142. onClick: (entity?: ObjectLike) => {
  143. fn(entity?.id)
  144. },
  145. },
  146. ],
  147. })
  148. expect(view.getByLabelText('label foo-test-action')).toBeInTheDocument()
  149. })
  150. })
  151. })