vitest.setup.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. // Copyright (C) 2012-2025 Zammad Foundation, https://zammad-foundation.org/
  2. import { loadErrorMessages, loadDevMessages } from '@apollo/client/dev'
  3. import '@testing-library/jest-dom/vitest'
  4. import { toBeDisabled } from '@testing-library/jest-dom/matchers'
  5. import { configure } from '@testing-library/vue'
  6. import { expect, vi } from 'vitest'
  7. import * as matchers from 'vitest-axe/matchers'
  8. import 'vitest-axe/extend-expect'
  9. import { ServiceWorkerHelper } from '#shared/utils/testSw.ts'
  10. import * as assertions from './support/assertions/index.ts'
  11. // Zammad custom assertions: toBeAvatarElement, toHaveClasses, toHaveImagePreview, toHaveCurrentUrl
  12. loadDevMessages()
  13. loadErrorMessages()
  14. vi.hoisted(() => {
  15. globalThis.__ = (source) => {
  16. return source
  17. }
  18. })
  19. window.sw = new ServiceWorkerHelper()
  20. configure({
  21. testIdAttribute: 'data-test-id',
  22. asyncUtilTimeout: process.env.CI ? 30_000 : 1_000,
  23. })
  24. Object.defineProperty(window, 'fetch', {
  25. value: (path: string) => {
  26. throw new Error(`calling fetch on ${path}`)
  27. },
  28. writable: true,
  29. configurable: true,
  30. })
  31. Object.defineProperty(globalThis, 'Notification', {
  32. value: {
  33. permission: undefined,
  34. requestPermission: async () => Promise.resolve('granted'),
  35. name: 'Notification',
  36. },
  37. writable: true,
  38. })
  39. class DOMRectList {
  40. length = 0
  41. // eslint-disable-next-line class-methods-use-this
  42. item = () => null;
  43. // eslint-disable-next-line class-methods-use-this
  44. [Symbol.iterator] = () => {
  45. //
  46. }
  47. }
  48. Object.defineProperty(Node.prototype, 'getClientRects', {
  49. value: new DOMRectList(),
  50. })
  51. Object.defineProperty(Element.prototype, 'scroll', { value: vi.fn() })
  52. Object.defineProperty(Element.prototype, 'scrollBy', { value: vi.fn() })
  53. Object.defineProperty(Element.prototype, 'scrollIntoView', { value: vi.fn() })
  54. const descriptor = Object.getOwnPropertyDescriptor(
  55. HTMLImageElement.prototype,
  56. 'src',
  57. )!
  58. Object.defineProperty(HTMLImageElement.prototype, 'src', {
  59. set(value) {
  60. descriptor.set?.call(this, value)
  61. this.dispatchEvent(new Event('load'))
  62. },
  63. get: descriptor.get,
  64. })
  65. Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
  66. value: function getContext() {
  67. return {
  68. drawImage: (img: HTMLImageElement) => {
  69. this.__image_src = img.src
  70. },
  71. translate: vi.fn(),
  72. scale: vi.fn(),
  73. }
  74. },
  75. })
  76. Object.defineProperty(HTMLCanvasElement.prototype, 'toDataURL', {
  77. value: function toDataURL() {
  78. return this.__image_src
  79. },
  80. })
  81. // Mock IntersectionObserver feature by injecting it into the global namespace.
  82. // More info here: https://vitest.dev/guide/mocking.html#globals
  83. const IntersectionObserverMock = vi.fn(() => ({
  84. disconnect: vi.fn(),
  85. observe: vi.fn(),
  86. takeRecords: vi.fn(),
  87. unobserve: vi.fn(),
  88. }))
  89. globalThis.IntersectionObserver = IntersectionObserverMock as any
  90. require.extensions['.css'] = () => ({})
  91. globalThis.requestAnimationFrame = (cb) => {
  92. setTimeout(cb, 0)
  93. return 0
  94. }
  95. globalThis.scrollTo = vi.fn<any>()
  96. globalThis.matchMedia = (media: string) => ({
  97. matches: false,
  98. media,
  99. onchange: null,
  100. addListener: vi.fn(),
  101. removeListener: vi.fn(),
  102. dispatchEvent: vi.fn(),
  103. addEventListener: vi.fn(),
  104. removeEventListener: vi.fn(),
  105. })
  106. vi.mock(
  107. '#shared/components/CommonNotifications/useNotifications.ts',
  108. async () => {
  109. const { useNotifications: originalUseNotifications } =
  110. await vi.importActual<any>(
  111. '#shared/components/CommonNotifications/useNotifications.ts',
  112. )
  113. let notifications: any
  114. const useNotifications = () => {
  115. if (notifications) return notifications
  116. const result = originalUseNotifications()
  117. notifications = {
  118. notify: vi.fn(result.notify),
  119. notifications: result.notifications,
  120. removeNotification: vi.fn(result.removeNotification),
  121. clearAllNotifications: vi.fn(result.clearAllNotifications),
  122. hasErrors: vi.fn(result.hasErrors),
  123. }
  124. return notifications
  125. }
  126. return {
  127. useNotifications,
  128. default: useNotifications,
  129. }
  130. },
  131. )
  132. // don't rely on tiptap, because it's not supported in JSDOM
  133. vi.mock(
  134. '#shared/components/Form/fields/FieldEditor/FieldEditorInput.vue',
  135. async () => {
  136. const { computed, defineComponent } = await import('vue')
  137. const component = defineComponent({
  138. name: 'FieldEditorInput',
  139. props: { context: { type: Object, required: true } },
  140. setup(props) {
  141. const value = computed({
  142. get: () => props.context._value,
  143. set: (value) => {
  144. props.context.node.input(value)
  145. },
  146. })
  147. return {
  148. value,
  149. name: props.context.node.name,
  150. id: props.context.id,
  151. }
  152. },
  153. template: `<textarea :id="id" :name="name" v-model="value" />`,
  154. })
  155. return { __esModule: true, default: component }
  156. },
  157. )
  158. // mock vueuse because of CommonDialog, it uses usePointerSwipe
  159. // that is not supported in JSDOM
  160. vi.mock('@vueuse/core', async () => {
  161. const mod =
  162. await vi.importActual<typeof import('@vueuse/core')>('@vueuse/core')
  163. return {
  164. ...mod,
  165. usePointerSwipe: vi
  166. .fn()
  167. .mockReturnValue({ distanceY: 0, isSwiping: false }),
  168. }
  169. })
  170. beforeEach((context) => {
  171. context.skipConsole = false
  172. if (!vi.isMockFunction(console.warn)) {
  173. vi.spyOn(console, 'warn').mockClear()
  174. } else {
  175. vi.mocked(console.warn).mockClear()
  176. }
  177. if (!vi.isMockFunction(console.error)) {
  178. vi.spyOn(console, 'error').mockClear()
  179. } else {
  180. vi.mocked(console.error).mockClear()
  181. }
  182. })
  183. afterEach((context) => {
  184. // we don't import it from `renderComponent`, because renderComponent may not be called
  185. // and it doesn't make sense to import everything from it
  186. if ('cleanupComponents' in globalThis) {
  187. globalThis.cleanupComponents()
  188. }
  189. if (context.skipConsole !== true) {
  190. expect(
  191. console.warn,
  192. 'there were no warning during test',
  193. ).not.toHaveBeenCalled()
  194. expect(
  195. console.error,
  196. 'there were no errors during test',
  197. ).not.toHaveBeenCalled()
  198. }
  199. })
  200. // Import the matchers for accessibility testing with aXe.
  201. expect.extend(matchers)
  202. expect.extend(assertions)
  203. // expect.extend(domMatchers)
  204. expect.extend({
  205. // allow aria-disabled in toBeDisabled
  206. toBeDisabled(received, ...args) {
  207. if (received instanceof Element) {
  208. const attr = received.getAttribute('aria-disabled')
  209. if (!this.isNot && attr === 'true') {
  210. return { pass: true, message: () => '' }
  211. }
  212. if (this.isNot && attr === 'true') {
  213. // pass will be reversed and it will fail
  214. return { pass: true, message: () => 'should not have "aria-disabled"' }
  215. }
  216. }
  217. return (toBeDisabled as any).call(this, received, ...args)
  218. },
  219. })
  220. process.on('uncaughtException', (e) => console.log('Uncaught Exception', e))
  221. process.on('unhandledRejection', (e) => console.log('Unhandled Rejection', e))
  222. declare module 'vitest' {
  223. interface TestContext {
  224. skipConsole: boolean
  225. }
  226. // eslint-disable-next-line @typescript-eslint/no-empty-interface
  227. // interface Assertion<T> extends TestingLibraryMatchers<null, T> {}
  228. }
  229. declare module 'vitest' {
  230. // eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars
  231. interface Assertion<T> extends matchers.AxeMatchers {}
  232. }
  233. declare global {
  234. function cleanupComponents(): void
  235. }