-
Notifications
You must be signed in to change notification settings - Fork 26
Expand file tree
/
Copy pathvitest.setup.mts
More file actions
385 lines (341 loc) · 11.7 KB
/
vitest.setup.mts
File metadata and controls
385 lines (341 loc) · 11.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
// Testing library matchers for Vitest
import "@testing-library/jest-dom/vitest";
import { beforeEach, type Mock, vi } from "vitest";
// ============================================================================
// Type Declarations
// ============================================================================
import React, { ReactNode } from "react";
interface MockRouter {
push: Mock;
replace: Mock;
back: Mock;
forward: Mock;
refresh: Mock;
prefetch: Mock;
}
interface MockNextNavigation {
setPathname: (pathname: string) => void;
setSearchParams: (params: string | Record<string, string>) => void;
setParams: (params: Record<string, string>) => void;
getRouter: () => MockRouter;
reset: () => void;
}
declare global {
var mockNextNavigation: MockNextNavigation;
}
// ============================================================================
// Polyfills for test environment compatibility
// ============================================================================
// Polyfill localStorage for Node.js 22+ where the built-in localStorage
// is a plain object without standard Storage methods (getItem, setItem, etc.).
// happy-dom should provide these but Node's built-in takes precedence.
if (
typeof globalThis.localStorage !== "undefined" &&
typeof globalThis.localStorage.getItem !== "function"
) {
const store = new Map<string, string>();
const storage = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => store.set(key, String(value)),
removeItem: (key: string) => store.delete(key),
clear: () => store.clear(),
key: (index: number) => [...store.keys()][index] ?? null,
get length() {
return store.size;
},
};
Object.defineProperty(globalThis, "localStorage", {
value: storage,
writable: true,
configurable: true,
});
}
// Polyfill for structuredClone (not available in jsdom)
// Required by Chakra UI v3 and other modern libraries
if (typeof structuredClone === "undefined") {
(global as Record<string, unknown>).structuredClone = <T,>(obj: T): T =>
JSON.parse(JSON.stringify(obj));
}
// Polyfill for ResizeObserver (not available in jsdom)
// Required by many UI libraries including Chakra UI
if (typeof ResizeObserver === "undefined") {
(global as Record<string, unknown>).ResizeObserver = class ResizeObserver {
callback: ResizeObserverCallback;
constructor(callback: ResizeObserverCallback) {
this.callback = callback;
}
observe() {
/* void on purpose */
}
unobserve() {
/* void on purpose */
}
disconnect() {
/* void on purpose */
}
};
}
// Polyfill for matchMedia (not fully implemented in jsdom)
if (typeof window.matchMedia === "undefined") {
Object.defineProperty(window, "matchMedia", {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
}
// ============================================================================
// Next.js Navigation Mock
// ============================================================================
const mockRouter: MockRouter = {
push: vi.fn(),
replace: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
prefetch: vi.fn(),
};
let mockPathname = "/";
let mockSearchParams = new URLSearchParams();
let mockParams: Record<string, string> = {};
vi.mock("next/navigation", () => {
// Create the mock function inside the factory to avoid hoisting issues
const useRouterMock = vi.fn(() => mockRouter);
return {
useRouter: useRouterMock,
usePathname: () => mockPathname,
useSearchParams: () => mockSearchParams,
useParams: () => mockParams,
notFound: vi.fn(),
redirect: vi.fn(),
};
});
// Export helpers to control mock state in tests
const mockNextNavigation: MockNextNavigation = {
setPathname: (pathname: string) => {
mockPathname = pathname;
},
setSearchParams: (params: string | Record<string, string>) => {
mockSearchParams = new URLSearchParams(params);
},
setParams: (params: Record<string, string>) => {
mockParams = params;
},
getRouter: () => mockRouter,
reset: () => {
mockPathname = "/";
mockSearchParams = new URLSearchParams();
mockParams = {};
mockRouter.push.mockClear();
mockRouter.replace.mockClear();
mockRouter.back.mockClear();
mockRouter.forward.mockClear();
mockRouter.refresh.mockClear();
mockRouter.prefetch.mockClear();
},
};
// Make available globally
global.mockNextNavigation = mockNextNavigation;
// Reset mocks before each test
beforeEach(() => {
global.mockNextNavigation.reset();
});
// ============================================================================
// Global Mocks for ESM-only Modules
// ============================================================================
// Mock react-markdown to avoid ESM issues
// Using simple string return instead of JSX to avoid parsing issues in .mts
vi.mock("react-markdown", () => ({
__esModule: true,
default: ({ children }: { children: unknown }) => children,
}));
// Mock remark-gfm
vi.mock("remark-gfm", () => ({
__esModule: true,
default: () => null,
}));
// Mock react-syntax-highlighter
// Using simple string return instead of JSX
vi.mock("react-syntax-highlighter", () => ({
Prism: ({ children }: { children: string }) => children,
}));
vi.mock("react-syntax-highlighter/dist/esm/styles/prism", () => ({
oneDark: {},
}));
// ag-grid-community is handled via alias in vitest.config.mts
// pointing to __mocks__/ag-grid-community.ts
// Mock react-icons/lu - returns simple span elements for used icons
vi.mock("react-icons/lu", () => {
const createIconMock = (name: string) => {
const IconComponent = (props: Record<string, unknown>) =>
React.createElement("span", { "data-testid": name, ...props });
IconComponent.displayName = name;
return IconComponent;
};
// Explicitly export the icons that are used in the codebase
return {
LuChartBarBig: createIconMock("LuChartBarBig"),
LuExternalLink: createIconMock("LuExternalLink"),
LuSave: createIconMock("LuSave"),
};
});
// Mock ag-grid-react for component rendering
vi.mock("ag-grid-react", () => {
return {
AgGridReact: ({ children }: { children?: ReactNode }) =>
React.createElement(
"div",
{ "data-testid": "ag-grid-mock" },
children ?? null,
),
};
});
// Note: ScreenshotDataGrid mock is handled via alias in vitest.config.mts
// pointing to packages/ui/src/components/data/__mocks__/ScreenshotDataGrid.tsx
// ============================================================================
// Network Request Mocking
// ============================================================================
// Store original fetch for potential use in tests that need it
const originalFetch = globalThis.fetch;
// Create a mock fetch that silently fails for unmocked requests
// This prevents AggregateError and NetworkError from tests that trigger
// navigation or make API calls that aren't explicitly mocked
const mockFetch = vi.fn(
async (input: RequestInfo | URL, _init?: RequestInit) => {
// Silently return a mock response for unmocked requests
// Tests that need real fetch behavior should mock it explicitly
if (process.env.NODE_ENV === "test") {
return new Response(
JSON.stringify({ error: "Network request not mocked" }),
{
status: 500,
statusText: "Network request not mocked in test",
},
);
}
return await originalFetch(input, _init);
},
);
// Replace global fetch with mock
globalThis.fetch = mockFetch;
// ============================================================================
// Suppress Network Errors in Console Output
// ============================================================================
// These errors are logged to console by happy-dom and other libraries
// during test execution and teardown. They don't affect test results
// but pollute the output.
const originalConsoleError = console.error;
const originalConsoleWarn = console.warn;
// Patterns for network-related errors to suppress
const networkErrorPatterns = [
/AggregateError/,
/ECONNREFUSED/,
/ECONNRESET/,
/socket hang up/,
/NetworkError/,
/AbortError/,
/The operation was aborted/,
/Failed to execute "fetch\(\)"/,
/connect ECONNREFUSED/,
/internalConnectMultiple/,
/afterConnectMultiple/,
/TLSSocket/,
/socketCloseListener/,
/socketErrorListener/,
/Fetch\.onError/,
/Fetch\.onAsyncTaskManagerAbort/,
/AsyncTaskManager/,
/DetachedBrowserFrame/,
/DetachedWindowAPI/,
/teardownWindow/,
];
function shouldSuppressMessage(message: string): boolean {
return networkErrorPatterns.some((pattern) => pattern.test(message));
}
function formatArgs(args: unknown[]): string {
return args
.map((arg) => {
if (arg instanceof Error) {
return `${arg.name}: ${arg.message}\n${arg.stack}`;
}
return String(arg);
})
.join(" ");
}
console.error = (...args: unknown[]) => {
if (shouldSuppressMessage(formatArgs(args))) {
return; // Suppress network-related errors
}
originalConsoleError.apply(console, args);
};
console.warn = (...args: unknown[]) => {
if (shouldSuppressMessage(formatArgs(args))) {
return; // Suppress network-related warnings
}
originalConsoleWarn.apply(console, args);
};
// Intercept stderr to catch errors that bypass console.error
// This catches Node.js internal error logging
const originalStderrWrite = process.stderr.write.bind(process.stderr);
process.stderr.write = ((
chunk: string | Uint8Array,
encodingOrCb?: BufferEncoding | ((err?: Error | null) => void),
cb?: (err?: Error | null) => void,
): boolean => {
const message = typeof chunk === "string" ? chunk : chunk.toString();
if (shouldSuppressMessage(message)) {
// Call the callback if provided to avoid breaking async operations
const callback = typeof encodingOrCb === "function" ? encodingOrCb : cb;
if (callback) callback();
return true;
}
return originalStderrWrite(chunk, encodingOrCb as BufferEncoding, cb);
}) as typeof process.stderr.write;
// ============================================================================
// Suppress Happy-DOM Async Errors
// ============================================================================
// Suppress unhandled promise rejections from happy-dom teardown
// These are DOMException [AbortError] from pending async operations
process.removeAllListeners("unhandledRejection");
process.on("unhandledRejection", (reason: unknown) => {
// Suppress AbortError and NetworkError from happy-dom
if (reason instanceof Error) {
const errorName = reason.name || "";
const errorMessage = reason.message || "";
if (
errorName === "AbortError" ||
errorName === "NetworkError" ||
errorMessage.includes("aborted") ||
errorMessage.includes("ECONNREFUSED") ||
errorMessage.includes("ECONNRESET") ||
errorMessage.includes("socket hang up")
) {
// Silently ignore these errors during tests
return;
}
}
// Log other unhandled rejections
originalConsoleError("Unhandled rejection:", reason);
});
// Also handle uncaught exceptions for socket errors
process.removeAllListeners("uncaughtException");
process.on("uncaughtException", (error: Error) => {
const errorMessage = error.message || "";
if (
errorMessage.includes("ECONNREFUSED") ||
errorMessage.includes("ECONNRESET") ||
errorMessage.includes("socket hang up")
) {
// Silently ignore socket errors during tests
return;
}
// Re-throw other exceptions
throw error;
});