setup.components.ts 2.5 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
  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 '../../../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) => { store.set(k, String(v)); },
  14. removeItem: (k: string) => { store.delete(k); },
  15. clear: () => { store.clear(); },
  16. key: (i: number) => Array.from(store.keys())[i] ?? null,
  17. get length() { return store.size; },
  18. } as Storage;
  19. Object.defineProperty(globalThis, 'localStorage', { value: storage, configurable: true });
  20. Object.defineProperty(globalThis, 'sessionStorage', { value: storage, configurable: true });
  21. }
  22. if (!window.matchMedia) {
  23. window.matchMedia = ((query: string) => ({
  24. matches: false,
  25. media: query,
  26. onchange: null,
  27. addListener: () => {},
  28. removeListener: () => {},
  29. addEventListener: () => {},
  30. removeEventListener: () => {},
  31. dispatchEvent: () => false,
  32. })) as unknown as typeof window.matchMedia;
  33. }
  34. if (typeof globalThis.ResizeObserver === 'undefined') {
  35. globalThis.ResizeObserver = class {
  36. observe() {}
  37. unobserve() {}
  38. disconnect() {}
  39. } as unknown as typeof ResizeObserver;
  40. }
  41. if (!Element.prototype.scrollIntoView) {
  42. Element.prototype.scrollIntoView = () => {};
  43. }
  44. if (!i18next.isInitialized) {
  45. void i18next.use(initReactI18next).init({
  46. lng: 'en-US',
  47. fallbackLng: 'en-US',
  48. resources: { 'en-US': { translation: enUS } },
  49. interpolation: { escapeValue: false, prefix: '{', suffix: '}' },
  50. returnNull: false,
  51. });
  52. }
  53. afterEach(async () => {
  54. cleanup();
  55. document.body.innerHTML = '';
  56. /*
  57. * React 19 defers passive-effect flushes onto a macrotask (setImmediate),
  58. * whose callback reads `window.event`. If one is still queued when vitest
  59. * tears down the jsdom environment, it fires after `window` is gone and
  60. * throws "window is not defined". Drain a few macrotask ticks here so any
  61. * pending callback runs while `window` still exists. Several ticks are used
  62. * because a microtask resolving mid-drain (rc-trigger/AntD) can queue a new
  63. * one behind the first.
  64. */
  65. for (let i = 0; i < 3; i += 1) {
  66. await new Promise((resolve) => setTimeout(resolve, 0));
  67. }
  68. });