setup.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. /**
  2. * Test setup file for Vitest.
  3. * Configures testing environment, mocks, and MSW server.
  4. */
  5. import '@testing-library/jest-dom';
  6. import { afterAll, afterEach, beforeAll, vi } from 'vitest';
  7. import { cleanup } from '@testing-library/react';
  8. import { server } from './mocks/server';
  9. // Initialize i18n for tests (suppresses react-i18next warnings)
  10. import '../i18n';
  11. // Setup MSW server
  12. beforeAll(() =>
  13. server.listen({
  14. // Bypass unhandled requests silently (don't warn, just let them through)
  15. // Handlers use wildcard (*) prefix to match any origin
  16. onUnhandledRequest: 'bypass',
  17. })
  18. );
  19. afterEach(() => {
  20. cleanup();
  21. server.resetHandlers();
  22. });
  23. afterAll(() => server.close());
  24. // Mock window.matchMedia for responsive components
  25. // Uses a plain function (not vi.fn) so vi.restoreAllMocks() in tests can't wipe it
  26. Object.defineProperty(window, 'matchMedia', {
  27. writable: true,
  28. value: (query: string) => ({
  29. matches: false,
  30. media: query,
  31. onchange: null,
  32. addListener: () => {},
  33. removeListener: () => {},
  34. addEventListener: () => {},
  35. removeEventListener: () => {},
  36. dispatchEvent: () => true,
  37. }),
  38. });
  39. // Mock ResizeObserver
  40. class ResizeObserverMock {
  41. observe = vi.fn();
  42. unobserve = vi.fn();
  43. disconnect = vi.fn();
  44. }
  45. vi.stubGlobal('ResizeObserver', ResizeObserverMock);
  46. // Mock IntersectionObserver
  47. class IntersectionObserverMock {
  48. observe = vi.fn();
  49. unobserve = vi.fn();
  50. disconnect = vi.fn();
  51. root = null;
  52. rootMargin = '';
  53. thresholds = [];
  54. }
  55. vi.stubGlobal('IntersectionObserver', IntersectionObserverMock);
  56. // Mock WebSocket
  57. class MockWebSocket {
  58. static readonly CONNECTING = 0;
  59. static readonly OPEN = 1;
  60. static readonly CLOSING = 2;
  61. static readonly CLOSED = 3;
  62. readyState = MockWebSocket.OPEN;
  63. onopen: ((event: Event) => void) | null = null;
  64. onclose: ((event: CloseEvent) => void) | null = null;
  65. onmessage: ((event: MessageEvent) => void) | null = null;
  66. onerror: ((event: Event) => void) | null = null;
  67. url: string;
  68. constructor(url: string) {
  69. this.url = url;
  70. setTimeout(() => this.onopen?.(new Event('open')), 0);
  71. }
  72. send = vi.fn();
  73. close = vi.fn();
  74. }
  75. vi.stubGlobal('WebSocket', MockWebSocket);
  76. // Mock scrollTo
  77. window.scrollTo = vi.fn();
  78. // Silence jsdom's "Not implemented: navigation (except hash changes)"
  79. // warning when production code does ``window.location.href = '/setup'``
  80. // (AuthContext setup-redirect) or other full-page nav assignments.
  81. //
  82. // jsdom defines ``href`` as a non-configurable accessor on
  83. // ``Location.prototype``, so it cannot be redefined on the instance via
  84. // ``Object.defineProperty``. We wrap the real jsdom Location in a Proxy
  85. // that turns ``href = "..."`` writes into silent no-ops; everything
  86. // else (reads of ``pathname`` / ``search`` / ``hash``, writes to
  87. // ``hash``, ``assign()`` / ``replace()`` calls, ``history.replaceState``
  88. // updating ``search``) passes through unchanged. The ``get`` trap is
  89. // deliberately permissive: returning a substitute value for a non-
  90. // configurable target property violates Proxy invariants and the spread
  91. // operator (``{ ...window.location }``) walks every key — tests that
  92. // use the spread to copy the location object must keep working.
  93. {
  94. const realLocation = window.location;
  95. const locationProxy = new Proxy(realLocation, {
  96. set(target, prop, value) {
  97. if (prop === 'href') {
  98. // Silently swallow "navigation not implemented". Tests asserting
  99. // on the redirect should replace ``window.location`` themselves
  100. // (several existing tests do exactly this).
  101. return true;
  102. }
  103. Reflect.set(target, prop, value);
  104. return true;
  105. },
  106. get(target, prop, receiver) {
  107. // Return the exact value present on the target. Returning a
  108. // bound/wrapped version of a non-configurable function (``assign``
  109. // is one) violates Proxy invariants (the spread operator at one
  110. // call site triggers this). Production code that does
  111. // ``window.location.assign(url)`` calls the function with the
  112. // proxy as ``this``, which jsdom still accepts because its
  113. // Location methods unwrap their receiver internally.
  114. return Reflect.get(target, prop, receiver);
  115. },
  116. });
  117. Object.defineProperty(window, 'location', {
  118. configurable: true,
  119. writable: true,
  120. value: locationProxy,
  121. });
  122. }
  123. // Mock localStorage
  124. const localStorageMock = {
  125. getItem: vi.fn(),
  126. setItem: vi.fn(),
  127. removeItem: vi.fn(),
  128. clear: vi.fn(),
  129. };
  130. Object.defineProperty(window, 'localStorage', { value: localStorageMock });
  131. // Suppress console output during tests (reduces noise)
  132. // Remove these lines if you need to debug test output
  133. vi.spyOn(console, 'log').mockImplementation(() => {});
  134. vi.spyOn(console, 'warn').mockImplementation(() => {});
  135. vi.spyOn(console, 'error').mockImplementation(() => {});