setup.components.ts 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. import { afterEach, vi } from 'vitest';
  2. import { cleanup } from '@testing-library/react';
  3. import i18next from 'i18next';
  4. import { initReactI18next } from 'react-i18next';
  5. import enUS from '../../../internal/web/translation/en-US.json';
  6. vi.mock('persian-calendar-suite', () => ({
  7. PersianDateTimePicker: () => null,
  8. }));
  9. if (typeof globalThis.localStorage === 'undefined') {
  10. const store = new Map<string, string>();
  11. const storage = {
  12. getItem: (k: string) => (store.has(k) ? store.get(k)! : null),
  13. setItem: (k: string, v: string) => {
  14. store.set(k, String(v));
  15. },
  16. removeItem: (k: string) => {
  17. store.delete(k);
  18. },
  19. clear: () => {
  20. store.clear();
  21. },
  22. key: (i: number) => Array.from(store.keys())[i] ?? null,
  23. get length() {
  24. return store.size;
  25. },
  26. } as Storage;
  27. Object.defineProperty(globalThis, 'localStorage', { value: storage, configurable: true });
  28. Object.defineProperty(globalThis, 'sessionStorage', { value: storage, configurable: true });
  29. }
  30. if (!window.matchMedia) {
  31. window.matchMedia = ((query: string) => ({
  32. matches: false,
  33. media: query,
  34. onchange: null,
  35. addListener: () => {},
  36. removeListener: () => {},
  37. addEventListener: () => {},
  38. removeEventListener: () => {},
  39. dispatchEvent: () => false,
  40. })) as unknown as typeof window.matchMedia;
  41. }
  42. if (typeof globalThis.ResizeObserver === 'undefined') {
  43. globalThis.ResizeObserver = class {
  44. observe() {}
  45. unobserve() {}
  46. disconnect() {}
  47. } as unknown as typeof ResizeObserver;
  48. }
  49. if (!Element.prototype.scrollIntoView) {
  50. Element.prototype.scrollIntoView = () => {};
  51. }
  52. // jsdom does not implement pseudo-element styles or Range geometry. Ant
  53. // Design and CodeMirror use these APIs for layout, so supply harmless test
  54. // fallbacks instead of emitting noisy "Not implemented" errors.
  55. const nativeGetComputedStyle = window.getComputedStyle.bind(window);
  56. window.getComputedStyle = ((element: Element) =>
  57. nativeGetComputedStyle(element)) as typeof window.getComputedStyle;
  58. if (!Range.prototype.getClientRects) {
  59. Range.prototype.getClientRects = () => [] as unknown as DOMRectList;
  60. }
  61. if (!i18next.isInitialized) {
  62. void i18next.use(initReactI18next).init({
  63. lng: 'en-US',
  64. fallbackLng: 'en-US',
  65. resources: { 'en-US': { translation: enUS } },
  66. interpolation: { escapeValue: false, prefix: '{', suffix: '}' },
  67. returnNull: false,
  68. });
  69. }
  70. afterEach(async () => {
  71. cleanup();
  72. document.body.innerHTML = '';
  73. /*
  74. * React 19 defers passive-effect flushes onto a macrotask (setImmediate),
  75. * whose callback reads `window.event`. If one is still queued when vitest
  76. * tears down the jsdom environment, it fires after `window` is gone and
  77. * throws "window is not defined". Drain a few macrotask ticks here so any
  78. * pending callback runs while `window` still exists. Several ticks are used
  79. * because a microtask resolving mid-drain (rc-trigger/AntD) can queue a new
  80. * one behind the first.
  81. */
  82. for (let i = 0; i < 3; i += 1) {
  83. await new Promise((resolve) => setTimeout(resolve, 0));
  84. }
  85. });
  86. import { HttpUtil, Msg } from '@/utils';
  87. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  88. vi.spyOn(HttpUtil, 'post').mockResolvedValue({ success: true, obj: {} } as any);
  89. vi.spyOn(HttpUtil, 'get').mockImplementation(
  90. async (url: string) => new Msg(true, '', url.includes('/panel/api/inbounds/options') ? [] : {}),
  91. );