CommonDialog.spec.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. // Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. import { flushPromises } from '@vue/test-utils'
  3. import { afterAll, beforeAll, expect } from 'vitest'
  4. import { renderComponent } from '#tests/support/components/index.ts'
  5. import { waitForNextTick } from '#tests/support/utils.ts'
  6. import CommonDialog from '../CommonDialog.vue'
  7. import { getDialogMeta, useDialog } from '../useDialog.ts'
  8. const html = String.raw
  9. describe('visuals for common dialog', () => {
  10. let mainElement: HTMLElement
  11. let app: HTMLDivElement
  12. beforeAll(() => {
  13. app = document.createElement('div')
  14. app.id = 'app'
  15. document.body.appendChild(app)
  16. mainElement = document.createElement('main')
  17. mainElement.id = 'main-content'
  18. app.insertAdjacentElement('beforeend', mainElement)
  19. })
  20. beforeEach(() => {
  21. const { dialogsOptions } = getDialogMeta()
  22. dialogsOptions.set('dialog', {
  23. name: 'dialog',
  24. component: vi.fn().mockResolvedValue({}),
  25. refocus: true,
  26. })
  27. })
  28. afterAll(() => {
  29. document.body.innerHTML = ''
  30. })
  31. it('rendering with header title and content', () => {
  32. const wrapper = renderComponent(CommonDialog, {
  33. props: {
  34. name: 'dialog',
  35. headerTitle: 'Some Title',
  36. },
  37. slots: {
  38. default: 'Content Slot',
  39. },
  40. router: true,
  41. })
  42. expect(wrapper.getByText('Some Title')).toBeInTheDocument()
  43. expect(wrapper.getByText('Content Slot')).toBeInTheDocument()
  44. expect(wrapper.getByLabelText('Close dialog')).toBeInTheDocument()
  45. })
  46. it('can render header title as slot', () => {
  47. const wrapper = renderComponent(CommonDialog, {
  48. props: {
  49. name: 'dialog',
  50. },
  51. slots: {
  52. header: 'Some Title',
  53. },
  54. router: true,
  55. })
  56. expect(wrapper.getByText('Some Title')).toBeInTheDocument()
  57. })
  58. it('can close dialog with keyboard and clicks', async () => {
  59. const wrapper = renderComponent(CommonDialog, {
  60. props: {
  61. name: 'dialog',
  62. },
  63. global: {
  64. stubs: {
  65. teleport: true,
  66. },
  67. },
  68. router: true,
  69. })
  70. await flushPromises()
  71. await wrapper.events.keyboard('{Escape}')
  72. const emitted = wrapper.emitted()
  73. expect(emitted.close).toHaveLength(1)
  74. expect(emitted.close[0]).toEqual([undefined])
  75. await wrapper.events.click(wrapper.getByLabelText('Close dialog'))
  76. expect(emitted.close).toHaveLength(2)
  77. await wrapper.events.click(wrapper.getByRole('button', { name: 'OK' }))
  78. expect(emitted.close).toHaveLength(3)
  79. expect(emitted.close[2]).toEqual([false])
  80. })
  81. it('rendering different footer button content', () => {
  82. const wrapper = renderComponent(CommonDialog, {
  83. props: {
  84. name: 'dialog',
  85. headerTitle: 'Some Title',
  86. footerActionOptions: {
  87. actionLabel: 'Yes, continue',
  88. },
  89. },
  90. slots: {
  91. default: 'Content Slot',
  92. },
  93. router: true,
  94. })
  95. expect(
  96. wrapper.getByRole('button', { name: 'Yes, continue' }),
  97. ).toBeInTheDocument()
  98. })
  99. it('has an accessible name', async () => {
  100. const wrapper = renderComponent(CommonDialog, {
  101. props: {
  102. headerTitle: 'foobar',
  103. name: 'dialog',
  104. },
  105. router: true,
  106. })
  107. expect(wrapper.getByRole('dialog')).toHaveAccessibleName('foobar')
  108. })
  109. it('traps focus inside the dialog', async () => {
  110. const externalForm = document.createElement('form')
  111. externalForm.innerHTML = html`
  112. <input data-test-id="form_input" type="text" />
  113. <select data-test-id="form_select" type="text" />
  114. `
  115. document.body.appendChild(externalForm)
  116. const wrapper = renderComponent(CommonDialog, {
  117. props: {
  118. name: 'dialog',
  119. },
  120. slots: {
  121. default: html`
  122. <input data-test-id="input" type="text" />
  123. <div data-test-id="div" tabindex="0" />
  124. <select data-test-id="select">
  125. <option value="1">1</option>
  126. </select>
  127. `,
  128. },
  129. router: true,
  130. })
  131. wrapper.getByTestId('input').focus()
  132. expect(wrapper.getByTestId('input')).toHaveFocus()
  133. await wrapper.events.keyboard('{Tab}')
  134. expect(wrapper.getByTestId('div')).toHaveFocus()
  135. await wrapper.events.keyboard('{Tab}')
  136. expect(wrapper.getByTestId('select')).toHaveFocus()
  137. await wrapper.events.keyboard('{Tab}')
  138. expect(
  139. wrapper.getByRole('button', { name: 'Cancel & Go Back' }),
  140. ).toHaveFocus()
  141. await wrapper.events.keyboard('{Tab}')
  142. expect(wrapper.getByRole('button', { name: 'OK' })).toHaveFocus()
  143. await wrapper.events.keyboard('{Tab}')
  144. expect(wrapper.getByLabelText('Close dialog')).toHaveFocus()
  145. await wrapper.events.keyboard('{Tab}')
  146. expect(wrapper.getByTestId('input')).toHaveFocus()
  147. })
  148. it('autofocuses the first focusable element', async () => {
  149. const wrapper = renderComponent(CommonDialog, {
  150. props: {
  151. name: 'dialog',
  152. },
  153. slots: {
  154. default: html`
  155. <div data-test-id="div" tabindex="0" />
  156. <select data-test-id="select">
  157. <option value="1">1</option>
  158. </select>
  159. `,
  160. },
  161. router: true,
  162. })
  163. await flushPromises()
  164. expect(wrapper.getByTestId('div')).toHaveFocus()
  165. })
  166. it('focuses close, if there is nothing focusable in dialog', async () => {
  167. const wrapper = renderComponent(CommonDialog, {
  168. props: {
  169. name: 'dialog',
  170. hideFooter: true,
  171. },
  172. })
  173. await flushPromises()
  174. expect(wrapper.getByLabelText('Close dialog')).toHaveFocus()
  175. })
  176. it('refocuses element that opened dialog', async () => {
  177. const wrapper = renderComponent(
  178. {
  179. template: `
  180. <button aria-haspopup="dialog" aria-controls="dialog" data-test-id="button"/>`,
  181. setup() {
  182. const dialog = useDialog({
  183. name: 'dialog',
  184. component: () =>
  185. import('#desktop/components/CommonDialog/CommonDialog.vue'),
  186. })
  187. dialog.open({ name: 'dialog', hideFooter: true })
  188. },
  189. },
  190. {
  191. dialog: true,
  192. router: true,
  193. },
  194. )
  195. await wrapper.events.click(wrapper.getByTestId('button'))
  196. expect(wrapper.getByTestId('button')).toHaveFocus()
  197. await wrapper.events.keyboard('{Escape}')
  198. await waitForNextTick()
  199. expect(wrapper.getByTestId('button')).toHaveFocus()
  200. })
  201. it('displays by default over the main content', () => {
  202. const wrapper = renderComponent(CommonDialog, {
  203. props: {
  204. name: 'dialog',
  205. fullscreen: false,
  206. },
  207. })
  208. expect(mainElement.children).not.include(wrapper.baseElement)
  209. expect(app.children).include(wrapper.baseElement)
  210. })
  211. it('supports displaying over entire viewport', () => {
  212. const wrapper = renderComponent(CommonDialog, {
  213. props: {
  214. name: 'dialog',
  215. fullscreen: true,
  216. },
  217. })
  218. expect(mainElement.children).include(wrapper.baseElement)
  219. })
  220. })