vitest.setup.ts 7.0 KB

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