vitest.setup.ts 6.9 KB

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