// Test setup file // Configure jsdom and global test utilities with improved type safety /* eslint-disable no-console */ import 'jest-environment-jsdom'; // Mock UUID library at module level let mockUuidCounter = 1; jest.mock('uuid', () => ({ v4: (): string => { const counter = mockUuidCounter++; const hex = counter.toString(16).padStart(32, '0'); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; }, })); // Define proper types for our mocks interface MockStorage { getItem: (key: string) => string | null; setItem: (key: string, value: string) => void; removeItem: (key: string) => void; clear: () => void; length: number; key: (index: number) => string | null; } interface MockResponse { ok: boolean; status: number; statusText: string; json: () => Promise>; text: () => Promise; blob: () => Promise; arrayBuffer: () => Promise; headers: Map; } interface MockFormData { append: (key: string, value: string | File) => void; get: (key: string) => string | File | null; has: (key: string) => boolean; delete: (key: string) => void; entries: () => IterableIterator<[string, string | File]>; forEach: ( callback: (value: string | File, key: string, parent: MockFormData) => void ) => void; } // Mock localStorage with proper implementation const createMockStorage = (): MockStorage => { let store: Record = {}; return { getItem: (key: string): string | null => { return store[key] || null; }, setItem: (key: string, value: string): void => { store[key] = value.toString(); }, removeItem: (key: string): void => { delete store[key]; }, clear: (): void => { store = {}; }, get length(): number { return Object.keys(store).length; }, key: (index: number): string | null => { const keys = Object.keys(store); return keys[index] || null; }, }; }; const localStorageMock = createMockStorage(); const sessionStorageMock = createMockStorage(); // Attach storage mocks to global Object.defineProperty(global, 'localStorage', { value: localStorageMock, writable: true, }); Object.defineProperty(global, 'sessionStorage', { value: sessionStorageMock, writable: true, }); // Mock fetch with comprehensive response handling const createMockResponse = ( data: Record = { id: 'test-message-id' }, options: Partial = {} ): MockResponse => ({ ok: true, status: 200, statusText: 'OK', json: () => Promise.resolve(data), text: () => Promise.resolve(JSON.stringify(data)), blob: () => Promise.resolve(new Blob([JSON.stringify(data)])), arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), headers: new Map(), ...options, }); global.fetch = jest .fn() .mockResolvedValue( createMockResponse() as unknown as Response ) as unknown as jest.MockedFunction; // Mock FormData with proper implementation class MockFormDataImpl implements MockFormData { private data: Map = new Map(); append(key: string, value: string | File): void { this.data.set(key, value); } get(key: string): string | File | null { return this.data.get(key) || null; } has(key: string): boolean { return this.data.has(key); } delete(key: string): void { this.data.delete(key); } entries(): IterableIterator<[string, string | File]> { return this.data.entries(); } forEach( callback: (value: string | File, key: string, parent: MockFormData) => void ): void { this.data.forEach((value, key) => callback(value, key, this)); } // Additional methods for completeness getAll(key: string): (string | File)[] { const value = this.data.get(key); return value ? [value] : []; } keys(): IterableIterator { return this.data.keys(); } values(): IterableIterator { return this.data.values(); } set(key: string, value: string | File): void { this.data.set(key, value); } [Symbol.iterator](): IterableIterator<[string, string | File]> { return this.data.entries(); } } global.FormData = MockFormDataImpl as unknown as typeof FormData; // Mock File API global.File = class MockFile { name: string; size: number; type: string; lastModified: number; constructor( bits: BlobPart[], filename: string, options: FilePropertyBag = {} ) { this.name = filename; this.size = bits.reduce((size, bit) => { if (typeof bit === 'string') return size + bit.length; if (bit instanceof ArrayBuffer) return size + bit.byteLength; return size; }, 0); this.type = options.type || ''; this.lastModified = options.lastModified || Date.now(); } stream(): ReadableStream { throw new Error('Not implemented in test environment'); } arrayBuffer(): Promise { return Promise.resolve(new ArrayBuffer(this.size)); } text(): Promise { return Promise.resolve('mock file content'); } slice(): Blob { return new Blob(); } } as unknown as typeof File; // Mock Blob API global.Blob = class MockBlob { size: number; type: string; constructor(blobParts: BlobPart[] = [], options: BlobPropertyBag = {}) { this.size = blobParts.reduce((size, part) => { if (typeof part === 'string') return size + part.length; if (part instanceof ArrayBuffer) return size + part.byteLength; return size; }, 0); this.type = options.type || ''; } arrayBuffer(): Promise { return Promise.resolve(new ArrayBuffer(this.size)); } text(): Promise { return Promise.resolve('mock blob content'); } stream(): ReadableStream { throw new Error('Not implemented in test environment'); } slice(): Blob { return new MockBlob() as unknown as Blob; } bytes(): Promise> { return Promise.resolve(new Uint8Array(new ArrayBuffer(this.size))); } } as unknown as typeof Blob; // Mock crypto functions global.btoa = (str: string): string => Buffer.from(str).toString('base64'); global.atob = (str: string): string => Buffer.from(str, 'base64').toString(); // Mock URL for file handling global.URL = { createObjectURL: jest.fn(() => 'mock-url'), revokeObjectURL: jest.fn(), } as unknown as typeof URL; // Mock crypto.getRandomValues for UUID generation Object.defineProperty(global, 'crypto', { value: { getRandomValues: (arr: Uint8Array): Uint8Array => { for (let i = 0; i < arr.length; i++) { arr[i] = Math.floor(Math.random() * 256); } return arr; }, randomUUID: (): string => { return 'mock-uuid-' + (mockUuidCounter++).toString().padStart(8, '0'); }, }, }); // Mock performance API global.performance = { now: jest.fn(() => Date.now()), mark: jest.fn(), measure: jest.fn(), getEntriesByType: jest.fn(() => []), getEntriesByName: jest.fn(() => []), } as unknown as Performance; // Mock ResizeObserver global.ResizeObserver = class MockResizeObserver { observe = jest.fn(); unobserve = jest.fn(); disconnect = jest.fn(); } as unknown as typeof ResizeObserver; // Mock IntersectionObserver global.IntersectionObserver = class MockIntersectionObserver { observe = jest.fn(); unobserve = jest.fn(); disconnect = jest.fn(); root = null; rootMargin = ''; thresholds = []; } as unknown as typeof IntersectionObserver; // Mock import.meta for Vite environment variables Object.defineProperty(globalThis, 'import', { value: { meta: { env: { NODE_ENV: 'test', MODE: 'test', DEV: false, PROD: false, SSR: false, VITE_COUCHDB_URL: 'http://localhost:5984', VITE_COUCHDB_USERNAME: 'admin', VITE_COUCHDB_PASSWORD: 'password', VITE_MAILGUN_API_KEY: 'test-key-12345', VITE_MAILGUN_DOMAIN: 'test.mailgun.org', VITE_MAILGUN_BASE_URL: 'https://api.mailgun.net/v3', VITE_MAILGUN_FROM_NAME: 'Test Medication App', VITE_MAILGUN_FROM_EMAIL: 'test@medapp.example.com', VITE_APP_BASE_URL: 'http://localhost:5173', }, }, }, writable: true, }); // Mock process.env for Node.js environment variables process.env = { ...process.env, NODE_ENV: 'test', JWT_SECRET: 'test-jwt-secret', REFRESH_TOKEN_SECRET: 'test-refresh-secret', }; // Setup function to clear all mocks before each test const clearAllMocks = (): void => { localStorageMock.clear(); sessionStorageMock.clear(); (global.fetch as jest.MockedFunction).mockClear(); mockUuidCounter = 1; // Reset UUID counter jest.clearAllMocks(); }; // Global beforeEach hook beforeEach(() => { clearAllMocks(); // Reset fetch mock to default success response (global.fetch as jest.MockedFunction).mockResolvedValue( createMockResponse() as unknown as Response ); }); // Console management for cleaner test output const originalConsole = { error: console.error, warn: console.warn, log: console.log, info: console.info, }; // Patterns to suppress in test output const SUPPRESSED_PATTERNS = [ // React warnings 'Warning: ReactDOM.render is deprecated', 'Warning: componentWillMount has been renamed', 'Warning: componentWillReceiveProps has been renamed', 'Warning: componentWillUpdate has been renamed', // Application specific warnings '📧 Mailgun Service', 'Failed to send verification email', 'Email sending failed', 'Configured for production with domain:', // Auth flow logging '🔐 Login attempt for:', '❌ User not found for email:', '👤 User found:', '🔍 Comparing passwords:', '❌ Password mismatch', '✅ Login successful for:', 'Registration failed: User already exists', 'Password mismatch', 'User not found for email:', // Database warnings 'CouchDB connection', 'Mock database operation', // JSDOM navigation warnings 'Error: Not implemented: navigation', 'Not implemented: navigation (except hash changes)', ]; const shouldSuppressMessage = (message: string): boolean => { return SUPPRESSED_PATTERNS.some(pattern => message.includes(pattern)); }; beforeAll(() => { // Suppress console noise during tests console.error = (...args: unknown[]): void => { const message = args[0]?.toString() || ''; if (!shouldSuppressMessage(message)) { originalConsole.error(...args); } }; console.warn = (...args: unknown[]): void => { const message = args[0]?.toString() || ''; if (!shouldSuppressMessage(message)) { originalConsole.warn(...args); } }; // Suppress info and log in tests unless explicitly needed console.info = jest.fn(); console.log = jest.fn(); }); afterAll(() => { // Restore original console methods Object.assign(console, originalConsole); }); // Utility functions for tests export const testUtils = { clearAllMocks, createMockResponse, shouldSuppressMessage, // Helper to create mock users for tests createMockUser: ( overrides: Record = {} ): Record => ({ _id: 'test-user-id', email: 'test@example.com', username: 'testuser', password: 'password123', emailVerified: false, status: 'PENDING', createdAt: new Date(), ...overrides, }), // Helper to mock async delays delay: (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)), // Helper to wait for async operations in tests waitFor: async ( callback: () => boolean | Promise, options: { timeout?: number; interval?: number } = {} ): Promise => { const { timeout = 5000, interval = 50 } = options; const start = Date.now(); while (Date.now() - start < timeout) { const result = await callback(); if (result) return; await testUtils.delay(interval); } throw new Error(`waitFor timeout after ${timeout}ms`); }, }; // Export for use in tests if needed export default testUtils;