- Add proper TypeScript types for all mocks - Implement UUID library mocking for unique token generation - Add comprehensive fetch, FormData, Blob, and File API mocks - Include crypto, performance, and observer API mocks - Add utility functions for test helpers - Improve console suppression for cleaner test output - Fix localStorage and sessionStorage implementation
453 lines
12 KiB
TypeScript
453 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 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',
|
|
|
|
// Auth flow logging
|
|
'🔐 Login attempt for:',
|
|
'❌ User not found for email:',
|
|
'👤 User found:',
|
|
'🔍 Comparing passwords:',
|
|
'❌ Password mismatch',
|
|
'✅ Login successful for:',
|
|
|
|
// Database warnings
|
|
'CouchDB connection',
|
|
'Mock database operation',
|
|
];
|
|
|
|
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;
|