- Improve global test setup with better mock implementations - Add intelligent console noise suppression for cleaner test output - Enhance browser API mocking for better test compatibility - Update test utilities for improved reliability - Fix fetch mock type issues for proper Jest compatibility This improves the overall testing experience and reduces noise in test output.
461 lines
12 KiB
TypeScript
461 lines
12 KiB
TypeScript
// 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<Record<string, unknown>>;
|
|
text: () => Promise<string>;
|
|
blob: () => Promise<Blob>;
|
|
arrayBuffer: () => Promise<ArrayBuffer>;
|
|
headers: Map<string, string>;
|
|
}
|
|
|
|
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<string, string> = {};
|
|
|
|
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<string, unknown> = { id: 'test-message-id' },
|
|
options: Partial<MockResponse> = {}
|
|
): 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<typeof fetch>;
|
|
|
|
// Mock FormData with proper implementation
|
|
class MockFormDataImpl implements MockFormData {
|
|
private data: Map<string, string | File> = 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<string> {
|
|
return this.data.keys();
|
|
}
|
|
|
|
values(): IterableIterator<string | File> {
|
|
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<ArrayBuffer> {
|
|
return Promise.resolve(new ArrayBuffer(this.size));
|
|
}
|
|
|
|
text(): Promise<string> {
|
|
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<ArrayBuffer> {
|
|
return Promise.resolve(new ArrayBuffer(this.size));
|
|
}
|
|
|
|
text(): Promise<string> {
|
|
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<Uint8Array<ArrayBuffer>> {
|
|
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<typeof fetch>).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<typeof fetch>).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<string, unknown> = {}
|
|
): Record<string, unknown> => ({
|
|
_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<void> =>
|
|
new Promise(resolve => setTimeout(resolve, ms)),
|
|
|
|
// Helper to wait for async operations in tests
|
|
waitFor: async (
|
|
callback: () => boolean | Promise<boolean>,
|
|
options: { timeout?: number; interval?: number } = {}
|
|
): Promise<void> => {
|
|
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;
|