@corez/mock
Advanced tools
Comparing version 0.2.2 to 0.2.4
/** | ||
* Configuration management for the mocking system | ||
*/ | ||
/** | ||
* Global configuration options | ||
*/ | ||
interface Config { | ||
/** Whether to enable debug mode */ | ||
debug: boolean; | ||
/** Whether to track method calls by default */ | ||
trackCalls: boolean; | ||
/** Whether to allow undefined properties */ | ||
allowUndefined: boolean; | ||
/** Whether to use strict mode */ | ||
strict: boolean; | ||
/** Default timeout for async operations (ms) */ | ||
timeout: number; | ||
/** Whether to preserve prototype chain */ | ||
preservePrototype: boolean; | ||
} | ||
/** | ||
* Configuration update type | ||
*/ | ||
type ConfigUpdate = Partial<Config>; | ||
/** | ||
* Configuration statistics | ||
*/ | ||
interface ConfigStats { | ||
updateCount: number; | ||
lastUpdate: number; | ||
cacheHits: number; | ||
} | ||
/** | ||
* Global configuration API | ||
* Similar to Jest's global config API | ||
*/ | ||
declare const configManager: { | ||
/** | ||
* Gets current configuration | ||
*/ | ||
get(): Readonly<Config>; | ||
/** | ||
* Updates configuration | ||
*/ | ||
set(options: ConfigUpdate): void; | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
reset(): void; | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
with<T>(options: ConfigUpdate, fn: () => T): T; | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
getStats(): ConfigStats; | ||
}; | ||
declare const mockConfig: { | ||
/** | ||
* Gets current configuration | ||
*/ | ||
get(): Readonly<Config>; | ||
/** | ||
* Updates configuration | ||
*/ | ||
set(options: ConfigUpdate): void; | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
reset(): void; | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
with<T>(options: ConfigUpdate, fn: () => T): T; | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
getStats(): ConfigStats; | ||
}; | ||
/** | ||
* Custom error types and error handling utilities for the mocking system | ||
*/ | ||
/** | ||
* Error codes for different types of mocking errors | ||
*/ | ||
declare enum MockErrorCode { | ||
CIRCULAR_REFERENCE = "CIRCULAR_REF", | ||
INVALID_TARGET = "INVALID_TARGET", | ||
INITIALIZATION_FAILED = "INIT_FAILED", | ||
TYPE_MISMATCH = "TYPE_MISMATCH", | ||
INVALID_IMPLEMENTATION = "INVALID_IMPL", | ||
INVALID_CONFIGURATION = "INVALID_CONFIG", | ||
VERIFICATION_ERROR = "VERIFY_ERROR", | ||
EXPECTATION_FAILED = "EXPECT_FAILED", | ||
PERFORMANCE_CONSTRAINT_VIOLATED = "PERF_VIOLATED" | ||
} | ||
/** | ||
* Base error class for all mocking related errors | ||
*/ | ||
declare class MockError extends Error { | ||
readonly code: MockErrorCode; | ||
readonly details?: Record<string, any> | undefined; | ||
constructor(code: MockErrorCode, message: string, details?: Record<string, any> | undefined); | ||
/** | ||
* Creates a circular reference error | ||
*/ | ||
static circularReference(property: string | symbol, details?: Record<string, any>): MockError; | ||
/** | ||
* Creates an invalid target error | ||
*/ | ||
static invalidTarget(target: any, expected: string): MockError; | ||
/** | ||
* Creates an initialization failed error | ||
*/ | ||
static initializationFailed(message: string, details?: Record<string, any>): MockError; | ||
/** | ||
* Creates a type mismatch error | ||
*/ | ||
static typeMismatch(expected: string, received: string, details?: Record<string, any>): MockError; | ||
/** | ||
* Creates an invalid implementation error | ||
*/ | ||
static invalidImplementation(message: string, details?: Record<string, any>): MockError; | ||
/** | ||
* Creates an expectation failed error | ||
*/ | ||
static expectationFailed(message: string, details?: Record<string, any>): MockError; | ||
/** | ||
* Creates a performance constraint violation error | ||
*/ | ||
static performanceConstraintViolated(message: string, details?: Record<string, any>): MockError; | ||
} | ||
/** | ||
* Cache management types and utilities for the mocking system | ||
*/ | ||
/** | ||
* Represents a cached value with its metadata | ||
*/ | ||
interface CacheEntry<T = any> { | ||
value: T; | ||
descriptor?: PropertyDescriptor; | ||
ref?: WeakRef<any>; | ||
timestamp: number; | ||
} | ||
/** | ||
* Cache manager for mock instances | ||
*/ | ||
declare class MockCache { | ||
private cache; | ||
private registry; | ||
/** | ||
* Set a value in the cache with optional descriptor | ||
*/ | ||
set(key: string | symbol, value: any, descriptor?: PropertyDescriptor): void; | ||
/** | ||
* Get a value from the cache | ||
*/ | ||
get(key: string | symbol): any | undefined; | ||
/** | ||
* Get a property descriptor from the cache | ||
*/ | ||
getDescriptor(key: string | symbol): PropertyDescriptor | undefined; | ||
/** | ||
* Check if a key exists in the cache | ||
*/ | ||
has(key: string | symbol): boolean; | ||
/** | ||
* Delete a key from the cache | ||
*/ | ||
delete(key: string | symbol): void; | ||
/** | ||
* Clear all entries from the cache | ||
*/ | ||
clear(): void; | ||
/** | ||
* Get all keys in the cache | ||
*/ | ||
keys(): IterableIterator<string | symbol>; | ||
} | ||
/** | ||
* Basic function type | ||
*/ | ||
type Fn = (...args: any[]) => any; | ||
/** | ||
* Async function type | ||
*/ | ||
type AsyncFn = (...args: any[]) => Promise<any>; | ||
/** | ||
* Class constructor type | ||
*/ | ||
type Constructor<T> = new (...args: any[]) => T; | ||
/** | ||
* Makes all properties optional recursively | ||
*/ | ||
type DeepPartial<T> = { | ||
[P in keyof T]?: T[P] extends (infer U)[] ? DeepPartial<U>[] : T[P] extends object ? DeepPartial<T[P]> : T[P]; | ||
}; | ||
/** | ||
* Recursive partial type with function handling | ||
*/ | ||
type RecursivePartial<T> = { | ||
[K in keyof T]?: T[K] extends Fn ? (...args: Parameters<T[K]>) => RecursivePartial<ReturnType<T[K]>> | ReturnType<T[K]> : T[K] extends Array<any> ? Array<RecursivePartial<T[K][number]>> : T[K] extends object ? RecursivePartial<T[K]> : T[K]; | ||
}; | ||
/** | ||
* Base configuration type for mocks | ||
*/ | ||
interface BaseConfig<T> { | ||
returns?: T; | ||
resolves?: Awaited<T>; | ||
rejects?: any; | ||
throws?: any; | ||
delay?: number; | ||
implementation?: (...args: any[]) => T; | ||
} | ||
/** | ||
* Mock call information | ||
*/ | ||
type MockCall<T extends any[]> = { | ||
args: T; | ||
returnValue: any; | ||
error?: any; | ||
timestamp: number; | ||
}; | ||
/** | ||
* Mock behavior configuration | ||
*/ | ||
type MockBehaviorConfig<T> = BaseConfig<T>; | ||
/** | ||
* Mock behavior store | ||
*/ | ||
type MockBehavior$1<T> = MockBehaviorConfig<T> & { | ||
callSpecific?: Map<number, MockBehaviorConfig<T>>; | ||
}; | ||
/** | ||
* Property descriptor type | ||
*/ | ||
type PropertyDescriptor$1<T> = { | ||
get?: () => T; | ||
set?: (value: T) => void; | ||
value?: T; | ||
configurable?: boolean; | ||
enumerable?: boolean; | ||
}; | ||
/** | ||
* Base matcher type | ||
*/ | ||
interface Matcher<T = any> { | ||
(value: T): boolean; | ||
__isMatcher: true; | ||
description: string; | ||
} | ||
/** | ||
* Mock matcher factory | ||
*/ | ||
type MatcherFactory = { | ||
any(): Matcher; | ||
string(): Matcher<string>; | ||
number(): Matcher<number>; | ||
boolean(): Matcher<boolean>; | ||
array(): Matcher<any[]>; | ||
object(): Matcher<object>; | ||
func(): Matcher<Fn>; | ||
contains(value: any): Matcher; | ||
matches(regex: RegExp): Matcher<string>; | ||
custom<T>(predicate: (value: T) => boolean, description: string): Matcher<T>; | ||
}; | ||
@@ -282,35 +16,8 @@ /** | ||
/** | ||
* Base mock function interface | ||
* Mock function type with Jest-like chaining methods | ||
*/ | ||
interface BaseMockFunction<T extends Fn = Fn> extends Function { | ||
interface MockFunction<T extends Fn = Fn> extends Function { | ||
(...args: Parameters<T>): ReturnType<T>; | ||
calls: { | ||
all(): Array<{ | ||
args: Parameters<T>; | ||
}>; | ||
count(): number; | ||
}; | ||
_isMockFunction?: boolean; | ||
_isMockFunction: boolean; | ||
_this?: any; | ||
} | ||
/** | ||
* Mock function type with Jest-like chaining methods | ||
*/ | ||
interface MockFunction<T extends Fn = Fn> extends BaseMockFunction<T> { | ||
mockReturnValue(value: ReturnType<T>): this; | ||
mockResolvedValue<U>(value: U): MockFunction<AsyncFn & { | ||
(...args: Parameters<T>): Promise<U>; | ||
}>; | ||
mockImplementation(fn: T): this; | ||
mockReset(): void; | ||
mockClear(): void; | ||
and: MockBehavior<T>; | ||
toHaveBeenCalled(): boolean; | ||
toHaveBeenCalledTimes(n: number): boolean; | ||
toHaveBeenCalledWith(...args: Parameters<T>): boolean; | ||
} | ||
/** | ||
* Jest mock type | ||
*/ | ||
interface JestMock<T extends Fn = Fn> extends BaseMockFunction<T> { | ||
mock: { | ||
@@ -322,701 +29,48 @@ calls: Parameters<T>[]; | ||
}>; | ||
instances: any[]; | ||
contexts: any[]; | ||
lastCall?: Parameters<T>; | ||
}; | ||
mockReturnValue(value: ReturnType<T>): this; | ||
mockResolvedValue<U>(value: U): this; | ||
mockImplementation(fn: T): this; | ||
mockReset(): void; | ||
mockClear(): void; | ||
} | ||
/** | ||
* Jasmine spy type | ||
*/ | ||
interface JasmineSpy<T extends Fn = Fn> extends BaseMockFunction<T> { | ||
calls: { | ||
all: () => Array<{ | ||
args: Parameters<T>; | ||
}>; | ||
count: () => number; | ||
}; | ||
mockImplementation: (fn: T) => MockFunction<T>; | ||
mockReturnValue: (value: ReturnType<T>) => MockFunction<T>; | ||
mockResolvedValue: <U>(value: U) => MockFunction<T>; | ||
mockRejectedValue: <U>(value: U) => MockFunction<T>; | ||
mockReset: () => void; | ||
mockClear: () => void; | ||
mockRestore: () => void; | ||
getMockImplementation: () => T | undefined; | ||
getMockName: () => string; | ||
and: MockBehavior<T>; | ||
toHaveBeenCalled: () => boolean; | ||
toHaveBeenCalledTimes: (n: number) => boolean; | ||
toHaveBeenCalledWith: (...args: Parameters<T>) => boolean; | ||
mockName: (name: string) => MockFunction<T>; | ||
mockReturnThis: () => MockFunction<T>; | ||
mockResolvedValueOnce: <U>(value: U) => MockFunction<T>; | ||
mockRejectedValueOnce: <U>(value: U) => MockFunction<T>; | ||
mockReturnValueOnce: (value: ReturnType<T>) => MockFunction<T>; | ||
mockImplementationOnce: (fn: T) => MockFunction<T>; | ||
} | ||
/** | ||
* Mock adapter capabilities | ||
*/ | ||
interface MockCapabilities { | ||
createSpy<T extends Fn = Fn>(): MockFunction<T>; | ||
spyOn<T extends object>(obj: T, key: keyof T): void; | ||
} | ||
/** | ||
* Base mock adapter interface | ||
*/ | ||
interface BaseAdapter extends MockCapabilities { | ||
getSpy(property: string): any; | ||
} | ||
/** | ||
* Mock adapter interface | ||
*/ | ||
interface Adapter extends BaseAdapter { | ||
replaceFn<T extends object>(obj: T, key: keyof T, fn: Fn): void; | ||
createPropertySpy<T>(): { | ||
get?: MockFunction; | ||
set?: MockFunction; | ||
}; | ||
} | ||
/** | ||
* Extended with interface | ||
*/ | ||
interface ExtendedWith<T> { | ||
with(stubs: RecursivePartial<T>): T; | ||
} | ||
/** | ||
* Mock call specific behavior | ||
*/ | ||
type MockCallBehavior<T extends Fn> = BaseConfig<ReturnType<T>> & { | ||
returns: (value: ReturnType<T>) => MockFunction<T>; | ||
resolves: (value: Awaited<ReturnType<T>>) => MockFunction<T>; | ||
rejects: (error: any) => MockFunction<T>; | ||
throws: (error: any) => MockFunction<T>; | ||
implementation: (fn: T) => MockFunction<T>; | ||
}; | ||
/** | ||
* Base mock interface | ||
*/ | ||
interface BaseMock<T> { | ||
__isMock: boolean; | ||
reset(): void; | ||
restore?(): void; | ||
with(stubs: RecursivePartial<T>): T; | ||
} | ||
/** | ||
* Deep mock type for objects | ||
*/ | ||
type DeepMock<T> = { | ||
[K in keyof T]: T[K] extends Fn ? MockFunction<T[K]> : T[K] extends object ? DeepMock<T[K]> : T[K]; | ||
} & BaseMock<T> & { | ||
defineProperty: <K extends keyof T>(prop: K, descriptor: PropertyDescriptor$1<T[K]>) => void; | ||
}; | ||
/** | ||
* Mock configuration options | ||
*/ | ||
interface MockOptions { | ||
modifyOriginal?: boolean; | ||
mockPrototype?: boolean; | ||
mockStatic?: boolean; | ||
preserveConstructor?: boolean; | ||
selective?: boolean; | ||
preserveThis?: boolean; | ||
autoSpy?: boolean; | ||
handleCircular?: boolean; | ||
} | ||
/** | ||
* Mock class options | ||
*/ | ||
interface MockClassOptions extends Pick<MockOptions, 'modifyOriginal' | 'mockPrototype' | 'mockStatic' | 'preserveConstructor'> { | ||
} | ||
/** | ||
* Mock object options | ||
*/ | ||
interface MockObjectOptions extends Pick<MockOptions, 'modifyOriginal' | 'mockPrototype'> { | ||
} | ||
/** | ||
* Mock result interface | ||
*/ | ||
interface MockResult { | ||
restore?(): void; | ||
reset(): void; | ||
} | ||
/** | ||
* Mock class result | ||
*/ | ||
interface MockClassResult<T> extends MockResult { | ||
Class: Constructor<T>; | ||
} | ||
/** | ||
* Mock object result | ||
*/ | ||
interface MockObjectResult<T> extends MockResult { | ||
object: T; | ||
} | ||
/** | ||
* Spy adapter type | ||
*/ | ||
interface SpyAdapter extends MockCapabilities { | ||
getSpy(property: string): any; | ||
spyAndCallFake<T extends object, K extends keyof T>(object: T, key: K, stub: T[K] & Function): void; | ||
spyAndCallThrough<T extends object, K extends keyof T>(object: T, key: K): void; | ||
} | ||
/** | ||
* Supported mock frameworks | ||
*/ | ||
type Framework = 'jasmine' | 'jest' | 'none'; | ||
/** | ||
* Makes all methods in T into MockFunction types | ||
*/ | ||
type MockOf<T> = { | ||
[P in keyof T]: T[P] extends Fn ? MockFunction<T[P]> : T[P] extends object ? MockOf<T[P]> : T[P]; | ||
}; | ||
/** | ||
* Makes all static members of a class into MockFunction types | ||
*/ | ||
type StaticMockOf<T> = { | ||
[P in keyof T]: T[P] extends Fn ? MockFunction<T[P]> : T[P]; | ||
}; | ||
/** | ||
* Represents a mocked class constructor | ||
*/ | ||
type ClsMock<T extends new (...args: any[]) => any> = StaticMockOf<T> & { | ||
new (...args: ConstructorParameters<T>): MockOf<InstanceType<T>>; | ||
prototype: MockOf<InstanceType<T>>; | ||
}; | ||
/** | ||
* Options for configuring class mocks | ||
*/ | ||
interface ClsMockOptions<T extends new (...args: any[]) => any> extends Pick<MockOptions, 'selective'> { | ||
implementation?: DeepPartial<InstanceType<T>>; | ||
} | ||
/** | ||
* Partial mock options | ||
*/ | ||
interface PartialOptions<T> extends Pick<MockOptions, 'selective' | 'preserveThis' | 'autoSpy' | 'handleCircular'> { | ||
} | ||
/** | ||
* Partial mock builder interface | ||
*/ | ||
interface PartialBuilder<T extends object> { | ||
with(stubs?: DeepPartial<T>, options?: PartialOptions<T>): T; | ||
spy(...methods: (keyof T)[]): this; | ||
preserve(...properties: (keyof T)[]): this; | ||
} | ||
/** | ||
* Universal mock function that provides a unified entry point for mocking objects and classes. | ||
* It automatically detects the input type and applies the appropriate mocking strategy. | ||
* | ||
* @template T -The type to mock | ||
* @param target -The target to mock (class constructor, object instance, or partial implementation) | ||
* @param options -Optional configuration for mocking behavior | ||
* @returns A mocked version of the target | ||
* @throws {TypeError} When the target type cannot be determined | ||
* @throws {Error} When circular references are detected without proper handling | ||
* | ||
* @remarks | ||
* This function serves as the main entry point for all mocking operations. | ||
* It intelligently determines the type of input and applies the most appropriate mocking strategy: | ||
* -Class mocking: Creates a mock class with spied methods | ||
* -Object mocking: Creates a proxy-based mock with tracked properties | ||
* -Partial mocking: Allows selective method/property mocking | ||
* -Interface mocking: Creates complete mock from type information | ||
* | ||
* @example | ||
* ```typescript | ||
* // Mock a class | ||
* class Service { | ||
* getData() { return 'data'; } | ||
* } | ||
* const MockService = mock(Service); | ||
* const instance = new MockService(); | ||
* | ||
* // Mock a class with options | ||
* const MockService = mock(Service, { | ||
* selective: true, | ||
* implementation: { | ||
* getData: () => 'mocked' | ||
* } | ||
* }); | ||
* | ||
* // Mock an object | ||
* interface User { | ||
* id: number; | ||
* getName(): string; | ||
* } | ||
* const mockUser = mock<User>({ | ||
* id: 1, | ||
* getName: () => 'John' | ||
* }); | ||
* | ||
* // Create a complete mock from interface | ||
* const mockUser = mock<User>(); | ||
* | ||
* // Partial mock of existing instance | ||
* const realService = new Service(); | ||
* const mockService = mock(realService, { | ||
* getData: () => 'mocked' | ||
* }); | ||
* ``` | ||
* | ||
* @see {@link mock.object} For creating complete mock objects | ||
* @see {@link mock.partial} For creating partial mocks | ||
* @see {@link mock.cls} For creating mock classes | ||
* @see {@link mock.of} For creating mocks from stubs | ||
*/ | ||
declare function mock<T extends new (...args: any[]) => any>(target: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
declare function mock<T extends object>(target: T, implementation?: DeepPartial<T>): T; | ||
declare function mock<T extends object>(partialImpl?: DeepPartial<T>): MockOf<T>; | ||
/** | ||
* Mock utilities namespace that provides a comprehensive set of mocking tools | ||
* for testing TypeScript/JavaScript applications. | ||
* | ||
* @namespace mock | ||
* @description | ||
* A comprehensive mocking framework that provides tools for: | ||
* -Function mocking with spy capabilities | ||
* -Object mocking with property tracking | ||
* -Class mocking with inheritance support | ||
* -Partial mocking for selective member overrides | ||
* -Framework compatibility (Jest/Jasmine) | ||
* | ||
* The namespace provides a unified API for all mocking operations while maintaining | ||
* type safety and providing detailed tracking and verification capabilities. | ||
*/ | ||
declare namespace mock { | ||
/** | ||
* Creates a spy function that can track calls and mock implementations. | ||
* The spy function maintains the same type signature as the original function. | ||
* | ||
* @template T -The function type to mock | ||
* @returns {MockFunction<T>} A mock function with spy capabilities | ||
* | ||
* @remarks | ||
* Spy functions provide comprehensive tracking and control over function calls: | ||
* -Call count tracking | ||
* -Arguments validation | ||
* -Return value mocking | ||
* -Implementation replacement | ||
* -Asynchronous behavior simulation | ||
* | ||
* The spy maintains the original function's type information while adding | ||
* additional methods for tracking and controlling behavior. | ||
* | ||
* @example | ||
* ```typescript | ||
* // Create a typed spy | ||
* const mockFn = mock.fn<(x: number) => string>(); | ||
* | ||
* // Configure behavior | ||
* mockFn.mockReturnValue('mocked'); | ||
* mockFn.mockImplementation(x => x.toString()); | ||
* | ||
* // Use in tests | ||
* mockFn(42); | ||
* expect(mockFn).toHaveBeenCalledWith(42); | ||
* expect(mockFn.calls.count()).toBe(1); | ||
* ``` | ||
* | ||
* @see {@link MockFunction} For the complete spy API | ||
*/ | ||
function fn<T extends Fn = Fn>(): MockFunction<T>; | ||
/** | ||
* Creates a fully mocked object where all properties and methods are spies. | ||
* The mock object maintains the same type information as the original. | ||
* | ||
* @template T -The object type to mock | ||
* @returns {T} A mock object with all members as spies | ||
* | ||
* @remarks | ||
* Object mocking provides comprehensive control over object behavior: | ||
* -Automatic spy creation for methods | ||
* -Property value tracking | ||
* -Getter/setter interception | ||
* -Nested object handling | ||
* -Circular reference protection | ||
* | ||
* The mock maintains the original object's type information while adding | ||
* spy capabilities to all methods and tracking for all properties. | ||
* | ||
* Features: | ||
* -Auto-creates spies for all methods | ||
* -Supports getters and setters | ||
* -Maintains property descriptors | ||
* -Handles nested objects | ||
* -Preserves type safety | ||
* | ||
* @example | ||
* ```typescript | ||
* interface Service { | ||
* getData(): Promise<string>; | ||
* value: number; | ||
* } | ||
* | ||
* const mockService = mock.object<Service>(); | ||
* mockService.getData.mockResolvedValue('data'); | ||
* mockService.value = 42; | ||
* | ||
* await mockService.getData(); | ||
* expect(mockService.getData).toHaveBeenCalled(); | ||
* ``` | ||
* | ||
* @see {@link mock.partial} For creating partial mocks | ||
* @see {@link mock.of} For creating mocks from stubs | ||
*/ | ||
function object<T extends object>(): T; | ||
/** | ||
* Creates a partial mock from an existing object, allowing selective mocking of members. | ||
* This is useful when you want to mock only specific parts of an object while keeping | ||
* the rest of the functionality intact. | ||
* | ||
* @template T -The object type to partially mock | ||
* @param base -The original object to create a partial mock from | ||
* @param stubs -Optional stubs to initialize the mock with | ||
* @param options -Configuration options for the partial mock | ||
* @returns {T | PartialBuilder<T>} A mock object with partial mocking or a builder for further configuration | ||
* | ||
* @remarks | ||
* Partial mocking provides fine-grained control over which members to mock: | ||
* -Selective method mocking | ||
* -Original behavior preservation | ||
* -Prototype chain maintenance | ||
* -Nested object support | ||
* -Circular reference handling | ||
* | ||
* Features: | ||
* -Selective member mocking | ||
* -Preserves original behavior | ||
* -Maintains prototype chain | ||
* -Supports nested objects | ||
* -Type-safe implementation | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* getData() { return 'real data'; } | ||
* process() { return this.getData().toUpperCase(); } | ||
* } | ||
* | ||
* // Method 1: Using with() | ||
* const mockService1 = mock.partial(new Service()).with({ | ||
* getData: () => 'mock data' | ||
* }); | ||
* | ||
* // Method 2: Direct two parameters | ||
* const mockService2 = mock.partial(new Service(), { | ||
* getData: () => 'mock data' | ||
* }); | ||
* | ||
* expect(mockService1.process()).toBe('MOCK DATA'); | ||
* expect(mockService2.process()).toBe('MOCK DATA'); | ||
* ``` | ||
* | ||
* @throws {Error} When circular references are detected without proper handling | ||
* @see {@link PartialBuilder} For the builder pattern API | ||
* @see {@link PartialOptions} For available configuration options | ||
*/ | ||
function partial<T extends object>(base: T, stubs: DeepPartial<T>, options?: PartialOptions<T>): T; | ||
function partial<T extends object>(base: T): PartialBuilder<T>; | ||
/** | ||
* Casts a partial object to its full type, useful for type coercion in mocks | ||
* @template T -The target type to cast to | ||
* @param partial -The partial object to cast | ||
* @returns The partial object cast to type T | ||
* @example | ||
* ```typescript | ||
* interface Complex { | ||
* nested: { value: number }; | ||
* } | ||
* const partial = { nested: { value: 42 } }; | ||
* const typed = mock.cast<Complex>(partial); | ||
* ``` | ||
*/ | ||
function cast<T extends object>(partial: DeepPartial<T>): T; | ||
/** | ||
* Creates a mock from stubs, supporting both object and array types. | ||
* Preserves provided values while automatically mocking undefined members. | ||
* | ||
* @template T -The type to create a mock from | ||
* @param stubs -Optional stubs to initialize the mock with | ||
* @returns {MockOf<T>} A mock object or array with all methods as MockFunction | ||
* | ||
* @remarks | ||
* Mock creation from stubs provides flexible initialization: | ||
* -Value preservation | ||
* -Automatic method mocking | ||
* -Array support | ||
* -Nested object handling | ||
* -Type inference | ||
* | ||
* Features: | ||
* -Preserves provided property values | ||
* -Preserves and monitors provided method implementations | ||
* -Automatically mocks undefined methods | ||
* -Handles nested objects and arrays | ||
* -Maintains type safety | ||
* | ||
* @example | ||
* ```typescript | ||
* interface User { | ||
* id: number; | ||
* name: string; | ||
* getData(): string; | ||
* } | ||
* | ||
* // With initial values | ||
* const mockUser = mock.of<User>({ | ||
* id: 1, | ||
* name: 'John' | ||
* }); | ||
* // id and name are preserved | ||
* // getData is automatically mocked | ||
* mockUser.getData.mockReturnValue('data'); | ||
* | ||
* // With method implementation | ||
* const mockWithMethod = mock.of<User>({ | ||
* id: 1, | ||
* getData: () => 'data' | ||
* }); | ||
* // getData implementation is preserved but monitored | ||
* mockWithMethod.getData.mockReturnValue('new data'); | ||
* ``` | ||
* | ||
* @throws {Error} When circular references are detected | ||
* @see {@link MockOf} For the resulting mock type | ||
*/ | ||
function of<T extends Array<any>>(stubs?: Array<DeepPartial<T[number]>>): MockOf<T>; | ||
function of<T extends ReadonlyArray<any>>(stubs?: ReadonlyArray<DeepPartial<T[number]>>): MockOf<T>; | ||
function of<T extends object>(stubs?: DeepPartial<T>): MockOf<T>; | ||
/** | ||
* Replaces a method with mock implementation while preserving original properties | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to replace | ||
* @param obj -The object containing the method | ||
* @param key -The key of the method to replace | ||
* @param impl -The mock implementation | ||
* @example | ||
* ```typescript | ||
* const obj = { method: () => 'original' }; | ||
* mock.replace(obj, 'method', () => 'mocked'); | ||
* ``` | ||
*/ | ||
function replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn): void; | ||
/** | ||
* Restores replaced methods to their original implementations | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to restore | ||
* @param obj -The object containing the method | ||
* @param key -Optional key of the specific method to restore | ||
* @returns {boolean} Whether any methods were restored | ||
* @example | ||
* ```typescript | ||
* // Restore specific method | ||
* mock.restore(service, 'method'); | ||
* | ||
* // Restore all replaced methods | ||
* mock.restore(service); | ||
* ``` | ||
*/ | ||
function restore<T extends object>(obj: T): boolean; | ||
function restore<T extends object, K extends keyof T>(obj: T, key: K): boolean; | ||
/** | ||
* Gets a list of currently replaced methods on an object | ||
* @template T -The object type to check | ||
* @param obj -The object to check | ||
* @returns {string[]} Array of replaced method names | ||
*/ | ||
function getReplacedMethods<T extends object>(obj: T): string[]; | ||
/** | ||
* Verifies if a method has been replaced | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to check | ||
* @param obj -The object containing the method | ||
* @param key -The key of the method to check | ||
* @returns {boolean} Whether the method is currently replaced | ||
*/ | ||
function isReplaced<T extends object, K extends keyof T>(obj: T, key: K): boolean; | ||
/** | ||
* Verifies if a restored method matches its original implementation | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to verify | ||
* @param obj -The object containing the method | ||
* @param key -The key of the method to verify | ||
* @returns {boolean} Whether the current implementation matches the original | ||
*/ | ||
function verifyRestored<T extends object, K extends keyof T>(obj: T, key: K): boolean; | ||
/** | ||
* Creates a mock class that maintains the original class's type information and behavior. | ||
* Supports both full monitoring and selective method mocking. | ||
* | ||
* @template T -The class type to mock | ||
* @param originalClass -The original class to create a mock from | ||
* @param options -Configuration options for class mocking | ||
* @returns {ClsMock<T>} A mock class with the same interface as the original | ||
* | ||
* @remarks | ||
* Class mocking provides comprehensive control over class behavior: | ||
* -Constructor interception | ||
* -Method spying | ||
* -Property tracking | ||
* -Static member handling | ||
* -Inheritance support | ||
* | ||
* Features: | ||
* -Maintains original class structure | ||
* -Supports constructor arguments | ||
* -Handles static members | ||
* -Preserves inheritance chain | ||
* -Allows selective mocking | ||
* | ||
* @example | ||
* ```typescript | ||
* // Monitor all methods | ||
* const MockService = mock.cls(DataService); | ||
* | ||
* // Selective monitoring with custom implementation | ||
* const MockService = mock.cls(DataService, { | ||
* selective: true, | ||
* implementation: { | ||
* getData: async () => 'mocked' | ||
* } | ||
* }); | ||
* ``` | ||
* | ||
* @throws {Error} When constructor initialization fails | ||
* @see {@link ClsMockOptions} For available configuration options | ||
* @see {@link ClsMock} For the resulting mock class type | ||
*/ | ||
function cls<T extends new (...args: any[]) => any>(originalClass: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
/** | ||
* Gets current configuration | ||
*/ | ||
function getConfig(): Readonly<Config>; | ||
/** | ||
* Updates configuration | ||
*/ | ||
function setConfig(options: Partial<Config>): void; | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
function resetConfig(): void; | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
function withConfig<T>(options: Partial<Config>, fn: () => T): T; | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
function getConfigStats(): ConfigStats; | ||
} | ||
/** | ||
* A universal spy implementation that provides advanced mocking capabilities. | ||
* This class serves as the foundation for creating function spies with rich features | ||
* such as call tracking, return value manipulation, and behavior verification. | ||
* | ||
* @template T - The type of function being spied on | ||
* | ||
* @example | ||
* ```typescript | ||
* // Create a spy for a simple function | ||
* const spy = new UniversalSpy<() => string>(); | ||
* const spyFn = spy.getSpy(); | ||
* | ||
* // Configure spy behavior | ||
* spyFn.mockReturnValue('mocked'); | ||
* | ||
* // Use the spy | ||
* const result = spyFn(); | ||
* expect(result).toBe('mocked'); | ||
* expect(spyFn).toHaveBeenCalled(); | ||
* ``` | ||
*/ | ||
declare class UniversalSpy<T extends Fn = Fn> { | ||
private tracker; | ||
private implementation?; | ||
private returnValue?; | ||
private error?; | ||
private spyFunction; | ||
constructor(implementation?: T); | ||
getSpy(): MockFunction<T>; | ||
private updateState; | ||
} | ||
/** | ||
* Creates a new spy function with optional implementation. | ||
* The spy function tracks all calls and provides methods for verifying behavior. | ||
* | ||
* @template T - The type of function to spy on | ||
* @param implementation - Optional implementation for the spy function | ||
* @returns A mock function that tracks calls and provides verification methods | ||
* | ||
* @example | ||
* ```typescript | ||
* // Create a spy with no implementation | ||
* const spy = createSpy<(name: string) => string>(); | ||
* | ||
* // Create a spy with implementation | ||
* const spy = createSpy((name: string) => `Hello ${name}`); | ||
* | ||
* // Use the spy | ||
* spy('John'); | ||
* expect(spy).toHaveBeenCalledWith('John'); | ||
* ``` | ||
* @param implementation - Optional implementation for the spy | ||
*/ | ||
declare function createSpy<T extends Fn>(implementation?: T): MockFunction<T>; | ||
/** | ||
* Replaces a method on an object with a spy while preserving the original implementation. | ||
* This is useful for monitoring method calls while maintaining the original behavior. | ||
* | ||
* @template T - The type of object containing the method | ||
* @param obj - The object containing the method to spy on | ||
* @param key - The key of the method to spy on | ||
* @param fn - The function to use as implementation | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* getData() { return 'data'; } | ||
* } | ||
* | ||
* const service = new Service(); | ||
* replaceFn(service, 'getData', () => 'mock data'); | ||
* | ||
* service.getData(); // Returns 'mock data' | ||
* expect(service.getData).toHaveBeenCalled(); | ||
* ``` | ||
* Creates a spy for a property with getter and/or setter. | ||
* @template T - The type of the property | ||
*/ | ||
declare function replaceFn<T extends object>(obj: T, key: keyof T, fn: Fn): void; | ||
/** | ||
* Creates a spy for an existing method on an object. | ||
* The spy will track all calls while maintaining the original implementation. | ||
* | ||
* @template T - The type of object containing the method | ||
* @param obj - The object containing the method to spy on | ||
* @param key - The key of the method to spy on | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* getData() { return 'data'; } | ||
* } | ||
* | ||
* const service = new Service(); | ||
* spyOn(service, 'getData'); | ||
* | ||
* service.getData(); // Returns 'data' | ||
* expect(service.getData).toHaveBeenCalled(); | ||
* ``` | ||
*/ | ||
declare function spyOn<T extends object>(obj: T, key: keyof T): void; | ||
/** | ||
* Creates spies for getter/setter properties. | ||
* This is useful for monitoring property access and modifications. | ||
* | ||
* @template T - The type of the property value | ||
* @returns An object containing spy functions for get and set operations | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* private _value: string = ''; | ||
* get value() { return this._value; } | ||
* set value(v: string) { this._value = v; } | ||
* } | ||
* | ||
* const service = new Service(); | ||
* const spy = createPropertySpy<string>(); | ||
* | ||
* Object.defineProperty(service, 'value', { | ||
* get: spy.get, | ||
* set: spy.set | ||
* }); | ||
* | ||
* service.value = 'test'; | ||
* expect(spy.set).toHaveBeenCalledWith('test'); | ||
* ``` | ||
*/ | ||
declare function createPropertySpy<T>(): { | ||
get?: MockFunction; | ||
set?: MockFunction; | ||
get?: MockFunction<() => T>; | ||
set?: MockFunction<(value: T) => void>; | ||
}; | ||
export { type Adapter, type AsyncFn, type BaseConfig, type CacheEntry, type ClsMock, type ClsMockOptions, type Config, type ConfigStats, type Constructor, type DeepMock, type DeepPartial, type ExtendedWith, type Fn, type Framework, type JasmineSpy, type JestMock, type Matcher, type MatcherFactory, type MockBehavior$1 as MockBehavior, type MockBehaviorConfig, MockCache, type MockCall, type MockCallBehavior, type MockClassOptions, type MockClassResult, MockError, MockErrorCode, type MockFunction, type MockObjectOptions, type MockObjectResult, type MockOf, type PartialBuilder, type PartialOptions, type PropertyDescriptor$1 as PropertyDescriptor, type RecursivePartial, type SpyAdapter, type StaticMockOf, UniversalSpy, configManager, createPropertySpy, createSpy, mock, mockConfig, replaceFn, spyOn }; | ||
export { type MockFunction, createPropertySpy, createSpy }; |
'use strict'; | ||
// src/config.ts | ||
var defaultConfig = { | ||
debug: process.env.NODE_ENV === "development", | ||
trackCalls: true, | ||
allowUndefined: true, | ||
strict: false, | ||
timeout: 5e3, | ||
preservePrototype: true | ||
}; | ||
function isValidConfigValue(key, value) { | ||
switch (key) { | ||
case "debug": | ||
case "trackCalls": | ||
case "allowUndefined": | ||
case "strict": | ||
case "preservePrototype": | ||
return typeof value === "boolean"; | ||
case "timeout": | ||
return typeof value === "number" && value > 0; | ||
default: | ||
return false; | ||
} | ||
} | ||
var GlobalConfig = class _GlobalConfig { | ||
constructor() { | ||
this.cachedConfig = null; | ||
this.lastUpdate = 0; | ||
this.updateCount = 0; | ||
this.cacheHits = 0; | ||
this.cacheTimeout = 1e3; | ||
this.config = { ...defaultConfig }; | ||
} | ||
/** | ||
* Gets the singleton instance | ||
*/ | ||
static getInstance() { | ||
if (!_GlobalConfig.instance) { | ||
_GlobalConfig.instance = new _GlobalConfig(); | ||
} | ||
return _GlobalConfig.instance; | ||
} | ||
/** | ||
* Gets the current configuration | ||
*/ | ||
getConfig() { | ||
if (this.cachedConfig && Date.now() - this.lastUpdate < this.cacheTimeout) { | ||
this.cacheHits++; | ||
return this.cachedConfig; | ||
} | ||
this.cachedConfig = Object.freeze({ ...this.config }); | ||
return this.cachedConfig; | ||
} | ||
/** | ||
* Updates the configuration | ||
*/ | ||
updateConfig(options) { | ||
const newConfig = { ...this.config }; | ||
let hasChanges = false; | ||
Object.keys(options).forEach((key) => { | ||
if (key in defaultConfig && isValidConfigValue(key, options[key])) { | ||
if (this.config[key] !== options[key]) { | ||
newConfig[key] = options[key]; | ||
hasChanges = true; | ||
} | ||
} | ||
}); | ||
if (hasChanges) { | ||
this.config = newConfig; | ||
this.invalidateCache(); | ||
this.updateCount++; | ||
} | ||
} | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
resetConfig() { | ||
const newConfig = { | ||
...defaultConfig, | ||
debug: process.env.NODE_ENV === "development" | ||
}; | ||
if (this.hasConfigChanged(newConfig)) { | ||
this.config = newConfig; | ||
this.invalidateCache(); | ||
this.updateCount++; | ||
} | ||
} | ||
/** | ||
* Gets update statistics | ||
*/ | ||
getStats() { | ||
return { | ||
updateCount: this.updateCount, | ||
lastUpdate: this.lastUpdate, | ||
cacheHits: this.cacheHits | ||
}; | ||
} | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
withConfig(options, fn) { | ||
const previous = { ...this.getConfig() }; | ||
try { | ||
this.updateConfig(options); | ||
return fn(); | ||
} finally { | ||
this.updateConfig(previous); | ||
} | ||
} | ||
/** | ||
* Checks if configuration has changed | ||
*/ | ||
hasConfigChanged(newConfig) { | ||
return Object.entries(newConfig).some(([key, value]) => { | ||
const configKey = key; | ||
return this.config[configKey] !== value; | ||
}); | ||
} | ||
/** | ||
* Invalidates the configuration cache | ||
*/ | ||
invalidateCache() { | ||
this.cachedConfig = null; | ||
this.lastUpdate = Date.now(); | ||
} | ||
}; | ||
var configManager = { | ||
/** | ||
* Gets current configuration | ||
*/ | ||
get() { | ||
return GlobalConfig.getInstance().getConfig(); | ||
}, | ||
/** | ||
* Updates configuration | ||
*/ | ||
set(options) { | ||
GlobalConfig.getInstance().updateConfig(options); | ||
}, | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
reset() { | ||
GlobalConfig.getInstance().resetConfig(); | ||
}, | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
with(options, fn) { | ||
return GlobalConfig.getInstance().withConfig(options, fn); | ||
}, | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
getStats() { | ||
return GlobalConfig.getInstance().getStats(); | ||
} | ||
}; | ||
var mockConfig = configManager; | ||
// src/errors.ts | ||
var MockErrorCode = /* @__PURE__ */ ((MockErrorCode2) => { | ||
MockErrorCode2["CIRCULAR_REFERENCE"] = "CIRCULAR_REF"; | ||
MockErrorCode2["INVALID_TARGET"] = "INVALID_TARGET"; | ||
MockErrorCode2["INITIALIZATION_FAILED"] = "INIT_FAILED"; | ||
MockErrorCode2["TYPE_MISMATCH"] = "TYPE_MISMATCH"; | ||
MockErrorCode2["INVALID_IMPLEMENTATION"] = "INVALID_IMPL"; | ||
MockErrorCode2["INVALID_CONFIGURATION"] = "INVALID_CONFIG"; | ||
MockErrorCode2["VERIFICATION_ERROR"] = "VERIFY_ERROR"; | ||
MockErrorCode2["EXPECTATION_FAILED"] = "EXPECT_FAILED"; | ||
MockErrorCode2["PERFORMANCE_CONSTRAINT_VIOLATED"] = "PERF_VIOLATED"; | ||
return MockErrorCode2; | ||
})(MockErrorCode || {}); | ||
var MockError = class _MockError extends Error { | ||
@@ -447,9 +276,16 @@ constructor(code, message, details) { | ||
} else if (this.implementation) { | ||
result = this.implementation.apply(spy._this || this, args); | ||
result = this.implementation.apply(spy._this, args); | ||
} else { | ||
result = void 0; | ||
} | ||
spy.mock.calls.push(args); | ||
spy.mock.results.push({ type: "return", value: result }); | ||
if (spy._this) { | ||
spy.mock.instances.push(spy._this); | ||
spy.mock.contexts.push(spy._this); | ||
} | ||
finishCall(void 0, result); | ||
return result; | ||
} catch (error) { | ||
spy.mock.results.push({ type: "throw", value: error }); | ||
finishCall(error instanceof Error ? error : new Error(String(error))); | ||
@@ -471,19 +307,26 @@ throw error; | ||
}); | ||
const boundSpy = new Proxy(spy, { | ||
apply: (target, thisArg, argumentsList) => { | ||
spy._this = thisArg; | ||
const result = target.apply(thisArg, argumentsList); | ||
spy._this = null; | ||
return result; | ||
} | ||
spy.calls = { | ||
all: () => this.tracker.getCallsFor("spy").map((call) => ({ args: call.args })), | ||
count: () => this.tracker.getCallsFor("spy").length | ||
}; | ||
Object.defineProperty(spy, "mock", { | ||
value: { | ||
calls: [], | ||
results: [], | ||
instances: [], | ||
contexts: [] | ||
}, | ||
writable: true, | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
boundSpy.mockReturnValue = (value) => { | ||
spy.mockReturnValue = (value) => { | ||
this.updateState({ returnValue: value }); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockResolvedValue = (value) => { | ||
spy.mockResolvedValue = (value) => { | ||
this.updateState({ returnValue: Promise.resolve(value) }); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockImplementation = (fn) => { | ||
spy.mockImplementation = (fn) => { | ||
if (fn !== null && fn !== void 0 && typeof fn !== "function") { | ||
@@ -493,36 +336,52 @@ throw new Error("Mock implementation must be a function"); | ||
this.updateState({ implementation: fn }); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockReset = () => { | ||
spy.mockReset = () => { | ||
this.tracker.reset(); | ||
this.updateState({}); | ||
}; | ||
boundSpy.mockClear = () => { | ||
spy.mockClear = () => { | ||
this.tracker.reset(); | ||
}; | ||
boundSpy.calls = { | ||
all: () => this.tracker.getCallsFor("spy").map((call) => ({ args: call.args })), | ||
count: () => this.tracker.getCallsFor("spy").length | ||
}; | ||
boundSpy.and = { | ||
returnValue: (value) => { | ||
boundSpy.mockReturnValue(value); | ||
return boundSpy; | ||
}, | ||
callFake: (fn) => { | ||
boundSpy.mockImplementation(fn); | ||
return boundSpy; | ||
}, | ||
const jasmineApi = { | ||
returnValue: spy.mockReturnValue, | ||
callFake: spy.mockImplementation, | ||
throwError: (error) => { | ||
this.updateState({ error }); | ||
return boundSpy; | ||
return spy; | ||
}, | ||
stub: () => { | ||
boundSpy.mockImplementation(void 0); | ||
return boundSpy; | ||
spy.mockReset(); | ||
return spy; | ||
} | ||
}; | ||
boundSpy.toHaveBeenCalled = () => this.tracker.getCallsFor("spy").length > 0; | ||
boundSpy.toHaveBeenCalledTimes = (n) => this.tracker.getCallsFor("spy").length === n; | ||
boundSpy.toHaveBeenCalledWith = (...args) => this.tracker.getCallsFor("spy").some((call) => call.args.length === args.length && call.args.every((arg, i) => arg === args[i])); | ||
Object.defineProperty(spy, "and", { | ||
value: jasmineApi, | ||
writable: false, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
spy.toHaveBeenCalled = () => this.tracker.getCallsFor("spy").length > 0; | ||
spy.toHaveBeenCalledTimes = (n) => this.tracker.getCallsFor("spy").length === n; | ||
spy.toHaveBeenCalledWith = (...args) => this.tracker.getCallsFor("spy").some((call) => call.args.length === args.length && call.args.every((arg, i) => arg === args[i])); | ||
const boundSpy = new Proxy(spy, { | ||
apply: (target, thisArg, argumentsList) => { | ||
spy._this = thisArg; | ||
const result = target.apply(thisArg, argumentsList); | ||
spy._this = null; | ||
return result; | ||
}, | ||
get: (target, prop) => { | ||
const value = target[prop]; | ||
if (typeof value === "function") { | ||
return function(...args) { | ||
spy._this = this; | ||
const result = value.apply(this, args); | ||
spy._this = null; | ||
return result; | ||
}; | ||
} | ||
return value; | ||
} | ||
}); | ||
this.spyFunction = boundSpy; | ||
@@ -559,23 +418,2 @@ } | ||
} | ||
function replaceFn(obj, key, fn) { | ||
const spy = createSpy(fn); | ||
spy.mockImplementation(fn); | ||
Object.defineProperty(obj, key, { | ||
value: spy, | ||
writable: true, | ||
configurable: true | ||
}); | ||
} | ||
function spyOn(obj, key) { | ||
const value = obj[key]; | ||
if (typeof value === "function") { | ||
const spy = createSpy(value); | ||
spy.mockImplementation(value); | ||
Object.defineProperty(obj, key, { | ||
value: spy, | ||
writable: true, | ||
configurable: true | ||
}); | ||
} | ||
} | ||
function createPropertySpy() { | ||
@@ -588,588 +426,5 @@ return { | ||
// src/mock.ts | ||
function mock(targetOrImpl, optionsOrImpl) { | ||
const mockOptions = optionsOrImpl && "trackCalls" in optionsOrImpl ? optionsOrImpl : {}; | ||
const currentConfig = { | ||
...mock.getConfig(), | ||
...mockOptions | ||
}; | ||
if (currentConfig.debug) { | ||
console.debug("Mocking:", targetOrImpl); | ||
} | ||
if (!targetOrImpl) { | ||
return mock.object(); | ||
} | ||
if (typeof targetOrImpl === "function" && targetOrImpl.prototype) { | ||
return mock.cls(targetOrImpl, optionsOrImpl); | ||
} | ||
if (targetOrImpl && typeof targetOrImpl === "object" && optionsOrImpl) { | ||
return mock.partial(targetOrImpl).with(optionsOrImpl); | ||
} | ||
return mock.of(targetOrImpl); | ||
} | ||
((mock2) => { | ||
function fn() { | ||
return createSpy(); | ||
} | ||
mock2.fn = fn; | ||
function object() { | ||
const target = {}; | ||
const cache = /* @__PURE__ */ new Map(); | ||
const descriptors = /* @__PURE__ */ new Map(); | ||
const refs = /* @__PURE__ */ new WeakMap(); | ||
const config = mock2.getConfig(); | ||
const handler = { | ||
get: (_, prop) => { | ||
if (prop === "then") { | ||
return void 0; | ||
} | ||
const descriptor = descriptors.get(prop); | ||
if (descriptor) { | ||
return descriptor.get?.(); | ||
} | ||
if (!cache.has(prop)) { | ||
if (!config.allowUndefined && !prop.toString().startsWith("__")) { | ||
if (config.strict) { | ||
throw new Error(`Accessing undefined property: ${String(prop)}`); | ||
} | ||
if (config.debug) { | ||
console.warn(`Accessing undefined property: ${String(prop)}`); | ||
} | ||
} | ||
const spy = createSpy(); | ||
if (config.trackCalls) { | ||
spy.mockImplementation(function(...args) { | ||
if (config.debug) { | ||
console.debug(`Called ${String(prop)} with:`, args); | ||
} | ||
return void 0; | ||
}); | ||
} | ||
cache.set(prop, spy); | ||
return spy; | ||
} | ||
return cache.get(prop); | ||
}, | ||
set: (_, prop, value) => { | ||
if (value && typeof value === "object") { | ||
if (refs.has(value)) { | ||
cache.set(prop, value); | ||
return true; | ||
} | ||
refs.set(value, true); | ||
} | ||
cache.delete(prop); | ||
cache.set(prop, value); | ||
return true; | ||
}, | ||
defineProperty: (_, prop, descriptor) => { | ||
descriptors.set(prop, { | ||
...descriptor, | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
return true; | ||
}, | ||
getOwnPropertyDescriptor: (_, prop) => { | ||
const descriptor = descriptors.get(prop); | ||
if (descriptor) { | ||
return { | ||
...descriptor, | ||
configurable: true, | ||
enumerable: true | ||
}; | ||
} | ||
if (cache.has(prop)) { | ||
return { | ||
value: cache.get(prop), | ||
writable: true, | ||
configurable: true, | ||
enumerable: true | ||
}; | ||
} | ||
return void 0; | ||
}, | ||
has: (_, prop) => { | ||
return descriptors.has(prop) || cache.has(prop); | ||
}, | ||
ownKeys: () => { | ||
return [...descriptors.keys(), ...cache.keys()]; | ||
} | ||
}; | ||
return new Proxy(target, handler); | ||
} | ||
mock2.object = object; | ||
function partial(base, stubs, options) { | ||
const config = mock2.getConfig(); | ||
const defaultOptions = { | ||
selective: false, | ||
preserveThis: config.preservePrototype, | ||
autoSpy: config.trackCalls, | ||
handleCircular: false | ||
}; | ||
const spyMethods = /* @__PURE__ */ new Set(); | ||
const preserveProps = /* @__PURE__ */ new Set(); | ||
function createMock(withStubs = {}, withOptions = {}) { | ||
const finalOptions = { ...defaultOptions, ...withOptions }; | ||
if (config.debug) { | ||
console.debug("Creating partial mock for:", base.constructor.name); | ||
} | ||
const result = Object.create(finalOptions.preserveThis ? Object.getPrototypeOf(base) : null); | ||
Object.getOwnPropertyNames(base).forEach((key) => { | ||
const typedKey = key; | ||
if (!Object.prototype.hasOwnProperty.call(withStubs, key) && !spyMethods.has(typedKey)) { | ||
if (preserveProps.has(typedKey)) { | ||
result[typedKey] = base[typedKey]; | ||
} else { | ||
const descriptor = Object.getOwnPropertyDescriptor(base, key); | ||
if (typeof descriptor.value === "function" && !finalOptions.selective) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(descriptor.value.bind(result)); | ||
result[typedKey] = spy; | ||
} else { | ||
Object.defineProperty(result, key, descriptor); | ||
} | ||
} | ||
} | ||
}); | ||
try { | ||
if (finalOptions.handleCircular) { | ||
const merged = { ...base, ...withStubs }; | ||
Object.assign(result, merged); | ||
for (const key in merged) { | ||
const value = merged[key]; | ||
if (value === base) { | ||
result[key] = result; | ||
} | ||
} | ||
} else { | ||
Object.keys(withStubs).forEach((key) => { | ||
const typedKey = key; | ||
if (preserveProps.has(typedKey)) { | ||
result[typedKey] = base[typedKey]; | ||
return; | ||
} | ||
const value = withStubs[key]; | ||
if (typeof value === "function") { | ||
const spy = createSpy(); | ||
spy.mockImplementation(finalOptions.preserveThis ? value.bind(result) : value); | ||
result[typedKey] = spy; | ||
} else if (value && typeof value === "object" && !Array.isArray(value)) { | ||
if (value === base && !finalOptions.handleCircular) { | ||
throw new Error(`Property ${String(typedKey)} has a circular reference`); | ||
} | ||
try { | ||
result[typedKey] = { ...base[typedKey], ...value }; | ||
} catch (e) { | ||
if (e instanceof RangeError) { | ||
throw new Error( | ||
`Property ${String(typedKey)} has a circular reference. | ||
Consider using handleCircular: true option.` | ||
); | ||
} | ||
throw e; | ||
} | ||
} else { | ||
result[typedKey] = value; | ||
} | ||
}); | ||
} | ||
} catch (e) { | ||
if (e instanceof RangeError && finalOptions.handleCircular) { | ||
return mock2.cast({ ...base, ...withStubs }); | ||
} | ||
throw e; | ||
} | ||
spyMethods.forEach((method) => { | ||
if (typeof result[method] === "function" && !preserveProps.has(method)) { | ||
const original = result[method]; | ||
const spy = createSpy(); | ||
spy.mockImplementation(finalOptions.preserveThis ? original.bind(result) : original); | ||
result[method] = spy; | ||
} | ||
}); | ||
return result; | ||
} | ||
const builder = { | ||
with: (withStubs, withOptions) => createMock(withStubs, withOptions), | ||
spy: (...methods) => { | ||
methods.forEach((method) => spyMethods.add(method)); | ||
return builder; | ||
}, | ||
preserve: (...properties) => { | ||
properties.forEach((prop) => preserveProps.add(prop)); | ||
return builder; | ||
} | ||
}; | ||
return stubs ? createMock(stubs, options) : builder; | ||
} | ||
mock2.partial = partial; | ||
function cast(partial2) { | ||
const proxies = /* @__PURE__ */ new WeakMap(); | ||
const handler = { | ||
get: (target, prop) => { | ||
if (prop === "then") { | ||
return void 0; | ||
} | ||
if (prop in target) { | ||
const value = target[prop]; | ||
if (value && typeof value === "object") { | ||
if (proxies.has(value)) { | ||
return proxies.get(value); | ||
} | ||
const proxy2 = new Proxy(value, handler); | ||
proxies.set(value, proxy2); | ||
return proxy2; | ||
} | ||
return value; | ||
} | ||
return void 0; | ||
} | ||
}; | ||
const proxy = new Proxy(partial2, handler); | ||
if (partial2 && typeof partial2 === "object") { | ||
proxies.set(partial2, proxy); | ||
} | ||
return proxy; | ||
} | ||
mock2.cast = cast; | ||
function of(stubs = {}) { | ||
if (Array.isArray(stubs)) { | ||
return stubs.map((item) => of(item)); | ||
} | ||
const base = object(); | ||
const result = Object.create(Object.getPrototypeOf(base)); | ||
const cache = /* @__PURE__ */ new Map(); | ||
Object.entries(stubs).forEach(([key, value]) => { | ||
result[key] = value; | ||
}); | ||
return new Proxy(result, { | ||
get(target, prop) { | ||
if (prop === "then") return void 0; | ||
if (cache.has(prop)) { | ||
return cache.get(prop); | ||
} | ||
const value = target[prop]; | ||
if (typeof value === "function") { | ||
const spy2 = createSpy(); | ||
spy2.mockImplementation(value); | ||
cache.set(prop, spy2); | ||
return spy2; | ||
} | ||
if (value !== void 0) { | ||
cache.set(prop, value); | ||
return value; | ||
} | ||
const spy = createSpy(); | ||
cache.set(prop, spy); | ||
return spy; | ||
} | ||
}); | ||
} | ||
mock2.of = of; | ||
const ORIGINAL_PREFIX = "__original_"; | ||
const EXCLUDED_PROPS = ["name", "length", "prototype", "_isMockFunction"]; | ||
function replace(obj, key, impl) { | ||
if (!obj || typeof key !== "string") return; | ||
const methodKey = String(key); | ||
const originalKey = `${ORIGINAL_PREFIX}${methodKey}`; | ||
const original = obj[key]; | ||
if (!(originalKey in obj)) { | ||
obj[originalKey] = original; | ||
} | ||
const spy = createSpy(); | ||
spy.mockImplementation(impl); | ||
if (typeof original === "function") { | ||
Object.getOwnPropertyNames(original).forEach((prop) => { | ||
if (!EXCLUDED_PROPS.includes(prop)) { | ||
spy[prop] = original[prop]; | ||
} | ||
}); | ||
} | ||
obj[key] = spy; | ||
} | ||
mock2.replace = replace; | ||
function restore(obj, key) { | ||
if (!obj) return false; | ||
const restoreMethod = (methodKey) => { | ||
const originalKey = `${ORIGINAL_PREFIX}${methodKey}`; | ||
if (!(originalKey in obj)) return false; | ||
const original = obj[originalKey]; | ||
const current = obj[methodKey]; | ||
if (current && typeof current === "function") { | ||
Object.getOwnPropertyNames(current).forEach((prop) => { | ||
if (!EXCLUDED_PROPS.includes(prop)) { | ||
try { | ||
original[prop] = current[prop]; | ||
} catch (e) { | ||
} | ||
} | ||
}); | ||
} | ||
try { | ||
obj[methodKey] = original; | ||
delete obj[originalKey]; | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
}; | ||
if (key !== void 0) { | ||
return restoreMethod(String(key)); | ||
} | ||
let anyRestored = false; | ||
const keys = Object.getOwnPropertyNames(obj); | ||
for (const k of keys) { | ||
if (k.startsWith(ORIGINAL_PREFIX)) { | ||
const methodKey = k.slice(ORIGINAL_PREFIX.length); | ||
if (restoreMethod(methodKey)) { | ||
anyRestored = true; | ||
} | ||
} | ||
} | ||
return anyRestored; | ||
} | ||
mock2.restore = restore; | ||
function getReplacedMethods(obj) { | ||
if (!obj) return []; | ||
return Object.getOwnPropertyNames(obj).filter((k) => k.startsWith(ORIGINAL_PREFIX)).map((k) => k.slice(ORIGINAL_PREFIX.length)).filter((k) => k && typeof obj[k] !== "undefined"); | ||
} | ||
mock2.getReplacedMethods = getReplacedMethods; | ||
function isReplaced(obj, key) { | ||
if (!obj) return false; | ||
const originalKey = `${ORIGINAL_PREFIX}${String(key)}`; | ||
const original = obj[originalKey]; | ||
const current = obj[key]; | ||
return originalKey in obj && current !== original; | ||
} | ||
mock2.isReplaced = isReplaced; | ||
function verifyRestored(obj, key) { | ||
if (!obj) return false; | ||
const originalKey = `${ORIGINAL_PREFIX}${String(key)}`; | ||
const original = obj[originalKey]; | ||
if (!original) return true; | ||
const current = obj[key]; | ||
if (current === original) return true; | ||
if (typeof current === "function" && typeof original === "function") { | ||
try { | ||
const currentStr = current.toString().replace(/\s+/g, ""); | ||
const originalStr = original.toString().replace(/\s+/g, ""); | ||
return currentStr === originalStr; | ||
} catch { | ||
return current === original; | ||
} | ||
} | ||
return false; | ||
} | ||
mock2.verifyRestored = verifyRestored; | ||
function handleStaticMethodInheritance(mockClass, originalClass) { | ||
let currentProto = Object.getPrototypeOf(originalClass); | ||
while (currentProto && currentProto !== Function.prototype) { | ||
Object.getOwnPropertyNames(currentProto).filter((prop) => typeof currentProto[prop] === "function").forEach((methodName) => { | ||
if (!mockClass[methodName]) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(currentProto[methodName].bind(mockClass)); | ||
mockClass[methodName] = spy; | ||
} | ||
}); | ||
currentProto = Object.getPrototypeOf(currentProto); | ||
} | ||
} | ||
function cls(originalClass, options = {}) { | ||
const config = mock2.getConfig(); | ||
const { selective = false, implementation = {} } = options; | ||
if (config.debug) { | ||
console.debug("Creating class mock for:", originalClass.name); | ||
} | ||
const createMethodSpy = (method, context) => { | ||
const spy = createSpy(); | ||
if (config.trackCalls) { | ||
spy.mockImplementation((...args) => { | ||
if (config.debug) { | ||
console.debug(`Called ${method.name} with:`, args); | ||
} | ||
return method.apply(context, args); | ||
}); | ||
} else { | ||
spy.mockImplementation((...args) => method.apply(context, args)); | ||
} | ||
return spy; | ||
}; | ||
const handleDescriptor = (name, descriptor, context) => { | ||
if (descriptor.get || descriptor.set) { | ||
const spies = createPropertySpy(); | ||
const implDescriptor = typeof name === "string" ? Object.getOwnPropertyDescriptor(implementation, name) : void 0; | ||
return { | ||
configurable: true, | ||
enumerable: true, | ||
get: descriptor.get && (implDescriptor?.get ? spies.get?.mockImplementation(implDescriptor.get) : !selective ? spies.get?.mockImplementation(descriptor.get.bind(context)) : descriptor.get.bind(context)), | ||
set: descriptor.set && (implDescriptor?.set ? spies.set?.mockImplementation(implDescriptor.set) : !selective ? spies.set?.mockImplementation(descriptor.set.bind(context)) : descriptor.set.bind(context)) | ||
}; | ||
} | ||
if (typeof descriptor.value === "function") { | ||
const impl = typeof name === "string" ? implementation[name] : void 0; | ||
if (impl) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(impl); | ||
return { ...descriptor, value: spy }; | ||
} | ||
if (!selective) { | ||
return { | ||
...descriptor, | ||
value: createMethodSpy(descriptor.value, context) | ||
}; | ||
} | ||
} | ||
return { | ||
...descriptor, | ||
value: descriptor.value | ||
}; | ||
}; | ||
function MockClass(...args) { | ||
if (!(this instanceof MockClass)) { | ||
return new MockClass(...args); | ||
} | ||
const instance = Object.create(originalClass.prototype); | ||
try { | ||
const temp = new originalClass(...args); | ||
Object.assign(instance, temp); | ||
} catch { | ||
} | ||
const processMembers = (target, source) => { | ||
Object.getOwnPropertyNames(source).forEach((name) => { | ||
if (name === "constructor") return; | ||
if (!target.hasOwnProperty(name)) { | ||
const descriptor = Object.getOwnPropertyDescriptor(source, name); | ||
if (descriptor) { | ||
Object.defineProperty(target, name, handleDescriptor(name, descriptor, instance)); | ||
} | ||
} | ||
}); | ||
}; | ||
processMembers(instance, originalClass.prototype); | ||
let proto = Object.getPrototypeOf(originalClass.prototype); | ||
while (proto && proto !== Object.prototype) { | ||
processMembers(instance, proto); | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
Object.setPrototypeOf(instance, MockClass.prototype); | ||
return instance; | ||
} | ||
MockClass.prototype = Object.create(originalClass.prototype); | ||
MockClass.prototype.constructor = MockClass; | ||
Object.getOwnPropertyNames(originalClass).forEach((name) => { | ||
if (name === "length" || name === "prototype" || name === "name") return; | ||
const descriptor = Object.getOwnPropertyDescriptor(originalClass, name); | ||
if (descriptor) { | ||
Object.defineProperty(MockClass, name, handleDescriptor(name, descriptor, originalClass)); | ||
} | ||
}); | ||
handleStaticMethodInheritance(MockClass, originalClass); | ||
return MockClass; | ||
} | ||
mock2.cls = cls; | ||
function getConfig() { | ||
return configManager.get(); | ||
} | ||
mock2.getConfig = getConfig; | ||
function setConfig(options) { | ||
configManager.set(options); | ||
} | ||
mock2.setConfig = setConfig; | ||
function resetConfig() { | ||
configManager.reset(); | ||
} | ||
mock2.resetConfig = resetConfig; | ||
function withConfig(options, fn2) { | ||
return configManager.with(options, fn2); | ||
} | ||
mock2.withConfig = withConfig; | ||
function getConfigStats() { | ||
return configManager.getStats(); | ||
} | ||
mock2.getConfigStats = getConfigStats; | ||
})(mock); | ||
// src/types/cache.ts | ||
var MockCache = class { | ||
constructor() { | ||
this.cache = /* @__PURE__ */ new Map(); | ||
this.registry = new FinalizationRegistry((key) => { | ||
this.cache.delete(key); | ||
}); | ||
} | ||
/** | ||
* Set a value in the cache with optional descriptor | ||
*/ | ||
set(key, value, descriptor) { | ||
const entry = { | ||
value, | ||
descriptor, | ||
timestamp: Date.now() | ||
}; | ||
if (value && typeof value === "object") { | ||
entry.ref = new WeakRef(value); | ||
this.registry.register(value, String(key)); | ||
} | ||
this.cache.set(key, entry); | ||
} | ||
/** | ||
* Get a value from the cache | ||
*/ | ||
get(key) { | ||
const entry = this.cache.get(key); | ||
if (!entry) return void 0; | ||
if (entry.ref) { | ||
const value = entry.ref.deref(); | ||
if (!value) { | ||
this.cache.delete(key); | ||
return void 0; | ||
} | ||
} | ||
return entry.value; | ||
} | ||
/** | ||
* Get a property descriptor from the cache | ||
*/ | ||
getDescriptor(key) { | ||
return this.cache.get(key)?.descriptor; | ||
} | ||
/** | ||
* Check if a key exists in the cache | ||
*/ | ||
has(key) { | ||
return this.cache.has(key); | ||
} | ||
/** | ||
* Delete a key from the cache | ||
*/ | ||
delete(key) { | ||
this.cache.delete(key); | ||
} | ||
/** | ||
* Clear all entries from the cache | ||
*/ | ||
clear() { | ||
this.cache.clear(); | ||
} | ||
/** | ||
* Get all keys in the cache | ||
*/ | ||
keys() { | ||
return this.cache.keys(); | ||
} | ||
}; | ||
exports.MockCache = MockCache; | ||
exports.MockError = MockError; | ||
exports.MockErrorCode = MockErrorCode; | ||
exports.UniversalSpy = UniversalSpy; | ||
exports.configManager = configManager; | ||
exports.createPropertySpy = createPropertySpy; | ||
exports.createSpy = createSpy; | ||
exports.mock = mock; | ||
exports.mockConfig = mockConfig; | ||
exports.replaceFn = replaceFn; | ||
exports.spyOn = spyOn; | ||
//# sourceMappingURL=index.js.map | ||
//# sourceMappingURL=index.js.map |
@@ -1,173 +0,2 @@ | ||
// src/config.ts | ||
var defaultConfig = { | ||
debug: process.env.NODE_ENV === "development", | ||
trackCalls: true, | ||
allowUndefined: true, | ||
strict: false, | ||
timeout: 5e3, | ||
preservePrototype: true | ||
}; | ||
function isValidConfigValue(key, value) { | ||
switch (key) { | ||
case "debug": | ||
case "trackCalls": | ||
case "allowUndefined": | ||
case "strict": | ||
case "preservePrototype": | ||
return typeof value === "boolean"; | ||
case "timeout": | ||
return typeof value === "number" && value > 0; | ||
default: | ||
return false; | ||
} | ||
} | ||
var GlobalConfig = class _GlobalConfig { | ||
constructor() { | ||
this.cachedConfig = null; | ||
this.lastUpdate = 0; | ||
this.updateCount = 0; | ||
this.cacheHits = 0; | ||
this.cacheTimeout = 1e3; | ||
this.config = { ...defaultConfig }; | ||
} | ||
/** | ||
* Gets the singleton instance | ||
*/ | ||
static getInstance() { | ||
if (!_GlobalConfig.instance) { | ||
_GlobalConfig.instance = new _GlobalConfig(); | ||
} | ||
return _GlobalConfig.instance; | ||
} | ||
/** | ||
* Gets the current configuration | ||
*/ | ||
getConfig() { | ||
if (this.cachedConfig && Date.now() - this.lastUpdate < this.cacheTimeout) { | ||
this.cacheHits++; | ||
return this.cachedConfig; | ||
} | ||
this.cachedConfig = Object.freeze({ ...this.config }); | ||
return this.cachedConfig; | ||
} | ||
/** | ||
* Updates the configuration | ||
*/ | ||
updateConfig(options) { | ||
const newConfig = { ...this.config }; | ||
let hasChanges = false; | ||
Object.keys(options).forEach((key) => { | ||
if (key in defaultConfig && isValidConfigValue(key, options[key])) { | ||
if (this.config[key] !== options[key]) { | ||
newConfig[key] = options[key]; | ||
hasChanges = true; | ||
} | ||
} | ||
}); | ||
if (hasChanges) { | ||
this.config = newConfig; | ||
this.invalidateCache(); | ||
this.updateCount++; | ||
} | ||
} | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
resetConfig() { | ||
const newConfig = { | ||
...defaultConfig, | ||
debug: process.env.NODE_ENV === "development" | ||
}; | ||
if (this.hasConfigChanged(newConfig)) { | ||
this.config = newConfig; | ||
this.invalidateCache(); | ||
this.updateCount++; | ||
} | ||
} | ||
/** | ||
* Gets update statistics | ||
*/ | ||
getStats() { | ||
return { | ||
updateCount: this.updateCount, | ||
lastUpdate: this.lastUpdate, | ||
cacheHits: this.cacheHits | ||
}; | ||
} | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
withConfig(options, fn) { | ||
const previous = { ...this.getConfig() }; | ||
try { | ||
this.updateConfig(options); | ||
return fn(); | ||
} finally { | ||
this.updateConfig(previous); | ||
} | ||
} | ||
/** | ||
* Checks if configuration has changed | ||
*/ | ||
hasConfigChanged(newConfig) { | ||
return Object.entries(newConfig).some(([key, value]) => { | ||
const configKey = key; | ||
return this.config[configKey] !== value; | ||
}); | ||
} | ||
/** | ||
* Invalidates the configuration cache | ||
*/ | ||
invalidateCache() { | ||
this.cachedConfig = null; | ||
this.lastUpdate = Date.now(); | ||
} | ||
}; | ||
var configManager = { | ||
/** | ||
* Gets current configuration | ||
*/ | ||
get() { | ||
return GlobalConfig.getInstance().getConfig(); | ||
}, | ||
/** | ||
* Updates configuration | ||
*/ | ||
set(options) { | ||
GlobalConfig.getInstance().updateConfig(options); | ||
}, | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
reset() { | ||
GlobalConfig.getInstance().resetConfig(); | ||
}, | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
with(options, fn) { | ||
return GlobalConfig.getInstance().withConfig(options, fn); | ||
}, | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
getStats() { | ||
return GlobalConfig.getInstance().getStats(); | ||
} | ||
}; | ||
var mockConfig = configManager; | ||
// src/errors.ts | ||
var MockErrorCode = /* @__PURE__ */ ((MockErrorCode2) => { | ||
MockErrorCode2["CIRCULAR_REFERENCE"] = "CIRCULAR_REF"; | ||
MockErrorCode2["INVALID_TARGET"] = "INVALID_TARGET"; | ||
MockErrorCode2["INITIALIZATION_FAILED"] = "INIT_FAILED"; | ||
MockErrorCode2["TYPE_MISMATCH"] = "TYPE_MISMATCH"; | ||
MockErrorCode2["INVALID_IMPLEMENTATION"] = "INVALID_IMPL"; | ||
MockErrorCode2["INVALID_CONFIGURATION"] = "INVALID_CONFIG"; | ||
MockErrorCode2["VERIFICATION_ERROR"] = "VERIFY_ERROR"; | ||
MockErrorCode2["EXPECTATION_FAILED"] = "EXPECT_FAILED"; | ||
MockErrorCode2["PERFORMANCE_CONSTRAINT_VIOLATED"] = "PERF_VIOLATED"; | ||
return MockErrorCode2; | ||
})(MockErrorCode || {}); | ||
var MockError = class _MockError extends Error { | ||
@@ -445,9 +274,16 @@ constructor(code, message, details) { | ||
} else if (this.implementation) { | ||
result = this.implementation.apply(spy._this || this, args); | ||
result = this.implementation.apply(spy._this, args); | ||
} else { | ||
result = void 0; | ||
} | ||
spy.mock.calls.push(args); | ||
spy.mock.results.push({ type: "return", value: result }); | ||
if (spy._this) { | ||
spy.mock.instances.push(spy._this); | ||
spy.mock.contexts.push(spy._this); | ||
} | ||
finishCall(void 0, result); | ||
return result; | ||
} catch (error) { | ||
spy.mock.results.push({ type: "throw", value: error }); | ||
finishCall(error instanceof Error ? error : new Error(String(error))); | ||
@@ -469,19 +305,26 @@ throw error; | ||
}); | ||
const boundSpy = new Proxy(spy, { | ||
apply: (target, thisArg, argumentsList) => { | ||
spy._this = thisArg; | ||
const result = target.apply(thisArg, argumentsList); | ||
spy._this = null; | ||
return result; | ||
} | ||
spy.calls = { | ||
all: () => this.tracker.getCallsFor("spy").map((call) => ({ args: call.args })), | ||
count: () => this.tracker.getCallsFor("spy").length | ||
}; | ||
Object.defineProperty(spy, "mock", { | ||
value: { | ||
calls: [], | ||
results: [], | ||
instances: [], | ||
contexts: [] | ||
}, | ||
writable: true, | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
boundSpy.mockReturnValue = (value) => { | ||
spy.mockReturnValue = (value) => { | ||
this.updateState({ returnValue: value }); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockResolvedValue = (value) => { | ||
spy.mockResolvedValue = (value) => { | ||
this.updateState({ returnValue: Promise.resolve(value) }); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockImplementation = (fn) => { | ||
spy.mockImplementation = (fn) => { | ||
if (fn !== null && fn !== void 0 && typeof fn !== "function") { | ||
@@ -491,36 +334,52 @@ throw new Error("Mock implementation must be a function"); | ||
this.updateState({ implementation: fn }); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockReset = () => { | ||
spy.mockReset = () => { | ||
this.tracker.reset(); | ||
this.updateState({}); | ||
}; | ||
boundSpy.mockClear = () => { | ||
spy.mockClear = () => { | ||
this.tracker.reset(); | ||
}; | ||
boundSpy.calls = { | ||
all: () => this.tracker.getCallsFor("spy").map((call) => ({ args: call.args })), | ||
count: () => this.tracker.getCallsFor("spy").length | ||
}; | ||
boundSpy.and = { | ||
returnValue: (value) => { | ||
boundSpy.mockReturnValue(value); | ||
return boundSpy; | ||
}, | ||
callFake: (fn) => { | ||
boundSpy.mockImplementation(fn); | ||
return boundSpy; | ||
}, | ||
const jasmineApi = { | ||
returnValue: spy.mockReturnValue, | ||
callFake: spy.mockImplementation, | ||
throwError: (error) => { | ||
this.updateState({ error }); | ||
return boundSpy; | ||
return spy; | ||
}, | ||
stub: () => { | ||
boundSpy.mockImplementation(void 0); | ||
return boundSpy; | ||
spy.mockReset(); | ||
return spy; | ||
} | ||
}; | ||
boundSpy.toHaveBeenCalled = () => this.tracker.getCallsFor("spy").length > 0; | ||
boundSpy.toHaveBeenCalledTimes = (n) => this.tracker.getCallsFor("spy").length === n; | ||
boundSpy.toHaveBeenCalledWith = (...args) => this.tracker.getCallsFor("spy").some((call) => call.args.length === args.length && call.args.every((arg, i) => arg === args[i])); | ||
Object.defineProperty(spy, "and", { | ||
value: jasmineApi, | ||
writable: false, | ||
enumerable: true, | ||
configurable: true | ||
}); | ||
spy.toHaveBeenCalled = () => this.tracker.getCallsFor("spy").length > 0; | ||
spy.toHaveBeenCalledTimes = (n) => this.tracker.getCallsFor("spy").length === n; | ||
spy.toHaveBeenCalledWith = (...args) => this.tracker.getCallsFor("spy").some((call) => call.args.length === args.length && call.args.every((arg, i) => arg === args[i])); | ||
const boundSpy = new Proxy(spy, { | ||
apply: (target, thisArg, argumentsList) => { | ||
spy._this = thisArg; | ||
const result = target.apply(thisArg, argumentsList); | ||
spy._this = null; | ||
return result; | ||
}, | ||
get: (target, prop) => { | ||
const value = target[prop]; | ||
if (typeof value === "function") { | ||
return function(...args) { | ||
spy._this = this; | ||
const result = value.apply(this, args); | ||
spy._this = null; | ||
return result; | ||
}; | ||
} | ||
return value; | ||
} | ||
}); | ||
this.spyFunction = boundSpy; | ||
@@ -557,23 +416,2 @@ } | ||
} | ||
function replaceFn(obj, key, fn) { | ||
const spy = createSpy(fn); | ||
spy.mockImplementation(fn); | ||
Object.defineProperty(obj, key, { | ||
value: spy, | ||
writable: true, | ||
configurable: true | ||
}); | ||
} | ||
function spyOn(obj, key) { | ||
const value = obj[key]; | ||
if (typeof value === "function") { | ||
const spy = createSpy(value); | ||
spy.mockImplementation(value); | ||
Object.defineProperty(obj, key, { | ||
value: spy, | ||
writable: true, | ||
configurable: true | ||
}); | ||
} | ||
} | ||
function createPropertySpy() { | ||
@@ -586,578 +424,4 @@ return { | ||
// src/mock.ts | ||
function mock(targetOrImpl, optionsOrImpl) { | ||
const mockOptions = optionsOrImpl && "trackCalls" in optionsOrImpl ? optionsOrImpl : {}; | ||
const currentConfig = { | ||
...mock.getConfig(), | ||
...mockOptions | ||
}; | ||
if (currentConfig.debug) { | ||
console.debug("Mocking:", targetOrImpl); | ||
} | ||
if (!targetOrImpl) { | ||
return mock.object(); | ||
} | ||
if (typeof targetOrImpl === "function" && targetOrImpl.prototype) { | ||
return mock.cls(targetOrImpl, optionsOrImpl); | ||
} | ||
if (targetOrImpl && typeof targetOrImpl === "object" && optionsOrImpl) { | ||
return mock.partial(targetOrImpl).with(optionsOrImpl); | ||
} | ||
return mock.of(targetOrImpl); | ||
} | ||
((mock2) => { | ||
function fn() { | ||
return createSpy(); | ||
} | ||
mock2.fn = fn; | ||
function object() { | ||
const target = {}; | ||
const cache = /* @__PURE__ */ new Map(); | ||
const descriptors = /* @__PURE__ */ new Map(); | ||
const refs = /* @__PURE__ */ new WeakMap(); | ||
const config = mock2.getConfig(); | ||
const handler = { | ||
get: (_, prop) => { | ||
if (prop === "then") { | ||
return void 0; | ||
} | ||
const descriptor = descriptors.get(prop); | ||
if (descriptor) { | ||
return descriptor.get?.(); | ||
} | ||
if (!cache.has(prop)) { | ||
if (!config.allowUndefined && !prop.toString().startsWith("__")) { | ||
if (config.strict) { | ||
throw new Error(`Accessing undefined property: ${String(prop)}`); | ||
} | ||
if (config.debug) { | ||
console.warn(`Accessing undefined property: ${String(prop)}`); | ||
} | ||
} | ||
const spy = createSpy(); | ||
if (config.trackCalls) { | ||
spy.mockImplementation(function(...args) { | ||
if (config.debug) { | ||
console.debug(`Called ${String(prop)} with:`, args); | ||
} | ||
return void 0; | ||
}); | ||
} | ||
cache.set(prop, spy); | ||
return spy; | ||
} | ||
return cache.get(prop); | ||
}, | ||
set: (_, prop, value) => { | ||
if (value && typeof value === "object") { | ||
if (refs.has(value)) { | ||
cache.set(prop, value); | ||
return true; | ||
} | ||
refs.set(value, true); | ||
} | ||
cache.delete(prop); | ||
cache.set(prop, value); | ||
return true; | ||
}, | ||
defineProperty: (_, prop, descriptor) => { | ||
descriptors.set(prop, { | ||
...descriptor, | ||
configurable: true, | ||
enumerable: true | ||
}); | ||
return true; | ||
}, | ||
getOwnPropertyDescriptor: (_, prop) => { | ||
const descriptor = descriptors.get(prop); | ||
if (descriptor) { | ||
return { | ||
...descriptor, | ||
configurable: true, | ||
enumerable: true | ||
}; | ||
} | ||
if (cache.has(prop)) { | ||
return { | ||
value: cache.get(prop), | ||
writable: true, | ||
configurable: true, | ||
enumerable: true | ||
}; | ||
} | ||
return void 0; | ||
}, | ||
has: (_, prop) => { | ||
return descriptors.has(prop) || cache.has(prop); | ||
}, | ||
ownKeys: () => { | ||
return [...descriptors.keys(), ...cache.keys()]; | ||
} | ||
}; | ||
return new Proxy(target, handler); | ||
} | ||
mock2.object = object; | ||
function partial(base, stubs, options) { | ||
const config = mock2.getConfig(); | ||
const defaultOptions = { | ||
selective: false, | ||
preserveThis: config.preservePrototype, | ||
autoSpy: config.trackCalls, | ||
handleCircular: false | ||
}; | ||
const spyMethods = /* @__PURE__ */ new Set(); | ||
const preserveProps = /* @__PURE__ */ new Set(); | ||
function createMock(withStubs = {}, withOptions = {}) { | ||
const finalOptions = { ...defaultOptions, ...withOptions }; | ||
if (config.debug) { | ||
console.debug("Creating partial mock for:", base.constructor.name); | ||
} | ||
const result = Object.create(finalOptions.preserveThis ? Object.getPrototypeOf(base) : null); | ||
Object.getOwnPropertyNames(base).forEach((key) => { | ||
const typedKey = key; | ||
if (!Object.prototype.hasOwnProperty.call(withStubs, key) && !spyMethods.has(typedKey)) { | ||
if (preserveProps.has(typedKey)) { | ||
result[typedKey] = base[typedKey]; | ||
} else { | ||
const descriptor = Object.getOwnPropertyDescriptor(base, key); | ||
if (typeof descriptor.value === "function" && !finalOptions.selective) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(descriptor.value.bind(result)); | ||
result[typedKey] = spy; | ||
} else { | ||
Object.defineProperty(result, key, descriptor); | ||
} | ||
} | ||
} | ||
}); | ||
try { | ||
if (finalOptions.handleCircular) { | ||
const merged = { ...base, ...withStubs }; | ||
Object.assign(result, merged); | ||
for (const key in merged) { | ||
const value = merged[key]; | ||
if (value === base) { | ||
result[key] = result; | ||
} | ||
} | ||
} else { | ||
Object.keys(withStubs).forEach((key) => { | ||
const typedKey = key; | ||
if (preserveProps.has(typedKey)) { | ||
result[typedKey] = base[typedKey]; | ||
return; | ||
} | ||
const value = withStubs[key]; | ||
if (typeof value === "function") { | ||
const spy = createSpy(); | ||
spy.mockImplementation(finalOptions.preserveThis ? value.bind(result) : value); | ||
result[typedKey] = spy; | ||
} else if (value && typeof value === "object" && !Array.isArray(value)) { | ||
if (value === base && !finalOptions.handleCircular) { | ||
throw new Error(`Property ${String(typedKey)} has a circular reference`); | ||
} | ||
try { | ||
result[typedKey] = { ...base[typedKey], ...value }; | ||
} catch (e) { | ||
if (e instanceof RangeError) { | ||
throw new Error( | ||
`Property ${String(typedKey)} has a circular reference. | ||
Consider using handleCircular: true option.` | ||
); | ||
} | ||
throw e; | ||
} | ||
} else { | ||
result[typedKey] = value; | ||
} | ||
}); | ||
} | ||
} catch (e) { | ||
if (e instanceof RangeError && finalOptions.handleCircular) { | ||
return mock2.cast({ ...base, ...withStubs }); | ||
} | ||
throw e; | ||
} | ||
spyMethods.forEach((method) => { | ||
if (typeof result[method] === "function" && !preserveProps.has(method)) { | ||
const original = result[method]; | ||
const spy = createSpy(); | ||
spy.mockImplementation(finalOptions.preserveThis ? original.bind(result) : original); | ||
result[method] = spy; | ||
} | ||
}); | ||
return result; | ||
} | ||
const builder = { | ||
with: (withStubs, withOptions) => createMock(withStubs, withOptions), | ||
spy: (...methods) => { | ||
methods.forEach((method) => spyMethods.add(method)); | ||
return builder; | ||
}, | ||
preserve: (...properties) => { | ||
properties.forEach((prop) => preserveProps.add(prop)); | ||
return builder; | ||
} | ||
}; | ||
return stubs ? createMock(stubs, options) : builder; | ||
} | ||
mock2.partial = partial; | ||
function cast(partial2) { | ||
const proxies = /* @__PURE__ */ new WeakMap(); | ||
const handler = { | ||
get: (target, prop) => { | ||
if (prop === "then") { | ||
return void 0; | ||
} | ||
if (prop in target) { | ||
const value = target[prop]; | ||
if (value && typeof value === "object") { | ||
if (proxies.has(value)) { | ||
return proxies.get(value); | ||
} | ||
const proxy2 = new Proxy(value, handler); | ||
proxies.set(value, proxy2); | ||
return proxy2; | ||
} | ||
return value; | ||
} | ||
return void 0; | ||
} | ||
}; | ||
const proxy = new Proxy(partial2, handler); | ||
if (partial2 && typeof partial2 === "object") { | ||
proxies.set(partial2, proxy); | ||
} | ||
return proxy; | ||
} | ||
mock2.cast = cast; | ||
function of(stubs = {}) { | ||
if (Array.isArray(stubs)) { | ||
return stubs.map((item) => of(item)); | ||
} | ||
const base = object(); | ||
const result = Object.create(Object.getPrototypeOf(base)); | ||
const cache = /* @__PURE__ */ new Map(); | ||
Object.entries(stubs).forEach(([key, value]) => { | ||
result[key] = value; | ||
}); | ||
return new Proxy(result, { | ||
get(target, prop) { | ||
if (prop === "then") return void 0; | ||
if (cache.has(prop)) { | ||
return cache.get(prop); | ||
} | ||
const value = target[prop]; | ||
if (typeof value === "function") { | ||
const spy2 = createSpy(); | ||
spy2.mockImplementation(value); | ||
cache.set(prop, spy2); | ||
return spy2; | ||
} | ||
if (value !== void 0) { | ||
cache.set(prop, value); | ||
return value; | ||
} | ||
const spy = createSpy(); | ||
cache.set(prop, spy); | ||
return spy; | ||
} | ||
}); | ||
} | ||
mock2.of = of; | ||
const ORIGINAL_PREFIX = "__original_"; | ||
const EXCLUDED_PROPS = ["name", "length", "prototype", "_isMockFunction"]; | ||
function replace(obj, key, impl) { | ||
if (!obj || typeof key !== "string") return; | ||
const methodKey = String(key); | ||
const originalKey = `${ORIGINAL_PREFIX}${methodKey}`; | ||
const original = obj[key]; | ||
if (!(originalKey in obj)) { | ||
obj[originalKey] = original; | ||
} | ||
const spy = createSpy(); | ||
spy.mockImplementation(impl); | ||
if (typeof original === "function") { | ||
Object.getOwnPropertyNames(original).forEach((prop) => { | ||
if (!EXCLUDED_PROPS.includes(prop)) { | ||
spy[prop] = original[prop]; | ||
} | ||
}); | ||
} | ||
obj[key] = spy; | ||
} | ||
mock2.replace = replace; | ||
function restore(obj, key) { | ||
if (!obj) return false; | ||
const restoreMethod = (methodKey) => { | ||
const originalKey = `${ORIGINAL_PREFIX}${methodKey}`; | ||
if (!(originalKey in obj)) return false; | ||
const original = obj[originalKey]; | ||
const current = obj[methodKey]; | ||
if (current && typeof current === "function") { | ||
Object.getOwnPropertyNames(current).forEach((prop) => { | ||
if (!EXCLUDED_PROPS.includes(prop)) { | ||
try { | ||
original[prop] = current[prop]; | ||
} catch (e) { | ||
} | ||
} | ||
}); | ||
} | ||
try { | ||
obj[methodKey] = original; | ||
delete obj[originalKey]; | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
}; | ||
if (key !== void 0) { | ||
return restoreMethod(String(key)); | ||
} | ||
let anyRestored = false; | ||
const keys = Object.getOwnPropertyNames(obj); | ||
for (const k of keys) { | ||
if (k.startsWith(ORIGINAL_PREFIX)) { | ||
const methodKey = k.slice(ORIGINAL_PREFIX.length); | ||
if (restoreMethod(methodKey)) { | ||
anyRestored = true; | ||
} | ||
} | ||
} | ||
return anyRestored; | ||
} | ||
mock2.restore = restore; | ||
function getReplacedMethods(obj) { | ||
if (!obj) return []; | ||
return Object.getOwnPropertyNames(obj).filter((k) => k.startsWith(ORIGINAL_PREFIX)).map((k) => k.slice(ORIGINAL_PREFIX.length)).filter((k) => k && typeof obj[k] !== "undefined"); | ||
} | ||
mock2.getReplacedMethods = getReplacedMethods; | ||
function isReplaced(obj, key) { | ||
if (!obj) return false; | ||
const originalKey = `${ORIGINAL_PREFIX}${String(key)}`; | ||
const original = obj[originalKey]; | ||
const current = obj[key]; | ||
return originalKey in obj && current !== original; | ||
} | ||
mock2.isReplaced = isReplaced; | ||
function verifyRestored(obj, key) { | ||
if (!obj) return false; | ||
const originalKey = `${ORIGINAL_PREFIX}${String(key)}`; | ||
const original = obj[originalKey]; | ||
if (!original) return true; | ||
const current = obj[key]; | ||
if (current === original) return true; | ||
if (typeof current === "function" && typeof original === "function") { | ||
try { | ||
const currentStr = current.toString().replace(/\s+/g, ""); | ||
const originalStr = original.toString().replace(/\s+/g, ""); | ||
return currentStr === originalStr; | ||
} catch { | ||
return current === original; | ||
} | ||
} | ||
return false; | ||
} | ||
mock2.verifyRestored = verifyRestored; | ||
function handleStaticMethodInheritance(mockClass, originalClass) { | ||
let currentProto = Object.getPrototypeOf(originalClass); | ||
while (currentProto && currentProto !== Function.prototype) { | ||
Object.getOwnPropertyNames(currentProto).filter((prop) => typeof currentProto[prop] === "function").forEach((methodName) => { | ||
if (!mockClass[methodName]) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(currentProto[methodName].bind(mockClass)); | ||
mockClass[methodName] = spy; | ||
} | ||
}); | ||
currentProto = Object.getPrototypeOf(currentProto); | ||
} | ||
} | ||
function cls(originalClass, options = {}) { | ||
const config = mock2.getConfig(); | ||
const { selective = false, implementation = {} } = options; | ||
if (config.debug) { | ||
console.debug("Creating class mock for:", originalClass.name); | ||
} | ||
const createMethodSpy = (method, context) => { | ||
const spy = createSpy(); | ||
if (config.trackCalls) { | ||
spy.mockImplementation((...args) => { | ||
if (config.debug) { | ||
console.debug(`Called ${method.name} with:`, args); | ||
} | ||
return method.apply(context, args); | ||
}); | ||
} else { | ||
spy.mockImplementation((...args) => method.apply(context, args)); | ||
} | ||
return spy; | ||
}; | ||
const handleDescriptor = (name, descriptor, context) => { | ||
if (descriptor.get || descriptor.set) { | ||
const spies = createPropertySpy(); | ||
const implDescriptor = typeof name === "string" ? Object.getOwnPropertyDescriptor(implementation, name) : void 0; | ||
return { | ||
configurable: true, | ||
enumerable: true, | ||
get: descriptor.get && (implDescriptor?.get ? spies.get?.mockImplementation(implDescriptor.get) : !selective ? spies.get?.mockImplementation(descriptor.get.bind(context)) : descriptor.get.bind(context)), | ||
set: descriptor.set && (implDescriptor?.set ? spies.set?.mockImplementation(implDescriptor.set) : !selective ? spies.set?.mockImplementation(descriptor.set.bind(context)) : descriptor.set.bind(context)) | ||
}; | ||
} | ||
if (typeof descriptor.value === "function") { | ||
const impl = typeof name === "string" ? implementation[name] : void 0; | ||
if (impl) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(impl); | ||
return { ...descriptor, value: spy }; | ||
} | ||
if (!selective) { | ||
return { | ||
...descriptor, | ||
value: createMethodSpy(descriptor.value, context) | ||
}; | ||
} | ||
} | ||
return { | ||
...descriptor, | ||
value: descriptor.value | ||
}; | ||
}; | ||
function MockClass(...args) { | ||
if (!(this instanceof MockClass)) { | ||
return new MockClass(...args); | ||
} | ||
const instance = Object.create(originalClass.prototype); | ||
try { | ||
const temp = new originalClass(...args); | ||
Object.assign(instance, temp); | ||
} catch { | ||
} | ||
const processMembers = (target, source) => { | ||
Object.getOwnPropertyNames(source).forEach((name) => { | ||
if (name === "constructor") return; | ||
if (!target.hasOwnProperty(name)) { | ||
const descriptor = Object.getOwnPropertyDescriptor(source, name); | ||
if (descriptor) { | ||
Object.defineProperty(target, name, handleDescriptor(name, descriptor, instance)); | ||
} | ||
} | ||
}); | ||
}; | ||
processMembers(instance, originalClass.prototype); | ||
let proto = Object.getPrototypeOf(originalClass.prototype); | ||
while (proto && proto !== Object.prototype) { | ||
processMembers(instance, proto); | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
Object.setPrototypeOf(instance, MockClass.prototype); | ||
return instance; | ||
} | ||
MockClass.prototype = Object.create(originalClass.prototype); | ||
MockClass.prototype.constructor = MockClass; | ||
Object.getOwnPropertyNames(originalClass).forEach((name) => { | ||
if (name === "length" || name === "prototype" || name === "name") return; | ||
const descriptor = Object.getOwnPropertyDescriptor(originalClass, name); | ||
if (descriptor) { | ||
Object.defineProperty(MockClass, name, handleDescriptor(name, descriptor, originalClass)); | ||
} | ||
}); | ||
handleStaticMethodInheritance(MockClass, originalClass); | ||
return MockClass; | ||
} | ||
mock2.cls = cls; | ||
function getConfig() { | ||
return configManager.get(); | ||
} | ||
mock2.getConfig = getConfig; | ||
function setConfig(options) { | ||
configManager.set(options); | ||
} | ||
mock2.setConfig = setConfig; | ||
function resetConfig() { | ||
configManager.reset(); | ||
} | ||
mock2.resetConfig = resetConfig; | ||
function withConfig(options, fn2) { | ||
return configManager.with(options, fn2); | ||
} | ||
mock2.withConfig = withConfig; | ||
function getConfigStats() { | ||
return configManager.getStats(); | ||
} | ||
mock2.getConfigStats = getConfigStats; | ||
})(mock); | ||
// src/types/cache.ts | ||
var MockCache = class { | ||
constructor() { | ||
this.cache = /* @__PURE__ */ new Map(); | ||
this.registry = new FinalizationRegistry((key) => { | ||
this.cache.delete(key); | ||
}); | ||
} | ||
/** | ||
* Set a value in the cache with optional descriptor | ||
*/ | ||
set(key, value, descriptor) { | ||
const entry = { | ||
value, | ||
descriptor, | ||
timestamp: Date.now() | ||
}; | ||
if (value && typeof value === "object") { | ||
entry.ref = new WeakRef(value); | ||
this.registry.register(value, String(key)); | ||
} | ||
this.cache.set(key, entry); | ||
} | ||
/** | ||
* Get a value from the cache | ||
*/ | ||
get(key) { | ||
const entry = this.cache.get(key); | ||
if (!entry) return void 0; | ||
if (entry.ref) { | ||
const value = entry.ref.deref(); | ||
if (!value) { | ||
this.cache.delete(key); | ||
return void 0; | ||
} | ||
} | ||
return entry.value; | ||
} | ||
/** | ||
* Get a property descriptor from the cache | ||
*/ | ||
getDescriptor(key) { | ||
return this.cache.get(key)?.descriptor; | ||
} | ||
/** | ||
* Check if a key exists in the cache | ||
*/ | ||
has(key) { | ||
return this.cache.has(key); | ||
} | ||
/** | ||
* Delete a key from the cache | ||
*/ | ||
delete(key) { | ||
this.cache.delete(key); | ||
} | ||
/** | ||
* Clear all entries from the cache | ||
*/ | ||
clear() { | ||
this.cache.clear(); | ||
} | ||
/** | ||
* Get all keys in the cache | ||
*/ | ||
keys() { | ||
return this.cache.keys(); | ||
} | ||
}; | ||
export { MockCache, MockError, MockErrorCode, UniversalSpy, configManager, createPropertySpy, createSpy, mock, mockConfig, replaceFn, spyOn }; | ||
export { createPropertySpy, createSpy }; | ||
//# sourceMappingURL=index.js.map | ||
//# sourceMappingURL=index.js.map |
{ | ||
"name": "@corez/mock", | ||
"version": "0.2.2", | ||
"version": "0.2.4", | ||
"description": "A powerful and flexible TypeScript mocking library for testing", | ||
@@ -52,3 +52,2 @@ "keywords": [ | ||
"devDependencies": { | ||
"@jest/globals": "^29.7.0", | ||
"@release-it/conventional-changelog": "^9.0.4", | ||
@@ -55,0 +54,0 @@ "@types/jest": "^29.5.14", |
1420
README.md
# @corez/mock | ||
A powerful and flexible TypeScript mocking library for testing. | ||
A powerful, flexible, and type-safe mocking library for TypeScript testing. | ||
@@ -10,125 +10,24 @@ [](https://badge.fury.io/js/@corez/mock) | ||
## Table of Contents | ||
- [Features](#features) | ||
- [Installation](#installation) | ||
- [Quick Start](#quick-start) | ||
- [Core Concepts](#core-concepts) | ||
- [API Reference](#api-reference) | ||
- [Advanced Usage](#advanced-usage) | ||
- [Best Practices](#best-practices) | ||
- [Troubleshooting](#troubleshooting) | ||
- [Contributing](#contributing) | ||
- [License](#license) | ||
- [@corez/mock](#corezmock) | ||
- [Table of Contents](#table-of-contents) | ||
- [Core Concepts](#core-concepts) | ||
- [What is Mocking?](#what-is-mocking) | ||
- [Type Safety](#type-safety) | ||
- [Framework Integration](#framework-integration) | ||
- [Installation](#installation) | ||
- [API Reference](#api-reference) | ||
- [Core Functions](#core-functions) | ||
- [mock()](#mock) | ||
- [mock.object()](#mockobject) | ||
- [mock.of()](#mockof) | ||
- [mock.cls()](#mockcls) | ||
- [mock.fn()](#mockfn) | ||
- [mock.partial()](#mockpartial) | ||
- [mock.cast()](#mockcast) | ||
- [mock.replace()](#mockreplace) | ||
- [mock.restore()](#mockrestore) | ||
- [Configuration Methods](#configuration-methods) | ||
- [Best Practices](#best-practices) | ||
- [Testing Patterns](#testing-patterns) | ||
- [Common Pitfalls](#common-pitfalls) | ||
- [Troubleshooting](#troubleshooting) | ||
- [Common Issues](#common-issues) | ||
- [Error Messages](#error-messages) | ||
- [Development](#development) | ||
- [TypeScript Configuration](#typescript-configuration) | ||
- [Contributing](#contributing) | ||
- [Development Guidelines](#development-guidelines) | ||
- [License](#license) | ||
## Features | ||
## Core Concepts | ||
- 🎯 **Type Safety** - Full TypeScript support with precise type inference | ||
- 🔄 **Deep Mocking** - Automatic mocking of nested objects and methods | ||
- 🕵️ **Spy Tracking** - Comprehensive call tracking and verification | ||
- 🎭 **Multiple Mocking Styles** - Support for functions, objects, and classes | ||
- 🔗 **Inheritance Support** - Proper handling of class inheritance and prototype chains | ||
- 🎮 **Intuitive API** - Clean and chainable API design | ||
- 🛡️ **Strict Mode** - Optional strict mode for rigorous testing | ||
- 🔍 **Debug Support** - Detailed logging for troubleshooting | ||
### What is Mocking? | ||
Mocking is a technique used in unit testing to isolate the code being tested by replacing dependencies with controlled | ||
test doubles. This library provides three main types of test doubles: | ||
1. **Mocks**: Complete replacements for dependencies | ||
- Full control over behavior | ||
- Verification capabilities | ||
- Type-safe implementations | ||
2. **Spies**: Wrappers around existing functions | ||
- Track method calls | ||
- Preserve original behavior | ||
- Add verification capabilities | ||
3. **Stubs**: Simple implementations | ||
- Return predefined values | ||
- No verification needed | ||
- Minimal implementation | ||
### Type Safety | ||
The library is built with TypeScript first in mind: | ||
```typescript | ||
interface UserService { | ||
getUser(id: number): Promise<User>; | ||
updateUser(id: number, data: Partial<User>): Promise<void>; | ||
} | ||
const mockService = mock<UserService>(); | ||
// ✅ Valid - matches interface | ||
mockService.getUser.mockResolvedValue({id: 1, name: 'User'}); | ||
// ❌ Type Error - wrong parameter type | ||
mockService.getUser.mockImplementation((id: string) => Promise.resolve({id: 1})); | ||
// ❌ Type Error - missing required property | ||
mockService.getUser.mockResolvedValue({id: 1}); | ||
``` | ||
Key type safety features: | ||
- Full interface compliance | ||
- Parameter type checking | ||
- Return type validation | ||
- Generic type support | ||
- Strict null checks | ||
- Method signature matching | ||
### Framework Integration | ||
Works seamlessly with popular testing frameworks: | ||
```typescript | ||
// Jest | ||
describe('UserService', () => { | ||
let mockService: jest.Mocked<UserService>; | ||
beforeEach(() => { | ||
mockService = mock<UserService>(); | ||
}); | ||
it('should mock async methods', async () => { | ||
mockService.getUser.mockResolvedValue({id: 1, name: 'Test'}); | ||
const result = await mockService.getUser(1); | ||
expect(result.name).toBe('Test'); | ||
}); | ||
}); | ||
// Jasmine | ||
describe('UserService', () => { | ||
let mockService: jasmine.SpyObj<UserService>; | ||
beforeEach(() => { | ||
mockService = mock<UserService>(); | ||
}); | ||
it('should track calls', () => { | ||
mockService.getUser(1); | ||
expect(mockService.getUser).toHaveBeenCalledWith(1); | ||
}); | ||
}); | ||
``` | ||
## Installation | ||
@@ -143,33 +42,16 @@ | ||
# Using pnpm (recommended) | ||
# Using pnpm | ||
pnpm add -D @corez/mock | ||
``` | ||
## API Reference | ||
## Quick Start | ||
### Core Functions | ||
#### mock() | ||
The primary function that intelligently determines how to create mocks based on the input type: | ||
```typescript | ||
// Function signatures | ||
function mock<T>(): T; // Create from interface | ||
function mock<T>(target: Class<T>): T; // Create from class | ||
function mock<T>(target: T): T; // Create from object | ||
function mock<T>(target?: T, options?: MockOptions<T>): T; // Create with options | ||
import {mock} from '@corez/mock'; | ||
interface MockOptions<T> { | ||
selective?: boolean; // Only mock specified methods | ||
preserveThis?: boolean; // Maintain original 'this' context | ||
handleCircular?: boolean; // Handle circular references | ||
implementation?: DeepPartial<T>; // Default implementations | ||
debug?: boolean; // Enable debug logging | ||
trackCalls?: boolean; // Track method calls | ||
preservePrototype?: boolean; // Preserve prototype chain | ||
} | ||
// Mock a function | ||
const greet = mock.fn<(name: string) => string>(); | ||
greet.mockImplementation(name => `Hello, ${name}!`); | ||
// Examples: | ||
// 1. Mock from interface | ||
// Mock an object | ||
interface UserService { | ||
@@ -180,34 +62,27 @@ getUser(id: number): Promise<User>; | ||
const mockService = mock<UserService>(); | ||
mockService.getUser.mockResolvedValue({id: 1, name: 'Mock'}); | ||
mockService.updateUser.mockResolvedValue(); | ||
const userService = mock.obj<UserService>( | ||
{ | ||
getUser: async id => ({id, name: 'John'}), | ||
updateUser: async user => {}, | ||
}, | ||
{ | ||
overrides: { | ||
getUser: async id => ({id, name: 'Mock User'}), | ||
}, | ||
}, | ||
); | ||
// 2. Mock from class | ||
class DataService { | ||
private data = new Map<string, any>(); | ||
async getData(id: string) { | ||
return this.data.get(id); | ||
// Mock a class | ||
class Database { | ||
async connect() { | ||
/* ... */ | ||
} | ||
async query(sql: string) { | ||
/* ... */ | ||
} | ||
} | ||
const mockDataService = mock(DataService); | ||
mockDataService.getData.mockResolvedValue({id: '1', data: 'mock'}); | ||
// 3. Mock from object | ||
const realService = { | ||
calculate: (x: number, y: number) => x + y, | ||
config: {timeout: 1000}, | ||
}; | ||
const mockService = mock(realService); | ||
mockService.calculate.mockReturnValue(42); | ||
mockService.config.timeout = 2000; | ||
// 4. Mock with options | ||
const mockWithOptions = mock<UserService>({ | ||
selective: true, | ||
preserveThis: true, | ||
implementation: { | ||
getUser: async id => ({id, name: 'Mock'}), | ||
const db = mock.cls(Database, { | ||
overrides: { | ||
query: async () => [{id: 1}], | ||
}, | ||
@@ -217,922 +92,334 @@ }); | ||
Key features: | ||
## Core Concepts | ||
- Intelligent type inference | ||
- Full TypeScript support | ||
- Automatic spy creation | ||
- Property tracking | ||
- Method call monitoring | ||
- Framework compatibility (Jest/Jasmine) | ||
- Chainable API support | ||
### Mock Functions | ||
#### mock.object() | ||
Create standalone mock functions with full tracking capabilities: | ||
Creates a fully mocked object where all properties and methods are spies: | ||
```typescript | ||
// Function signature | ||
function object<T extends object>(): T; | ||
const mockFn = mock.fn<(x: number) => number>(); | ||
// Examples: | ||
interface Service { | ||
getData(): Promise<string>; | ||
processValue(value: string): string; | ||
config: { | ||
timeout: number; | ||
retries: number; | ||
}; | ||
} | ||
// Set implementation | ||
mockFn.mockImplementation(x => x * 2); | ||
const mockService = mock.object<Service>(); | ||
// Set return value | ||
mockFn.mockReturnValue(42); | ||
// Mock methods | ||
mockService.getData.mockResolvedValue('test data'); | ||
mockService.processValue.mockImplementation(value => `processed-${value}`); | ||
// Handle async scenarios | ||
mockFn.mockResolvedValue('result'); | ||
mockFn.mockRejectedValue(new Error('failed')); | ||
// Set properties | ||
mockService.config = { | ||
timeout: 1000, | ||
retries: 3, | ||
}; | ||
// Use the mock | ||
await mockService.getData(); // Returns 'test data' | ||
mockService.processValue('input'); // Returns 'processed-input' | ||
expect(mockService.getData).toHaveBeenCalled(); | ||
expect(mockService.processValue).toHaveBeenCalledWith('input'); | ||
// Verify calls | ||
expect(mockFn.calls.count()).toBe(1); | ||
expect(mockFn.calls.all()[0].args).toEqual([1]); | ||
``` | ||
Key features: | ||
### Mock Objects | ||
- Creates spies for all methods automatically | ||
- Supports property tracking | ||
- Handles nested objects | ||
- Preserves type information | ||
- Supports getters and setters | ||
- Handles circular references | ||
- Maintains property descriptors | ||
Create mock objects with automatic method tracking: | ||
#### mock.of() | ||
Creates a mock from stubs, supporting both object and array types: | ||
```typescript | ||
// Function signatures | ||
function of<T extends Array<any>>(stubs?: Array<DeepPartial<T[number]>>): MockOf<T>; | ||
function of<T extends ReadonlyArray<any>>(stubs?: ReadonlyArray<DeepPartial<T[number]>>): MockOf<T>; | ||
function of<T extends object>(stubs?: DeepPartial<T>): MockOf<T>; | ||
// Examples: | ||
// 1. Object mocking | ||
interface User { | ||
id: number; | ||
name: string; | ||
getData(): Promise<string>; | ||
getDetails(): {age: number; email: string}; | ||
interface UserService { | ||
getUser(id: number): Promise<User>; | ||
updateUser(user: User): Promise<void>; | ||
} | ||
// With initial values | ||
const mockUser = mock.of<User>({ | ||
id: 1, | ||
name: 'John', | ||
}); | ||
// id and name are preserved | ||
// getData and getDetails are automatically mocked | ||
mockUser.getData.mockResolvedValue('test data'); | ||
mockUser.getDetails.mockReturnValue({age: 30, email: 'john@example.com'}); | ||
// With method implementation | ||
const mockWithMethod = mock.of<User>({ | ||
id: 1, | ||
getData: async () => 'real data', | ||
getDetails: () => ({age: 25, email: 'test@example.com'}), | ||
}); | ||
// Method implementations are preserved but monitored | ||
expect(mockWithMethod.getData).toHaveBeenCalled(); | ||
// 2. Array mocking | ||
interface Task { | ||
id: number; | ||
title: string; | ||
complete(): Promise<void>; | ||
} | ||
const mockTasks = mock.of<Task[]>([ | ||
{id: 1, title: 'Task 1'}, | ||
{id: 2, title: 'Task 2'}, | ||
]); | ||
// Array items are mocked | ||
mockTasks[0].complete.mockResolvedValue(); | ||
mockTasks[1].complete.mockRejectedValue(new Error('Failed')); | ||
// 3. Nested objects | ||
interface ComplexObject { | ||
data: { | ||
id: number; | ||
nested: { | ||
value: string; | ||
}; | ||
}; | ||
getNestedValue(): string; | ||
} | ||
const mockComplex = mock.of<ComplexObject>({ | ||
data: { | ||
id: 1, | ||
nested: { | ||
value: 'test', | ||
const userService = mock.obj<UserService>( | ||
{ | ||
getUser: async id => ({id, name: 'John'}), | ||
updateUser: async user => {}, | ||
}, | ||
{ | ||
overrides: { | ||
getUser: async id => ({id, name: 'Mock User'}), | ||
}, | ||
}, | ||
getNestedValue: function () { | ||
return this.data.nested.value; | ||
}, | ||
}); | ||
); | ||
// Access call information | ||
const getUserMock = userService.getUser as MockFunction; | ||
console.log(getUserMock.calls.count()); | ||
console.log(getUserMock.calls.all()); | ||
``` | ||
Key features: | ||
### Mock Classes | ||
- Preserves provided values | ||
- Automatically mocks undefined methods | ||
- Supports array mocking | ||
- Handles nested objects | ||
- Maintains type safety | ||
- Monitors method calls | ||
- Supports method chaining | ||
Create mock classes with automatic method tracking: | ||
#### mock.cls() | ||
Creates mock classes with full type safety and spy capabilities: | ||
```typescript | ||
// Function signature | ||
function cls<T extends new (...args: any[]) => any>(classConstructor: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
interface ClsMockOptions<T extends new (...args: any[]) => any> { | ||
selective?: boolean; // Only mock specified methods | ||
implementation?: DeepPartial<InstanceType<T>>; // Default implementations | ||
} | ||
// Examples: | ||
// 1. Basic class mocking | ||
class UserService { | ||
private value: string; | ||
constructor(initialValue: string = '') { | ||
this.value = initialValue; | ||
class Database { | ||
async connect() { | ||
/* ... */ | ||
} | ||
getValue(): string { | ||
return this.value; | ||
async query(sql: string) { | ||
/* ... */ | ||
} | ||
setValue(newValue: string): void { | ||
this.value = newValue; | ||
} | ||
static staticMethod(): string { | ||
return 'static'; | ||
} | ||
} | ||
// Create mock class | ||
const MockService = mock.cls(UserService); | ||
const instance = new MockService('test'); | ||
// Instance methods are spied on | ||
expect(instance.getValue()).toBe('test'); // Original behavior preserved | ||
expect(instance.getValue).toHaveBeenCalled(); // Call tracked | ||
// Static methods are spied on | ||
expect(MockService.staticMethod()).toBe('static'); // Original behavior preserved | ||
expect(MockService.staticMethod).toHaveBeenCalled(); // Call tracked | ||
// 2. Selective mocking | ||
const SelectiveMock = mock.cls(UserService, { | ||
selective: true, | ||
implementation: { | ||
getValue: () => 'mocked', | ||
const MockDatabase = mock.cls(Database, { | ||
overrides: { | ||
query: async () => [{id: 1}], | ||
}, | ||
}); | ||
const selectiveInstance = new SelectiveMock(); | ||
expect(selectiveInstance.getValue()).toBe('mocked'); // Mocked method | ||
expect(selectiveInstance.getValue).toHaveBeenCalled(); // Call tracked | ||
expect(SelectiveMock.staticMethod()).toBe('static'); // Original not mocked | ||
// 3. Inheritance support | ||
class BaseClass { | ||
static baseStatic() { | ||
return 'base'; | ||
} | ||
baseMethod() { | ||
return 'base method'; | ||
} | ||
} | ||
class DerivedClass extends BaseClass { | ||
static derivedStatic() { | ||
return 'derived'; | ||
} | ||
derivedMethod() { | ||
return 'derived method'; | ||
} | ||
} | ||
const MockDerived = mock.cls(DerivedClass); | ||
const derivedInstance = new MockDerived(); | ||
// Both base and derived methods are mocked | ||
expect(MockDerived.baseStatic()).toBe('base'); | ||
expect(MockDerived.derivedStatic()).toBe('derived'); | ||
expect(derivedInstance.baseMethod()).toBe('base method'); | ||
expect(derivedInstance.derivedMethod()).toBe('derived method'); | ||
const db = new MockDatabase(); | ||
// Access call information | ||
const queryMock = db.query as MockFunction; | ||
console.log(queryMock.calls.count()); | ||
``` | ||
Key features: | ||
## API Reference | ||
- Mocks both instance and static methods | ||
- Preserves original behavior by default | ||
- Supports selective mocking | ||
- Handles inheritance chain | ||
- Maintains prototype chain | ||
- Tracks all method calls | ||
- Supports constructor arguments | ||
- Type-safe implementation | ||
### Core APIs | ||
#### mock.fn() | ||
#### `mock.fn<T extends Fn = Fn>(): MockFunction<T>` | ||
Creates mock functions with full spy capabilities: | ||
Creates a mock function with tracking capabilities: | ||
```typescript | ||
// Function signature | ||
function fn<T extends Fn = Fn>(): MockFunction<T>; | ||
const mockFn = mock.fn<(x: number) => number>(); | ||
interface MockFunction<T extends Fn = Fn> extends Function { | ||
(...args: Parameters<T>): ReturnType<T>; | ||
mockReturnValue(value: ReturnType<T>): this; | ||
mockResolvedValue<U>(value: U): MockFunction<AsyncFn & {(...args: Parameters<T>): Promise<U>}>; | ||
mockImplementation(fn: T): this; | ||
mockReset(): void; | ||
mockClear(): void; | ||
calls: { | ||
all(): Array<{args: Parameters<T>}>; | ||
count(): number; | ||
}; | ||
toHaveBeenCalled(): boolean; | ||
toHaveBeenCalledTimes(n: number): boolean; | ||
toHaveBeenCalledWith(...args: Parameters<T>): boolean; | ||
} | ||
// Set implementation | ||
mockFn.mockImplementation(x => x * 2); | ||
// Examples: | ||
// 1. Basic mock function | ||
const mockFn = mock.fn(); | ||
mockFn.mockReturnValue('result'); | ||
mockFn('arg1'); | ||
expect(mockFn).toHaveBeenCalledWith('arg1'); | ||
expect(mockFn.calls.count()).toBe(1); | ||
// Set return value | ||
mockFn.mockReturnValue(42); | ||
// 2. Typed mock function | ||
interface User { | ||
id: number; | ||
name: string; | ||
} | ||
const getUser = mock.fn<(id: number) => Promise<User>>(); | ||
getUser.mockResolvedValue({id: 1, name: 'Test User'}); | ||
await getUser(1); | ||
expect(getUser).toHaveBeenCalledWith(1); | ||
// Handle async scenarios | ||
mockFn.mockResolvedValue('result'); | ||
mockFn.mockRejectedValue(new Error('failed')); | ||
// 3. With implementation | ||
const calculate = mock.fn((x: number, y: number) => x * y); | ||
expect(calculate(2, 3)).toBe(6); | ||
expect(calculate).toHaveBeenCalledWith(2, 3); | ||
// 4. Multiple return values | ||
const multiValue = mock.fn(); | ||
multiValue.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValue(3); | ||
expect(multiValue()).toBe(1); | ||
expect(multiValue()).toBe(2); | ||
expect(multiValue()).toBe(3); | ||
// 5. Async function mocking | ||
const fetchData = mock.fn<(query: string) => Promise<any>>(); | ||
fetchData.mockResolvedValueOnce({data: 'test'}).mockRejectedValueOnce(new Error('Network error')); | ||
await fetchData('query1'); // Returns { data: 'test' } | ||
await expect(fetchData('query2')).rejects.toThrow('Network error'); | ||
// 6. Call tracking | ||
const trackedFn = mock.fn(); | ||
trackedFn('a', 1); | ||
trackedFn('b', 2); | ||
expect(trackedFn.calls.count()).toBe(2); | ||
expect(trackedFn.calls.all()).toEqual([{args: ['a', 1]}, {args: ['b', 2]}]); | ||
// Verify calls | ||
expect(mockFn.calls.count()).toBe(1); | ||
expect(mockFn.calls.all()[0].args).toEqual([1]); | ||
``` | ||
Key features: | ||
#### `mock.obj<T extends object>(target: T | undefined, options?: ObjMockOptions<T>): MockObject<T>` | ||
- Type-safe mock functions | ||
- Flexible return values | ||
- Async support | ||
- Call tracking | ||
- Chainable API | ||
- Multiple return values | ||
- Error simulation | ||
- Framework compatibility | ||
Creates a mock object with automatic method tracking: | ||
#### mock.partial() | ||
Creates partial mocks from existing objects or class instances: | ||
```typescript | ||
// Function signature | ||
function partial<T extends object>(base: T, stubs?: DeepPartial<T>, options?: PartialOptions<T>): T | PartialBuilder<T>; | ||
interface PartialOptions<T> { | ||
selective?: boolean; // Only mock specified methods | ||
preserveThis?: boolean; // Maintain original 'this' context | ||
handleCircular?: boolean; // Handle circular references | ||
interface UserService { | ||
getUser(id: number): Promise<User>; | ||
updateUser(user: User): Promise<void>; | ||
} | ||
// Examples: | ||
// 1. Basic partial mocking | ||
class UserService { | ||
private value: string = 'original'; | ||
getValue(): string { | ||
return this.value; | ||
} | ||
setValue(newValue: string): void { | ||
this.value = newValue; | ||
} | ||
process(data: string): string { | ||
return data.toUpperCase(); | ||
} | ||
} | ||
const service = new UserService(); | ||
const mockService = mock.partial(service, { | ||
getValue: () => 'mocked', | ||
}); | ||
expect(mockService.getValue()).toBe('mocked'); // Mocked method | ||
expect(mockService.getValue).toHaveBeenCalled(); // Call tracked | ||
expect(mockService.process('test')).toBe('TEST'); // Original method | ||
// 2. Selective mocking with preserved context | ||
class DataService { | ||
private data = new Map<string, any>(); | ||
constructor(initialData: Record<string, any> = {}) { | ||
Object.entries(initialData).forEach(([key, value]) => { | ||
this.data.set(key, value); | ||
}); | ||
} | ||
getData(key: string): any { | ||
return this.data.get(key); | ||
} | ||
setData(key: string, value: any): void { | ||
this.data.set(key, value); | ||
} | ||
} | ||
const dataService = new DataService({key: 'value'}); | ||
const mockDataService = mock.partial( | ||
dataService, | ||
const userService = mock.obj<UserService>( | ||
{ | ||
getData: function (this: DataService, key: string) { | ||
return this.data.get(key)?.toUpperCase(); | ||
}, | ||
getUser: async id => ({id, name: 'John'}), | ||
updateUser: async user => {}, | ||
}, | ||
{ | ||
preserveThis: true, | ||
overrides: { | ||
getUser: async id => ({id, name: 'Mock User'}), | ||
}, | ||
}, | ||
); | ||
mockDataService.setData('key', 'test'); | ||
expect(mockDataService.getData('key')).toBe('TEST'); // Uses this context | ||
expect(mockDataService.getData).toHaveBeenCalledWith('key'); | ||
// Access call information | ||
const getUserMock = userService.getUser as MockFunction; | ||
console.log(getUserMock.calls.count()); | ||
console.log(getUserMock.calls.all()); | ||
// 3. Handling circular references | ||
interface Node { | ||
value: string; | ||
next?: Node; | ||
} | ||
// Verify specific calls | ||
expect(getUserMock.calls.count()).toBe(1); | ||
expect(getUserMock.calls.all()[0].args).toEqual([1]); | ||
``` | ||
const node: Node = { | ||
value: 'root', | ||
}; | ||
node.next = node; // Circular reference | ||
#### `mock.cls<T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>): ClsMock<T>` | ||
const mockNode = mock.partial( | ||
node, | ||
{ | ||
value: 'mocked', | ||
}, | ||
{ | ||
handleCircular: true, | ||
}, | ||
); | ||
Creates a mock class with automatic method tracking: | ||
expect(mockNode.value).toBe('mocked'); | ||
expect(mockNode.next?.value).toBe('mocked'); // Circular reference handled | ||
// 4. Chainable API | ||
class Service { | ||
getValue() { | ||
return 'original'; | ||
```typescript | ||
class Database { | ||
async connect() { | ||
/* ... */ | ||
} | ||
transform(value: string) { | ||
return value.toUpperCase(); | ||
async query(sql: string) { | ||
/* ... */ | ||
} | ||
} | ||
const chainableMock = mock | ||
.partial(new Service()) | ||
.spy('getValue') | ||
.preserve('transform') | ||
.with({ | ||
getValue: () => 'mocked', | ||
}); | ||
const MockDatabase = mock.cls(Database, { | ||
overrides: { | ||
query: async () => [{id: 1}], | ||
}, | ||
}); | ||
expect(chainableMock.getValue()).toBe('mocked'); | ||
expect(chainableMock.getValue).toHaveBeenCalled(); | ||
expect(chainableMock.transform('test')).toBe('TEST'); | ||
const db = new MockDatabase(); | ||
// Access call information | ||
const queryMock = db.query as MockFunction; | ||
console.log(queryMock.calls.count()); | ||
// Verify method calls | ||
expect(queryMock.calls.count()).toBe(1); | ||
expect(queryMock.calls.all()[0].args).toEqual(['SELECT * FROM users']); | ||
``` | ||
Key features: | ||
#### `mock.compose<T extends Fn>(): MockFunction<T>;` | ||
- Selective method mocking | ||
- Original behavior preservation | ||
- Context preservation | ||
- Circular reference handling | ||
- Property tracking | ||
- Method call monitoring | ||
- Chainable API support | ||
- Type-safe implementation | ||
#### `mock.compose<T extends new (...args: any[]) => any>(target: T, options?: {overrides?: DeepPartial<InstanceType<T>>} & Partial<Config>): ClsMock<T>;` | ||
#### mock.cast() | ||
#### `mock.compose<T extends object>(target: T, options?: {overrides?: DeepPartial<T>; replace?: {[K in keyof T]?: T[K] extends Fn ? Fn : never}} & Partial<Config>): T;` | ||
Casts an existing mock to a different type while preserving its behavior: | ||
#### `mock.compose<T extends object>(partialImpl: DeepPartial<T>, options?: Partial<Config>): T;` | ||
Creates a mock from a class constructor, object, or function: | ||
```typescript | ||
// Function signature | ||
function cast<T, U>(mock: T): U; | ||
// Mock a function | ||
const mockFn = mock.compose<(x: number) => string>(); | ||
mockFn.mockImplementation(x => x.toString()); | ||
// Examples: | ||
// 1. Basic type casting | ||
interface BaseService { | ||
getData(): string; | ||
// Mock a class | ||
class Database { | ||
async query(sql: string) { | ||
/* ... */ | ||
} | ||
} | ||
interface ExtendedService extends BaseService { | ||
getExtendedData(): number; | ||
} | ||
const MockDatabase = mock.compose(Database, { | ||
overrides: { | ||
query: async () => [{id: 1}], | ||
}, | ||
}); | ||
const baseMock = mock.object<BaseService>(); | ||
const extendedMock = mock.cast<BaseService, ExtendedService>(baseMock); | ||
// Original behavior is preserved | ||
baseMock.getData.mockReturnValue('data'); | ||
expect(extendedMock.getData()).toBe('data'); | ||
// New methods are available | ||
extendedMock.getExtendedData.mockReturnValue(42); | ||
expect(extendedMock.getExtendedData()).toBe(42); | ||
// 2. Complex object casting | ||
interface ComplexType { | ||
nested: { | ||
deep: { | ||
value: number; | ||
}; | ||
}; | ||
method(): string; | ||
// Mock an object | ||
interface Api { | ||
fetch(url: string): Promise<any>; | ||
} | ||
const partial = { | ||
nested: { | ||
deep: { | ||
value: 42, | ||
const api = mock.compose<Api>( | ||
{ | ||
fetch: async url => ({data: []}), | ||
}, | ||
{ | ||
overrides: { | ||
fetch: async url => ({data: [{id: 1}]}), | ||
}, | ||
}, | ||
method: () => 'test', | ||
}; | ||
); | ||
const typed = mock.cast<ComplexType>(partial); | ||
expect(typed.nested.deep.value).toBe(42); | ||
expect(typed.method()).toBe('test'); | ||
// 3. Array type casting | ||
interface Item { | ||
id: number; | ||
name: string; | ||
// Mock with partial implementation | ||
interface ComplexApi { | ||
getUsers(): Promise<User[]>; | ||
getUser(id: number): Promise<User>; | ||
createUser(user: User): Promise<void>; | ||
} | ||
const items = [ | ||
{id: 1, name: 'Item 1'}, | ||
{id: 2, name: 'Item 2'}, | ||
]; | ||
const typedItems = mock.cast<Item[]>(items); | ||
expect(typedItems[0].id).toBe(1); | ||
expect(typedItems[1].name).toBe('Item 2'); | ||
const partialApi = mock.compose<ComplexApi>({ | ||
getUsers: async () => [{id: 1, name: 'John'}], | ||
getUser: async id => ({id, name: 'John'}), | ||
}); | ||
``` | ||
Key features: | ||
#### `mock.cast<T extends object>(partial: DeepPartial<T>, options?: Partial<Config>): T` | ||
- Type-safe casting | ||
- Behavior preservation | ||
- Property access | ||
- Method call tracking | ||
- Nested object support | ||
- Array support | ||
- Framework compatibility | ||
Casts a partial implementation to a complete mock: | ||
#### mock.replace() | ||
Temporarily replaces a method or property on an object: | ||
```typescript | ||
// Function signature | ||
function replace<T extends object, K extends keyof T>(obj: T, key: K, impl: T[K] & Function): void; | ||
// Examples: | ||
// 1. Basic method replacement | ||
class UserService { | ||
async getUser(id: number) { | ||
// Real implementation | ||
return {id, name: 'Real User'}; | ||
} | ||
getValue() { | ||
return 'original'; | ||
} | ||
interface CompleteApi { | ||
getUsers(): Promise<User[]>; | ||
getUser(id: number): Promise<User>; | ||
createUser(user: User): Promise<void>; | ||
updateUser(user: User): Promise<void>; | ||
deleteUser(id: number): Promise<void>; | ||
} | ||
const service = new UserService(); | ||
// Replace async method | ||
mock.replace(service, 'getUser', async id => { | ||
return {id, name: 'Mock User'}; | ||
// Only implement the methods we need | ||
const api = mock.cast<CompleteApi>({ | ||
getUsers: async () => [{id: 1, name: 'John'}], | ||
getUser: async id => ({id, name: 'John'}), | ||
}); | ||
// Method is replaced and monitored | ||
const user = await service.getUser(1); | ||
expect(user.name).toBe('Mock User'); | ||
expect(service.getUser).toHaveBeenCalledWith(1); | ||
// All other methods will be automatically mocked | ||
await api.createUser({id: 1, name: 'Test'}); // Works, returns undefined | ||
await api.updateUser({id: 1, name: 'Test'}); // Works, returns undefined | ||
await api.deleteUser(1); // Works, returns undefined | ||
// 2. Function property handling | ||
const original = function () { | ||
return 'original'; | ||
}; | ||
(original as any).customProp = 'test'; | ||
const obj = {method: original}; | ||
mock.replace(obj, 'method', () => 'mocked'); | ||
// Original properties are preserved | ||
expect((obj.method as any).customProp).toBe('test'); | ||
expect(obj.method()).toBe('mocked'); | ||
// 3. Multiple replacements | ||
class Service { | ||
method1() { | ||
return 'original1'; | ||
} | ||
method2() { | ||
return 'original2'; | ||
} | ||
} | ||
const svc = new Service(); | ||
mock.replace(svc, 'method1', () => 'mocked1'); | ||
mock.replace(svc, 'method2', () => 'mocked2'); | ||
expect(svc.method1()).toBe('mocked1'); | ||
expect(svc.method2()).toBe('mocked2'); | ||
expect(svc.method1).toHaveBeenCalled(); | ||
expect(svc.method2).toHaveBeenCalled(); | ||
// 4. Restore original methods | ||
mock.restore(svc, 'method1'); // Restore single method | ||
expect(svc.method1()).toBe('original1'); | ||
mock.restore(svc); // Restore all methods | ||
expect(svc.method2()).toBe('original2'); | ||
// Access call information | ||
const createUserMock = api.createUser as MockFunction; | ||
expect(createUserMock.calls.count()).toBe(1); | ||
``` | ||
Key features: | ||
#### `mock.replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn, options?: Partial<Config>): void` | ||
- Temporary method replacement | ||
- Original property preservation | ||
- Method call tracking | ||
- Multiple replacements | ||
- Selective restoration | ||
- Type safety | ||
- Framework compatibility | ||
Replaces methods while preserving original implementation: | ||
#### mock.restore() | ||
Restores replaced methods to their original implementations: | ||
```typescript | ||
// Function signatures | ||
function restore<T extends object>(obj: T): boolean; // Restore all methods | ||
function restore<T extends object, K extends keyof T>(obj: T, key: K): boolean; // Restore specific method | ||
// Examples: | ||
// 1. Basic method restoration | ||
class UserService { | ||
async getUser(id: number) { | ||
return {id, name: 'Real User'}; | ||
} | ||
} | ||
const service = new UserService(); | ||
mock.replace(service, 'getUser', async id => ({id, name: 'Mock User'})); | ||
// Verify mock works | ||
const mockResult = await service.getUser(1); | ||
expect(mockResult.name).toBe('Mock User'); | ||
// Restore original method | ||
const restored = mock.restore(service, 'getUser'); | ||
expect(restored).toBe(true); // Method was restored | ||
// Original behavior is restored | ||
const realResult = await service.getUser(1); | ||
expect(realResult.name).toBe('Real User'); | ||
// 2. Batch restoration | ||
class Service { | ||
method1() { | ||
return 'original1'; | ||
async getData() { | ||
/* ... */ | ||
} | ||
method2() { | ||
return 'original2'; | ||
} | ||
} | ||
const svc = new Service(); | ||
mock.replace(svc, 'method1', () => 'mocked1'); | ||
mock.replace(svc, 'method2', () => 'mocked2'); | ||
// Get list of replaced methods | ||
const replaced = mock.getReplacedMethods(svc); | ||
expect(replaced).toEqual(['method1', 'method2']); | ||
// Restore all methods | ||
const batchRestored = mock.restore(svc); | ||
expect(batchRestored).toBe(true); // Methods were restored | ||
// 3. Restoration verification | ||
class VerifyService { | ||
getValue() { | ||
return 'original'; | ||
async processData(data: any) { | ||
/* ... */ | ||
} | ||
} | ||
const verifySvc = new VerifyService(); | ||
mock.replace(verifySvc, 'getValue', () => 'mocked'); | ||
const service = new Service(); | ||
// Check if method is replaced | ||
expect(mock.isReplaced(verifySvc, 'getValue')).toBe(true); | ||
// Replace single method | ||
mock.replace(service, 'getData', async () => ['mocked']); | ||
// Restore and verify | ||
mock.restore(verifySvc, 'getValue'); | ||
expect(mock.verifyRestored(verifySvc, 'getValue')).toBe(true); | ||
// Verify the method was replaced | ||
expect(await service.getData()).toEqual(['mocked']); | ||
// 4. Property preservation | ||
class PropService { | ||
method() { | ||
return 'original'; | ||
} | ||
} | ||
// Original processData method remains unchanged | ||
expect(service.processData).toBeDefined(); | ||
const propSvc = new PropService(); | ||
const original = propSvc.method; | ||
(original as any).customProp = 'test'; | ||
// Access call information | ||
const getDataMock = service.getData as unknown as MockFunction; | ||
expect(getDataMock.calls.count()).toBe(1); | ||
// Replace and verify custom property is preserved | ||
mock.replace(propSvc, 'method', () => 'mocked'); | ||
expect((propSvc.method as any).customProp).toBe('test'); | ||
// Restore and verify custom property is still preserved | ||
mock.restore(propSvc, 'method'); | ||
expect((propSvc.method as any).customProp).toBe('test'); | ||
expect(propSvc.method()).toBe('original'); | ||
// 5. Error handling | ||
class InvalidService { | ||
method() {} | ||
} | ||
const invalidSvc = new InvalidService(); | ||
// Attempting to restore a method that wasn't replaced | ||
const notRestored = mock.restore(invalidSvc, 'method'); | ||
expect(notRestored).toBe(false); // No methods were restored | ||
// Attempting to restore an invalid property | ||
const invalidRestored = mock.restore(invalidSvc, 'nonexistent' as any); | ||
expect(invalidRestored).toBe(false); // No methods were restored | ||
// Restore original implementation | ||
mock.restore(service); | ||
``` | ||
Key features: | ||
### Mock Control | ||
- Batch restoration support | ||
- Restoration status tracking | ||
- Property preservation | ||
- Restoration verification | ||
- Method replacement checking | ||
- Graceful error handling | ||
- Type-safe implementation | ||
- Framework compatibility | ||
All mocks provide these control methods: | ||
#### Configuration Methods | ||
The library provides several methods to configure mocking behavior: | ||
```typescript | ||
// Configuration interface | ||
interface Config { | ||
debug: boolean; // Enable debug logging (default: false in production) | ||
trackCalls: boolean; // Track method calls (default: true) | ||
allowUndefined: boolean; // Allow undefined properties (default: true) | ||
strict: boolean; // Throw on undefined properties (default: false) | ||
preservePrototype: boolean; // Preserve prototype chain (default: true) | ||
handleCircular: boolean; // Handle circular references (default: false) | ||
preserveThis: boolean; // Preserve this context (default: false) | ||
selective: boolean; // Only mock specified methods (default: false) | ||
} | ||
// Clear call history | ||
mock.mockClear(); // Clears calls but keeps implementation | ||
// Get current configuration | ||
const config = mock.getConfig(); | ||
// Reset completely | ||
mock.mockReset(); // Clears everything | ||
// Update configuration | ||
mock.setConfig({ | ||
debug: true, // Enable detailed logging | ||
trackCalls: true, // Track all method calls | ||
allowUndefined: false, // Throw on undefined properties | ||
strict: true, // Strict mode enabled | ||
preservePrototype: true, // Keep prototype chain | ||
handleCircular: true, // Handle circular references | ||
preserveThis: true, // Keep original context | ||
selective: false, // Mock all methods | ||
}); | ||
// Restore original | ||
mock.mockRestore(); // Restores original implementation | ||
// Reset to defaults | ||
mock.resetConfig(); | ||
// Scoped configuration | ||
mock.withConfig( | ||
{ | ||
debug: true, | ||
trackCalls: true, | ||
}, | ||
() => { | ||
// Configuration only applies within this function scope | ||
const mockObj = mock.object<Service>(); | ||
mockObj.method(); // Will be logged and tracked | ||
}, | ||
); | ||
// Configuration scope example with async functions | ||
await mock.withConfig( | ||
{ | ||
preserveThis: true, | ||
handleCircular: true, | ||
}, | ||
async () => { | ||
const mockService = mock.partial(realService); | ||
await mockService.asyncMethod(); | ||
}, | ||
); | ||
// Nested configuration scopes | ||
mock.withConfig({debug: true}, () => { | ||
mock.withConfig({strict: true}, () => { | ||
// Both debug and strict are true here | ||
}); | ||
// Only debug is true here | ||
}); | ||
// Get configuration statistics | ||
interface ConfigStats { | ||
configChanges: number; // Number of times config was changed | ||
activeScopes: number; // Number of active scoped configs | ||
defaultResets: number; // Number of times config was reset | ||
} | ||
const stats = mock.getConfigStats(); | ||
// Access state | ||
mock.calls.all(); // Call arguments with context | ||
mock.calls.count(); // Number of calls | ||
``` | ||
Configuration options: | ||
## Advanced Usage | ||
- `debug`: Enable debug logging (default: false in production) | ||
- `trackCalls`: Track method calls (default: true) | ||
- `allowUndefined`: Allow undefined properties (default: true) | ||
- `strict`: Throw on undefined properties (default: false) | ||
- `preservePrototype`: Preserve prototype chain (default: true) | ||
- `handleCircular`: Handle circular references (default: false) | ||
- `preserveThis`: Preserve original context (default: false) | ||
- `selective`: Only mock specified methods (default: false) | ||
### Strict Mode | ||
## Best Practices | ||
Enable strict mode for rigorous testing: | ||
### Testing Patterns | ||
1. **Clean State** | ||
- Reset mocks between tests | ||
- Use beforeEach/afterEach hooks | ||
- Avoid state leakage | ||
2. **Type Safety** | ||
- Always provide proper types | ||
- Use interface mocking when possible | ||
- Leverage TypeScript's type inference | ||
3. **Selective Mocking** | ||
- Mock only what you need | ||
- Preserve original behavior when possible | ||
- Use partial mocks for large classes | ||
### Common Pitfalls | ||
1. **Context Loss** | ||
- Use preserveThis option | ||
- Be careful with arrow functions | ||
- Maintain proper binding | ||
2. **Memory Leaks** | ||
- Reset mocks after use | ||
- Clean up spies | ||
- Handle circular references | ||
3. **Circular References** | ||
- Enable handleCircular option for objects with self-references | ||
- Use mock.partial with handleCircular for complex objects | ||
- Consider restructuring deeply nested objects | ||
- Monitor memory usage in tests with circular structures | ||
4. **Type Safety** | ||
- Enable TypeScript strict mode (strict: true in tsconfig.json) | ||
- Use explicit type parameters with mock functions | ||
- Avoid type assertions unless necessary | ||
- Leverage interface mocking for better type inference | ||
## Troubleshooting | ||
### Common Issues | ||
1. **Type Inference Issues** | ||
Problem: | ||
```typescript | ||
const mock = mock<Service>(); // Type 'any' inferred | ||
const strict = mock.obj<Service>({}, {strict: true}); | ||
strict.unknownMethod(); // Throws error | ||
``` | ||
Solution: | ||
### Async Mocking | ||
```typescript | ||
// Provide explicit type or interface | ||
interface Service { | ||
method(): void; | ||
} | ||
const mockService = mock<Service>(); | ||
``` | ||
Handle async operations: | ||
2. **This Context Lost** | ||
Problem: | ||
```typescript | ||
class Service { | ||
private value = 'test'; | ||
method() { | ||
return this.value; | ||
} | ||
} | ||
const mockService = mock.partial(new Service()); | ||
mockService.method(); // this is undefined | ||
``` | ||
Solution: | ||
```typescript | ||
const mockService = mock.partial( | ||
new Service(), | ||
const api = mock.obj<Api>( | ||
{}, | ||
{ | ||
preserveThis: true, | ||
overrides: { | ||
fetch: async url => { | ||
if (url === '/users') { | ||
return [{id: 1}]; | ||
} | ||
throw new Error('Not found'); | ||
}, | ||
}, | ||
}, | ||
@@ -1142,22 +429,31 @@ ); | ||
3. **Circular References** | ||
### Partial Mocking | ||
Problem: | ||
Selectively mock methods: | ||
```typescript | ||
interface Node { | ||
next: Node; | ||
class UserService { | ||
async getUser(id: number) { | ||
/* ... */ | ||
} | ||
async validate(user: User) { | ||
/* ... */ | ||
} | ||
} | ||
const node = {next: null}; | ||
node.next = node; | ||
``` | ||
Solution: | ||
const service = new UserService(); | ||
```typescript | ||
const mockNode = mock.partial( | ||
node, | ||
// Using replace for method replacement | ||
mock.replace(service, 'getUser', async id => ({ | ||
id, | ||
name: 'Mock User', | ||
})); | ||
// Using overrides for partial implementation | ||
const partialService = mock.obj<UserService>( | ||
{}, | ||
{ | ||
handleCircular: true, | ||
overrides: { | ||
getUser: async id => ({id, name: 'Mock User'}), | ||
}, | ||
}, | ||
@@ -1167,147 +463,63 @@ ); | ||
### Error Messages | ||
## Best Practices | ||
Common error messages and their solutions: | ||
1. **Reset Between Tests** | ||
1. **"Cannot spy on property which has no getter"** | ||
```typescript | ||
beforeEach(() => { | ||
mockFn.mockReset(); | ||
// or | ||
mockObj.mockReset(); | ||
}); | ||
``` | ||
- Ensure the property exists on the target object | ||
- Check if the property is accessible | ||
- Use proper access modifiers | ||
2. **Type Safety** | ||
2. **"Cannot mock non-function value"** | ||
```typescript | ||
// Prefer interfaces for better type inference | ||
interface Service { | ||
method(): string; | ||
} | ||
const mock = mock.obj<Service>(); | ||
``` | ||
- Verify the target is actually a function | ||
- Check if the property is a getter/setter | ||
- Ensure proper type definitions | ||
3. **Error Handling** | ||
3. **"Maximum call stack size exceeded"** | ||
```typescript | ||
// Always test error cases | ||
api.fetch.mockRejectedValue(new Error('Network error')); | ||
await expect(api.fetch()).rejects.toThrow('Network error'); | ||
``` | ||
- Check for circular references | ||
- Enable handleCircular option | ||
- Review recursive mock implementations | ||
- Consider using mock.partial with handleCircular option | ||
4. **Verification** | ||
```typescript | ||
// Verify call count and arguments | ||
expect(mockFn.calls.count()).toBe(1); | ||
expect(mockFn.calls.all()[0].args).toEqual(['expected arg']); | ||
``` | ||
4. **"Property has a circular reference"** | ||
## Troubleshooting | ||
- Enable handleCircular in mock options | ||
- Use mock.partial with handleCircular: true | ||
- Consider restructuring the object graph | ||
- Use WeakMap for circular reference tracking | ||
### Common Issues | ||
5. **"Accessing undefined property"** | ||
1. **Mock Not Tracking Calls** | ||
- Check if strict mode is enabled | ||
- Verify property exists on mock object | ||
- Consider using allowUndefined option | ||
- Add explicit property definitions | ||
- Enable `trackCalls` in config | ||
- Ensure mock is properly created | ||
6. **"Invalid spy implementation"** | ||
- Ensure mock implementation matches original signature | ||
- Check for proper this context binding | ||
- Verify async/sync function compatibility | ||
- Review method parameter types | ||
2. **Type Inference Issues** | ||
## Development | ||
- Use explicit type parameters | ||
- Define interfaces for complex types | ||
```bash | ||
# Install dependencies | ||
pnpm install | ||
3. **Prototype Chain Issues** | ||
- Enable `preservePrototype` | ||
- Use `mock.cls()` for classes | ||
# Run tests | ||
pnpm test | ||
# Build | ||
pnpm build | ||
# Lint | ||
pnpm lint | ||
# Format | ||
pnpm format | ||
# Run tests with coverage | ||
pnpm test:coverage | ||
``` | ||
For development, make sure to: | ||
1. Write tests for new features | ||
2. Update documentation for API changes | ||
3. Follow the TypeScript coding style | ||
4. Run the full test suite before submitting PR | ||
### TypeScript Configuration | ||
The library is built with strict TypeScript settings: | ||
```json | ||
{ | ||
"compilerOptions": { | ||
"strict": true, | ||
"noImplicitAny": true, | ||
"strictNullChecks": true, | ||
"strictFunctionTypes": true, | ||
"strictBindCallApply": true, | ||
"strictPropertyInitialization": true, | ||
"noImplicitThis": true, | ||
"useUnknownInCatchVariables": true, | ||
"alwaysStrict": true, | ||
"noUncheckedIndexedAccess": true, | ||
"noImplicitReturns": true, | ||
"noFallthroughCasesInSwitch": true, | ||
"exactOptionalPropertyTypes": true | ||
} | ||
} | ||
``` | ||
These strict settings ensure: | ||
- No implicit any types | ||
- Null and undefined checks | ||
- Strict function type checking | ||
- Proper this context handling | ||
- Strict property initialization | ||
- Index signature checking | ||
- Exhaustive switch/case handling | ||
## Contributing | ||
1. Fork the repository | ||
2. Create your feature branch (`git checkout -b feature/amazing-feature`) | ||
3. Commit your changes (`git commit -m 'Add some amazing feature'`) | ||
4. Push to the branch (`git push origin feature/amazing-feature`) | ||
5. Open a Pull Request at https://github.com/corezlab/mock | ||
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. | ||
Please ensure your PR: | ||
- Includes tests for new features | ||
- Updates documentation as needed | ||
- Follows the existing code style | ||
- Includes a clear description of changes | ||
### Development Guidelines | ||
1. **Code Style** | ||
- Follow TypeScript best practices | ||
- Use ESLint and Prettier | ||
- Write clear comments | ||
- Use meaningful variable names | ||
2. **Testing** | ||
- Write unit tests for new features | ||
- Maintain test coverage | ||
- Test edge cases | ||
- Use meaningful test descriptions | ||
3. **Documentation** | ||
- Update README.md as needed | ||
- Document new features | ||
- Include examples | ||
- Keep API documentation up to date | ||
## License | ||
This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details. | ||
Apache-2.0 - see [LICENSE](LICENSE) for details. |
/** | ||
* Configuration management for the mocking system | ||
* Mock configuration options | ||
*/ | ||
/** | ||
* Global configuration options | ||
*/ | ||
export interface Config { | ||
/** Whether to enable debug mode */ | ||
debug: boolean; | ||
/** Whether to track method calls by default */ | ||
trackCalls: boolean; | ||
/** Whether to allow undefined properties */ | ||
allowUndefined: boolean; | ||
/** Whether to use strict mode */ | ||
strict: boolean; | ||
/** Default timeout for async operations (ms) */ | ||
timeout: number; | ||
/** Whether to preserve prototype chain */ | ||
preservePrototype: boolean; | ||
} | ||
/** | ||
* Default configuration values | ||
*/ | ||
const defaultConfig: Config = { | ||
debug: process.env.NODE_ENV === 'development', | ||
trackCalls: true, | ||
allowUndefined: true, | ||
strict: false, | ||
timeout: 5000, | ||
preservePrototype: true, | ||
}; | ||
/** | ||
* Configuration value types | ||
*/ | ||
type ConfigValue<K extends keyof Config> = Config[K]; | ||
/** | ||
* Type guard for configuration values | ||
*/ | ||
function isValidConfigValue<K extends keyof Config>(key: K, value: unknown): value is ConfigValue<K> { | ||
switch (key) { | ||
case 'debug': | ||
case 'trackCalls': | ||
case 'allowUndefined': | ||
case 'strict': | ||
case 'preservePrototype': | ||
return typeof value === 'boolean'; | ||
case 'timeout': | ||
return typeof value === 'number' && value > 0; | ||
default: | ||
return false; | ||
} | ||
} | ||
/** | ||
* Configuration update type | ||
*/ | ||
type ConfigUpdate = Partial<Config>; | ||
/** | ||
* Configuration statistics | ||
*/ | ||
export interface ConfigStats { | ||
updateCount: number; | ||
lastUpdate: number; | ||
cacheHits: number; | ||
} | ||
/** | ||
* Global configuration state | ||
*/ | ||
class GlobalConfig { | ||
private config: Config; | ||
private cachedConfig: Readonly<Config> | null = null; | ||
private lastUpdate: number = 0; | ||
private updateCount: number = 0; | ||
private cacheHits: number = 0; | ||
private readonly cacheTimeout: number = 1000; // 1 second cache timeout | ||
private static instance: GlobalConfig; | ||
private constructor() { | ||
this.config = {...defaultConfig}; | ||
} | ||
/** | ||
* Gets the singleton instance | ||
* Whether to allow accessing undefined properties | ||
* Used in object mocking to control property access | ||
* @default false | ||
*/ | ||
static getInstance(): GlobalConfig { | ||
if (!GlobalConfig.instance) { | ||
GlobalConfig.instance = new GlobalConfig(); | ||
} | ||
return GlobalConfig.instance; | ||
} | ||
allowUndefined: boolean; | ||
/** | ||
* Gets the current configuration | ||
* Whether to use strict mode | ||
* In strict mode, adding new properties to mock objects is not allowed | ||
* @default false | ||
*/ | ||
getConfig(): Readonly<Config> { | ||
if (this.cachedConfig && Date.now() - this.lastUpdate < this.cacheTimeout) { | ||
this.cacheHits++; | ||
return this.cachedConfig; | ||
} | ||
strict: boolean; | ||
this.cachedConfig = Object.freeze({...this.config}); | ||
return this.cachedConfig; | ||
} | ||
/** | ||
* Updates the configuration | ||
* Whether to track method calls | ||
* When enabled, creates a MethodTracker to record call history | ||
* @default false | ||
*/ | ||
updateConfig(options: ConfigUpdate): void { | ||
const newConfig = {...this.config}; | ||
let hasChanges = false; | ||
trackCalls: boolean; | ||
type Key = keyof typeof options; | ||
(Object.keys(options) as Key[]).forEach(key => { | ||
if (key in defaultConfig && isValidConfigValue(key, options[key])) { | ||
if (this.config[key] !== options[key]) { | ||
(newConfig[key] as any) = options[key]; | ||
hasChanges = true; | ||
} | ||
} | ||
}); | ||
if (hasChanges) { | ||
this.config = newConfig; | ||
this.invalidateCache(); | ||
this.updateCount++; | ||
} | ||
} | ||
/** | ||
* Resets configuration to defaults | ||
* Whether to enable debug mode | ||
* When enabled, outputs debug logs for mock creation and interactions | ||
* @default false | ||
*/ | ||
resetConfig(): void { | ||
const newConfig = { | ||
...defaultConfig, | ||
debug: process.env.NODE_ENV === 'development', | ||
}; | ||
if (this.hasConfigChanged(newConfig)) { | ||
this.config = newConfig; | ||
this.invalidateCache(); | ||
this.updateCount++; | ||
} | ||
} | ||
debug: boolean; | ||
/** | ||
* Gets update statistics | ||
* Default timeout for async operations in milliseconds | ||
* Used in async mocks, promise wrappers, and retry mechanisms | ||
* @default 5000 | ||
*/ | ||
getStats(): ConfigStats { | ||
return { | ||
updateCount: this.updateCount, | ||
lastUpdate: this.lastUpdate, | ||
cacheHits: this.cacheHits, | ||
}; | ||
} | ||
timeout: number; | ||
/** | ||
* Creates a scoped configuration | ||
* Whether to preserve the prototype chain | ||
* @default true | ||
*/ | ||
withConfig<T>(options: ConfigUpdate, fn: () => T): T { | ||
const previous = {...this.getConfig()}; | ||
try { | ||
this.updateConfig(options); | ||
return fn(); | ||
} finally { | ||
this.updateConfig(previous); | ||
} | ||
} | ||
/** | ||
* Checks if configuration has changed | ||
*/ | ||
private hasConfigChanged(newConfig: Partial<Config>): boolean { | ||
return Object.entries(newConfig).some(([key, value]) => { | ||
const configKey = key as keyof Config; | ||
return this.config[configKey] !== value; | ||
}); | ||
} | ||
/** | ||
* Invalidates the configuration cache | ||
*/ | ||
private invalidateCache(): void { | ||
this.cachedConfig = null; | ||
this.lastUpdate = Date.now(); | ||
} | ||
preservePrototype: boolean; | ||
} | ||
/** | ||
* Global configuration API | ||
* Similar to Jest's global config API | ||
* Default configuration values | ||
*/ | ||
export const configManager = { | ||
/** | ||
* Gets current configuration | ||
*/ | ||
get(): Readonly<Config> { | ||
return GlobalConfig.getInstance().getConfig(); | ||
}, | ||
/** | ||
* Updates configuration | ||
*/ | ||
set(options: ConfigUpdate): void { | ||
GlobalConfig.getInstance().updateConfig(options); | ||
}, | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
reset(): void { | ||
GlobalConfig.getInstance().resetConfig(); | ||
}, | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
with<T>(options: ConfigUpdate, fn: () => T): T { | ||
return GlobalConfig.getInstance().withConfig(options, fn); | ||
}, | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
getStats(): ConfigStats { | ||
return GlobalConfig.getInstance().getStats(); | ||
}, | ||
export const DEFAULT_CONFIG: Config = { | ||
allowUndefined: false, | ||
strict: false, | ||
trackCalls: false, | ||
debug: false, | ||
timeout: 5000, | ||
preservePrototype: true, | ||
}; | ||
// For backward compatibility | ||
export const mockConfig = configManager; |
@@ -18,2 +18,3 @@ /** | ||
PERFORMANCE_CONSTRAINT_VIOLATED = 'PERF_VIOLATED', | ||
UNDEFINED_METHOD = 'UNDEFINED_METHOD', | ||
} | ||
@@ -20,0 +21,0 @@ |
@@ -1,5 +0,2 @@ | ||
export * from './config'; | ||
export * from './errors'; | ||
export * from './mock'; | ||
export * from './spy'; | ||
export * from './types'; | ||
export {createPropertySpy, createSpy} from './spy'; | ||
export type {MockFunction} from './types'; |
1142
src/mock.ts
@@ -1,1066 +0,154 @@ | ||
import {Config, configManager, ConfigStats} from './config'; | ||
import {createPropertySpy, createSpy, replaceFn} from './spy'; | ||
import {ClsMock, ClsMockOptions, DeepPartial, Fn, MockFunction, MockOf, PartialBuilder, PartialOptions} from './types'; | ||
import {Config, DEFAULT_CONFIG} from './config'; | ||
import {cast, cls, compose, fn, obj, replace} from './mocks'; | ||
import {MockRegistry} from './registry'; | ||
import { | ||
ClsMock, | ||
ClsMockOptions, | ||
Constructor, | ||
DeepPartial, | ||
Fn, | ||
Mock, | ||
MockFunction, | ||
MockObject, | ||
ObjMockOptions, | ||
} from './types'; | ||
/** | ||
* Universal mock function that provides a unified entry point for mocking objects and classes. | ||
* It automatically detects the input type and applies the appropriate mocking strategy. | ||
* | ||
* @template T -The type to mock | ||
* @param target -The target to mock (class constructor, object instance, or partial implementation) | ||
* @param options -Optional configuration for mocking behavior | ||
* @returns A mocked version of the target | ||
* @throws {TypeError} When the target type cannot be determined | ||
* @throws {Error} When circular references are detected without proper handling | ||
* | ||
* @remarks | ||
* This function serves as the main entry point for all mocking operations. | ||
* It intelligently determines the type of input and applies the most appropriate mocking strategy: | ||
* -Class mocking: Creates a mock class with spied methods | ||
* -Object mocking: Creates a proxy-based mock with tracked properties | ||
* -Partial mocking: Allows selective method/property mocking | ||
* -Interface mocking: Creates complete mock from type information | ||
* | ||
* @example | ||
* ```typescript | ||
* // Mock a class | ||
* class Service { | ||
* getData() { return 'data'; } | ||
* } | ||
* const MockService = mock(Service); | ||
* const instance = new MockService(); | ||
* | ||
* // Mock a class with options | ||
* const MockService = mock(Service, { | ||
* selective: true, | ||
* implementation: { | ||
* getData: () => 'mocked' | ||
* } | ||
* }); | ||
* | ||
* // Mock an object | ||
* interface User { | ||
* id: number; | ||
* getName(): string; | ||
* } | ||
* const mockUser = mock<User>({ | ||
* id: 1, | ||
* getName: () => 'John' | ||
* }); | ||
* | ||
* // Create a complete mock from interface | ||
* const mockUser = mock<User>(); | ||
* | ||
* // Partial mock of existing instance | ||
* const realService = new Service(); | ||
* const mockService = mock(realService, { | ||
* getData: () => 'mocked' | ||
* }); | ||
* ``` | ||
* | ||
* @see {@link mock.object} For creating complete mock objects | ||
* @see {@link mock.partial} For creating partial mocks | ||
* @see {@link mock.cls} For creating mock classes | ||
* @see {@link mock.of} For creating mocks from stubs | ||
* Mock implementation with configuration management and registry capabilities. | ||
* Provides a unified interface for creating and managing mocks with shared configuration. | ||
*/ | ||
export function mock<T extends new (...args: any[]) => any>(target: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
export function mock<T extends object>(target: T, implementation?: DeepPartial<T>): T; | ||
export function mock<T extends object>(partialImpl?: DeepPartial<T>): MockOf<T>; | ||
export function mock<T extends object>( | ||
targetOrImpl?: T | DeepPartial<T> | (new (...args: any[]) => any), | ||
optionsOrImpl?: ClsMockOptions<any> | DeepPartial<T>, | ||
): T | ClsMock<any> | MockOf<T> { | ||
// Merge configuration | ||
const mockOptions = optionsOrImpl && 'trackCalls' in optionsOrImpl ? (optionsOrImpl as ClsMockOptions<any>) : {}; | ||
const currentConfig: Config = { | ||
...mock.getConfig(), | ||
...mockOptions, | ||
}; | ||
class MockImpl extends MockRegistry implements Mock { | ||
private currentConfig: Config; | ||
if (currentConfig.debug) { | ||
console.debug('Mocking:', targetOrImpl); | ||
constructor(config: Partial<Config> = {}) { | ||
super(); | ||
this.currentConfig = {...DEFAULT_CONFIG, ...config}; | ||
} | ||
// Case 1: No arguments -create complete mock object | ||
if (!targetOrImpl) { | ||
return mock.object<T>(); | ||
} | ||
// Case 2: Target is a class constructor | ||
if (typeof targetOrImpl === 'function' && targetOrImpl.prototype) { | ||
return mock.cls(targetOrImpl as new (...args: any[]) => any, optionsOrImpl as ClsMockOptions<any>); | ||
} | ||
// Case 3: Target is an existing instance | ||
if (targetOrImpl && typeof targetOrImpl === 'object' && optionsOrImpl) { | ||
return mock.partial(targetOrImpl as T).with(optionsOrImpl as DeepPartial<T>); | ||
} | ||
// Case 4: Partial implementation provided | ||
return mock.of<T>(targetOrImpl as DeepPartial<T>); | ||
} | ||
/** | ||
* Mock utilities namespace that provides a comprehensive set of mocking tools | ||
* for testing TypeScript/JavaScript applications. | ||
* | ||
* @namespace mock | ||
* @description | ||
* A comprehensive mocking framework that provides tools for: | ||
* -Function mocking with spy capabilities | ||
* -Object mocking with property tracking | ||
* -Class mocking with inheritance support | ||
* -Partial mocking for selective member overrides | ||
* -Framework compatibility (Jest/Jasmine) | ||
* | ||
* The namespace provides a unified API for all mocking operations while maintaining | ||
* type safety and providing detailed tracking and verification capabilities. | ||
*/ | ||
export namespace mock { | ||
/** | ||
* Creates a spy function that can track calls and mock implementations. | ||
* The spy function maintains the same type signature as the original function. | ||
* | ||
* @template T -The function type to mock | ||
* @returns {MockFunction<T>} A mock function with spy capabilities | ||
* | ||
* @remarks | ||
* Spy functions provide comprehensive tracking and control over function calls: | ||
* -Call count tracking | ||
* -Arguments validation | ||
* -Return value mocking | ||
* -Implementation replacement | ||
* -Asynchronous behavior simulation | ||
* | ||
* The spy maintains the original function's type information while adding | ||
* additional methods for tracking and controlling behavior. | ||
* | ||
* @example | ||
* ```typescript | ||
* // Create a typed spy | ||
* const mockFn = mock.fn<(x: number) => string>(); | ||
* | ||
* // Configure behavior | ||
* mockFn.mockReturnValue('mocked'); | ||
* mockFn.mockImplementation(x => x.toString()); | ||
* | ||
* // Use in tests | ||
* mockFn(42); | ||
* expect(mockFn).toHaveBeenCalledWith(42); | ||
* expect(mockFn.calls.count()).toBe(1); | ||
* ``` | ||
* | ||
* @see {@link MockFunction} For the complete spy API | ||
* Get or set mock configuration | ||
* @param config Optional configuration to update | ||
* @returns Current configuration when called without arguments | ||
*/ | ||
export function fn<T extends Fn = Fn>(): MockFunction<T> { | ||
return createSpy<T>(); | ||
} | ||
/** | ||
* Creates a fully mocked object where all properties and methods are spies. | ||
* The mock object maintains the same type information as the original. | ||
* | ||
* @template T -The object type to mock | ||
* @returns {T} A mock object with all members as spies | ||
* | ||
* @remarks | ||
* Object mocking provides comprehensive control over object behavior: | ||
* -Automatic spy creation for methods | ||
* -Property value tracking | ||
* -Getter/setter interception | ||
* -Nested object handling | ||
* -Circular reference protection | ||
* | ||
* The mock maintains the original object's type information while adding | ||
* spy capabilities to all methods and tracking for all properties. | ||
* | ||
* Features: | ||
* -Auto-creates spies for all methods | ||
* -Supports getters and setters | ||
* -Maintains property descriptors | ||
* -Handles nested objects | ||
* -Preserves type safety | ||
* | ||
* @example | ||
* ```typescript | ||
* interface Service { | ||
* getData(): Promise<string>; | ||
* value: number; | ||
* } | ||
* | ||
* const mockService = mock.object<Service>(); | ||
* mockService.getData.mockResolvedValue('data'); | ||
* mockService.value = 42; | ||
* | ||
* await mockService.getData(); | ||
* expect(mockService.getData).toHaveBeenCalled(); | ||
* ``` | ||
* | ||
* @see {@link mock.partial} For creating partial mocks | ||
* @see {@link mock.of} For creating mocks from stubs | ||
*/ | ||
export function object<T extends object>(): T { | ||
const target = {} as T; | ||
const cache = new Map<string | symbol, any>(); | ||
const descriptors = new Map<string | symbol, PropertyDescriptor>(); | ||
const refs = new WeakMap(); | ||
const config = mock.getConfig(); | ||
const handler: ProxyHandler<T> = { | ||
get: (_, prop: string | symbol) => { | ||
if (prop === 'then') { | ||
return undefined; | ||
} | ||
const descriptor = descriptors.get(prop); | ||
if (descriptor) { | ||
return descriptor.get?.(); | ||
} | ||
if (!cache.has(prop)) { | ||
if (!config.allowUndefined && !prop.toString().startsWith('__')) { | ||
if (config.strict) { | ||
throw new Error(`Accessing undefined property: ${String(prop)}`); | ||
} | ||
if (config.debug) { | ||
console.warn(`Accessing undefined property: ${String(prop)}`); | ||
} | ||
} | ||
const spy = createSpy(); | ||
if (config.trackCalls) { | ||
spy.mockImplementation(function (...args: any[]) { | ||
if (config.debug) { | ||
console.debug(`Called ${String(prop)} with:`, args); | ||
} | ||
return undefined; | ||
}); | ||
} | ||
cache.set(prop, spy); | ||
return spy; | ||
} | ||
return cache.get(prop); | ||
}, | ||
set: (_, prop: string | symbol, value: any) => { | ||
// If value is an object, check for circular references | ||
if (value && typeof value === 'object') { | ||
if (refs.has(value)) { | ||
// If a reference already exists, use the original value directly | ||
cache.set(prop, value); | ||
return true; | ||
} | ||
refs.set(value, true); | ||
} | ||
cache.delete(prop); | ||
cache.set(prop, value); | ||
return true; | ||
}, | ||
defineProperty: (_, prop: string | symbol, descriptor: PropertyDescriptor) => { | ||
descriptors.set(prop, { | ||
...descriptor, | ||
configurable: true, | ||
enumerable: true, | ||
}); | ||
return true; | ||
}, | ||
getOwnPropertyDescriptor: (_, prop: string | symbol) => { | ||
const descriptor = descriptors.get(prop); | ||
if (descriptor) { | ||
return { | ||
...descriptor, | ||
configurable: true, | ||
enumerable: true, | ||
}; | ||
} | ||
if (cache.has(prop)) { | ||
return { | ||
value: cache.get(prop), | ||
writable: true, | ||
configurable: true, | ||
enumerable: true, | ||
}; | ||
} | ||
return undefined; | ||
}, | ||
has: (_, prop: string | symbol) => { | ||
return descriptors.has(prop) || cache.has(prop); | ||
}, | ||
ownKeys: () => { | ||
return [...descriptors.keys(), ...cache.keys()]; | ||
}, | ||
}; | ||
return new Proxy(target, handler); | ||
} | ||
/** | ||
* Creates a partial mock from an existing object, allowing selective mocking of members. | ||
* This is useful when you want to mock only specific parts of an object while keeping | ||
* the rest of the functionality intact. | ||
* | ||
* @template T -The object type to partially mock | ||
* @param base -The original object to create a partial mock from | ||
* @param stubs -Optional stubs to initialize the mock with | ||
* @param options -Configuration options for the partial mock | ||
* @returns {T | PartialBuilder<T>} A mock object with partial mocking or a builder for further configuration | ||
* | ||
* @remarks | ||
* Partial mocking provides fine-grained control over which members to mock: | ||
* -Selective method mocking | ||
* -Original behavior preservation | ||
* -Prototype chain maintenance | ||
* -Nested object support | ||
* -Circular reference handling | ||
* | ||
* Features: | ||
* -Selective member mocking | ||
* -Preserves original behavior | ||
* -Maintains prototype chain | ||
* -Supports nested objects | ||
* -Type-safe implementation | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* getData() { return 'real data'; } | ||
* process() { return this.getData().toUpperCase(); } | ||
* } | ||
* | ||
* // Method 1: Using with() | ||
* const mockService1 = mock.partial(new Service()).with({ | ||
* getData: () => 'mock data' | ||
* }); | ||
* | ||
* // Method 2: Direct two parameters | ||
* const mockService2 = mock.partial(new Service(), { | ||
* getData: () => 'mock data' | ||
* }); | ||
* | ||
* expect(mockService1.process()).toBe('MOCK DATA'); | ||
* expect(mockService2.process()).toBe('MOCK DATA'); | ||
* ``` | ||
* | ||
* @throws {Error} When circular references are detected without proper handling | ||
* @see {@link PartialBuilder} For the builder pattern API | ||
* @see {@link PartialOptions} For available configuration options | ||
*/ | ||
export function partial<T extends object>(base: T, stubs: DeepPartial<T>, options?: PartialOptions<T>): T; | ||
export function partial<T extends object>(base: T): PartialBuilder<T>; | ||
export function partial<T extends object>( | ||
base: T, | ||
stubs?: DeepPartial<T>, | ||
options?: PartialOptions<T>, | ||
): T | PartialBuilder<T> { | ||
const config = mock.getConfig(); | ||
const defaultOptions: Required<PartialOptions<T>> = { | ||
selective: false, | ||
preserveThis: config.preservePrototype, | ||
autoSpy: config.trackCalls, | ||
handleCircular: false, | ||
}; | ||
const spyMethods = new Set<keyof T>(); | ||
const preserveProps = new Set<keyof T>(); | ||
function createMock(withStubs: DeepPartial<T> = {} as DeepPartial<T>, withOptions: PartialOptions<T> = {}): T { | ||
const finalOptions = {...defaultOptions, ...withOptions}; | ||
let currentKey = ''; | ||
if (config.debug) { | ||
console.debug('Creating partial mock for:', base.constructor.name); | ||
} | ||
// Create a new object with the same prototype chain | ||
const result = Object.create(finalOptions.preserveThis ? Object.getPrototypeOf(base) : null) as T; | ||
// First pass: copy all properties to handle circular references | ||
Object.getOwnPropertyNames(base).forEach(key => { | ||
const typedKey = key as keyof T; | ||
if (!Object.prototype.hasOwnProperty.call(withStubs, key) && !spyMethods.has(typedKey)) { | ||
if (preserveProps.has(typedKey)) { | ||
result[typedKey] = base[typedKey]; | ||
} else { | ||
const descriptor = Object.getOwnPropertyDescriptor(base, key)!; | ||
if (typeof descriptor.value === 'function' && !finalOptions.selective) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(descriptor.value.bind(result)); | ||
result[typedKey] = spy as any; | ||
} else { | ||
Object.defineProperty(result, key, descriptor); | ||
} | ||
} | ||
} | ||
}); | ||
// Second pass: apply stubs and handle spies | ||
try { | ||
if (finalOptions.handleCircular) { | ||
const merged = {...base, ...withStubs} as T; | ||
Object.assign(result, merged); | ||
// Handle circular references | ||
for (const key in merged) { | ||
const value = (merged as any)[key]; | ||
if (value === base) { | ||
(result as any)[key] = result; | ||
} | ||
} | ||
} else { | ||
Object.keys(withStubs).forEach(key => { | ||
const typedKey = key as keyof T; | ||
if (preserveProps.has(typedKey)) { | ||
result[typedKey] = base[typedKey]; | ||
return; | ||
} | ||
const value = (withStubs as any)[key]; | ||
if (typeof value === 'function') { | ||
const spy = createSpy(); | ||
spy.mockImplementation(finalOptions.preserveThis ? value.bind(result) : value); | ||
result[typedKey] = spy as any; | ||
} else if (value && typeof value === 'object' && !Array.isArray(value)) { | ||
// Check for circular references | ||
if (value === base && !finalOptions.handleCircular) { | ||
throw new Error(`Property ${String(typedKey)} has a circular reference`); | ||
} | ||
try { | ||
result[typedKey] = {...(base as any)[typedKey], ...value}; | ||
} catch (e) { | ||
if (e instanceof RangeError) { | ||
throw new Error( | ||
`Property ${String(typedKey)} has a circular reference.\nConsider using handleCircular: true option.`, | ||
); | ||
} | ||
throw e; | ||
} | ||
} else { | ||
result[typedKey] = value; | ||
} | ||
}); | ||
} | ||
} catch (e) { | ||
if (e instanceof RangeError && finalOptions.handleCircular) { | ||
return mock.cast<T>({...base, ...withStubs}); | ||
} | ||
throw e; | ||
} | ||
// Third pass: handle additional spy methods | ||
spyMethods.forEach(method => { | ||
if (typeof result[method] === 'function' && !preserveProps.has(method)) { | ||
const original = result[method] as Function; | ||
const spy = createSpy(); | ||
spy.mockImplementation(finalOptions.preserveThis ? original.bind(result) : original); | ||
result[method] = spy as any; | ||
} | ||
}); | ||
return result; | ||
configure(): Config; | ||
configure(config: Partial<Config>): void; | ||
configure(config?: Partial<Config>): Config | void { | ||
if (!config) { | ||
return {...this.currentConfig}; | ||
} | ||
const builder: PartialBuilder<T> = { | ||
with: (withStubs?: DeepPartial<T>, withOptions?: PartialOptions<T>) => createMock(withStubs, withOptions), | ||
spy: (...methods: (keyof T)[]) => { | ||
methods.forEach(method => spyMethods.add(method)); | ||
return builder; | ||
}, | ||
preserve: (...properties: (keyof T)[]) => { | ||
properties.forEach(prop => preserveProps.add(prop)); | ||
return builder; | ||
}, | ||
}; | ||
return stubs ? createMock(stubs, options) : builder; | ||
Object.assign(this.currentConfig, config); | ||
} | ||
/** | ||
* Casts a partial object to its full type, useful for type coercion in mocks | ||
* @template T -The target type to cast to | ||
* @param partial -The partial object to cast | ||
* @returns The partial object cast to type T | ||
* @example | ||
* ```typescript | ||
* interface Complex { | ||
* nested: { value: number }; | ||
* } | ||
* const partial = { nested: { value: 42 } }; | ||
* const typed = mock.cast<Complex>(partial); | ||
* ``` | ||
* Create a mock function with tracking capabilities | ||
* @param options Optional configuration overrides | ||
* @returns Mocked function with spy features | ||
*/ | ||
export function cast<T extends object>(partial: DeepPartial<T>): T { | ||
const proxies = new WeakMap(); // Used to cache created proxy objects | ||
const handler: ProxyHandler<any> = { | ||
get: (target, prop) => { | ||
if (prop === 'then') { | ||
return undefined; | ||
} | ||
if (prop in target) { | ||
const value = target[prop]; | ||
if (value && typeof value === 'object') { | ||
// If a proxy has been created for this object, return the cached proxy directly. | ||
if (proxies.has(value)) { | ||
return proxies.get(value); | ||
} | ||
// Create new proxy and cache | ||
const proxy = new Proxy(value, handler); | ||
proxies.set(value, proxy); | ||
return proxy; | ||
} | ||
return value; | ||
} | ||
return undefined; | ||
}, | ||
}; | ||
const proxy = new Proxy(partial, handler); | ||
if (partial && typeof partial === 'object') { | ||
proxies.set(partial, proxy); | ||
} | ||
return proxy as T; | ||
fn<T extends Fn>(options?: Partial<Config>): MockFunction<T> { | ||
const mockFn = fn<T>(); | ||
this.registerMock(mockFn, 'function'); | ||
return mockFn; | ||
} | ||
/** | ||
* Creates a mock from stubs, supporting both object and array types. | ||
* Preserves provided values while automatically mocking undefined members. | ||
* | ||
* @template T -The type to create a mock from | ||
* @param stubs -Optional stubs to initialize the mock with | ||
* @returns {MockOf<T>} A mock object or array with all methods as MockFunction | ||
* | ||
* @remarks | ||
* Mock creation from stubs provides flexible initialization: | ||
* -Value preservation | ||
* -Automatic method mocking | ||
* -Array support | ||
* -Nested object handling | ||
* -Type inference | ||
* | ||
* Features: | ||
* -Preserves provided property values | ||
* -Preserves and monitors provided method implementations | ||
* -Automatically mocks undefined methods | ||
* -Handles nested objects and arrays | ||
* -Maintains type safety | ||
* | ||
* @example | ||
* ```typescript | ||
* interface User { | ||
* id: number; | ||
* name: string; | ||
* getData(): string; | ||
* } | ||
* | ||
* // With initial values | ||
* const mockUser = mock.of<User>({ | ||
* id: 1, | ||
* name: 'John' | ||
* }); | ||
* // id and name are preserved | ||
* // getData is automatically mocked | ||
* mockUser.getData.mockReturnValue('data'); | ||
* | ||
* // With method implementation | ||
* const mockWithMethod = mock.of<User>({ | ||
* id: 1, | ||
* getData: () => 'data' | ||
* }); | ||
* // getData implementation is preserved but monitored | ||
* mockWithMethod.getData.mockReturnValue('new data'); | ||
* ``` | ||
* | ||
* @throws {Error} When circular references are detected | ||
* @see {@link MockOf} For the resulting mock type | ||
* Create a mock object with tracking capabilities | ||
* @param target Original object to mock | ||
* @param options Mock configuration and implementation | ||
* @returns Mocked object with spy features | ||
*/ | ||
export function of<T extends Array<any>>(stubs?: Array<DeepPartial<T[number]>>): MockOf<T>; | ||
export function of<T extends ReadonlyArray<any>>(stubs?: ReadonlyArray<DeepPartial<T[number]>>): MockOf<T>; | ||
export function of<T extends object>(stubs?: DeepPartial<T>): MockOf<T>; | ||
export function of<T extends object>(stubs: any = {} as T): MockOf<T> { | ||
if (Array.isArray(stubs)) { | ||
return stubs.map(item => of(item)) as MockOf<T>; | ||
} | ||
const base = object<T>(); | ||
const result = Object.create(Object.getPrototypeOf(base)); | ||
const cache = new Map<string | symbol, any>(); | ||
// Copy properties from stubs | ||
Object.entries(stubs).forEach(([key, value]) => { | ||
result[key] = value; | ||
}); | ||
return new Proxy(result, { | ||
get(target, prop) { | ||
if (prop === 'then') return undefined; | ||
// Check cache first | ||
if (cache.has(prop)) { | ||
return cache.get(prop); | ||
} | ||
// Get value from target | ||
const value = target[prop]; | ||
// If it's a function, create a spy | ||
if (typeof value === 'function') { | ||
const spy = createSpy(); | ||
spy.mockImplementation(value); | ||
cache.set(prop, spy); | ||
return spy; | ||
} | ||
// Return existing value or create spy for undefined methods | ||
if (value !== undefined) { | ||
cache.set(prop, value); | ||
return value; | ||
} | ||
const spy = createSpy(); | ||
cache.set(prop, spy); | ||
return spy; | ||
}, | ||
}) as MockOf<T>; | ||
obj<T extends object>(target: T | undefined, options: ObjMockOptions<T> = {}): MockObject<T> { | ||
const mockObj = obj(target, {...this.currentConfig, ...options}); | ||
this.registerMock(mockObj, 'object', target); | ||
return mockObj; | ||
} | ||
/** | ||
* Copies function properties from source to target | ||
* @internal | ||
* Create a mock class with tracking capabilities | ||
* @param target Original class to mock | ||
* @param options Mock configuration and implementation | ||
* @returns Mocked class constructor | ||
*/ | ||
function copyFunctionProperties(source: any, target: any): void { | ||
if (typeof source === 'function') { | ||
Object.getOwnPropertyNames(source).forEach(prop => { | ||
if (prop !== 'name' && prop !== 'length' && prop !== 'prototype') { | ||
target[prop] = source[prop]; | ||
} | ||
}); | ||
} | ||
cls<T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>): ClsMock<T> { | ||
const mockCls = cls(target, {...this.currentConfig, ...options}); | ||
this.registerMock(mockCls as unknown as T, 'class', target); | ||
return mockCls; | ||
} | ||
const ORIGINAL_PREFIX = '__original_'; | ||
const EXCLUDED_PROPS = ['name', 'length', 'prototype', '_isMockFunction']; | ||
/** | ||
* Replaces a method with mock implementation while preserving original properties | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to replace | ||
* @param obj -The object containing the method | ||
* @param key -The key of the method to replace | ||
* @param impl -The mock implementation | ||
* @example | ||
* ```typescript | ||
* const obj = { method: () => 'original' }; | ||
* mock.replace(obj, 'method', () => 'mocked'); | ||
* ``` | ||
* Create a mock from partial implementation | ||
* @param partial Partial implementation to mock | ||
* @param options Mock configuration | ||
* @returns Complete mock object | ||
*/ | ||
export function replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn): void { | ||
if (!obj || typeof key !== 'string') return; | ||
const methodKey = String(key); | ||
const originalKey = `${ORIGINAL_PREFIX}${methodKey}`; | ||
const original = obj[key]; | ||
// Store original method for restoration | ||
if (!(originalKey in obj)) { | ||
(obj as any)[originalKey] = original; | ||
} | ||
// Create spy with implementation | ||
const spy = createSpy(); | ||
spy.mockImplementation(impl); | ||
// Copy properties from original function to spy | ||
if (typeof original === 'function') { | ||
Object.getOwnPropertyNames(original).forEach(prop => { | ||
if (!EXCLUDED_PROPS.includes(prop)) { | ||
(spy as any)[prop] = (original as any)[prop]; | ||
} | ||
}); | ||
} | ||
// Replace the method with spy | ||
obj[key] = spy as any; | ||
cast<T extends object>(partial: DeepPartial<T>, options?: Partial<Config>): T { | ||
const mockObj = cast(partial, {...this.currentConfig, ...options}); | ||
this.registerMock(mockObj, 'object', partial as T); | ||
return mockObj; | ||
} | ||
/** | ||
* Restores replaced methods to their original implementations | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to restore | ||
* @param obj -The object containing the method | ||
* @param key -Optional key of the specific method to restore | ||
* @returns {boolean} Whether any methods were restored | ||
* @example | ||
* ```typescript | ||
* // Restore specific method | ||
* mock.restore(service, 'method'); | ||
* | ||
* // Restore all replaced methods | ||
* mock.restore(service); | ||
* ``` | ||
* Create a composed mock from target | ||
* @param target Class or object to compose mock from | ||
* @param options Mock configuration and implementation | ||
*/ | ||
export function restore<T extends object>(obj: T): boolean; | ||
export function restore<T extends object, K extends keyof T>(obj: T, key: K): boolean; | ||
export function restore<T extends object, K extends keyof T>(obj: T, key?: K): boolean { | ||
if (!obj) return false; | ||
// Helper to restore a single method | ||
const restoreMethod = (methodKey: string): boolean => { | ||
const originalKey = `${ORIGINAL_PREFIX}${methodKey}`; | ||
if (!(originalKey in obj)) return false; | ||
const original = (obj as any)[originalKey]; | ||
const current = obj[methodKey as keyof T]; | ||
// Copy any added properties from current implementation to original | ||
if (current && typeof current === 'function') { | ||
Object.getOwnPropertyNames(current).forEach(prop => { | ||
if (!EXCLUDED_PROPS.includes(prop)) { | ||
try { | ||
(original as any)[prop] = (current as any)[prop]; | ||
} catch (e) { | ||
// Ignore property copy errors | ||
} | ||
} | ||
}); | ||
} | ||
// Restore original method | ||
try { | ||
obj[methodKey as keyof T] = original; | ||
delete (obj as any)[originalKey]; | ||
return true; | ||
} catch (e) { | ||
return false; | ||
} | ||
}; | ||
// Case 1: Restore specific method | ||
if (key !== undefined) { | ||
return restoreMethod(String(key)); | ||
} | ||
// Case 2: Restore all replaced methods | ||
let anyRestored = false; | ||
const keys = Object.getOwnPropertyNames(obj); | ||
for (const k of keys) { | ||
if (k.startsWith(ORIGINAL_PREFIX)) { | ||
const methodKey = k.slice(ORIGINAL_PREFIX.length); | ||
if (restoreMethod(methodKey)) { | ||
anyRestored = true; | ||
} | ||
} | ||
} | ||
return anyRestored; | ||
compose<T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
compose<T extends object>(target: T, options?: {overrides?: DeepPartial<T>} & Partial<Config>): T; | ||
compose<T extends object>(target: T | Constructor<T>, options?: any): any { | ||
const mockObj = compose(target, {...this.currentConfig, ...options}); | ||
this.registerMock(mockObj, typeof target === 'function' ? 'class' : 'object', target); | ||
return mockObj; | ||
} | ||
/** | ||
* Gets a list of currently replaced methods on an object | ||
* @template T -The object type to check | ||
* @param obj -The object to check | ||
* @returns {string[]} Array of replaced method names | ||
* Replace a method with mock implementation | ||
* @param obj Target object | ||
* @param key Method key to replace | ||
* @param impl Mock implementation | ||
* @param options Mock configuration | ||
*/ | ||
export function getReplacedMethods<T extends object>(obj: T): string[] { | ||
if (!obj) return []; | ||
return Object.getOwnPropertyNames(obj) | ||
.filter(k => k.startsWith(ORIGINAL_PREFIX)) | ||
.map(k => k.slice(ORIGINAL_PREFIX.length)) | ||
.filter(k => k && typeof obj[k as keyof T] !== 'undefined'); | ||
replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn, options?: Partial<Config>): void { | ||
replace(obj, key, impl, {...this.currentConfig, ...options}); | ||
} | ||
} | ||
/** | ||
* Verifies if a method has been replaced | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to check | ||
* @param obj -The object containing the method | ||
* @param key -The key of the method to check | ||
* @returns {boolean} Whether the method is currently replaced | ||
*/ | ||
export function isReplaced<T extends object, K extends keyof T>(obj: T, key: K): boolean { | ||
if (!obj) return false; | ||
const originalKey = `${ORIGINAL_PREFIX}${String(key)}`; | ||
const original = (obj as any)[originalKey]; | ||
const current = obj[key]; | ||
return originalKey in obj && current !== original; | ||
} | ||
/** | ||
* Mock function type that combines Mock interface with configuration capability | ||
*/ | ||
type MockFn = Mock & ((config?: Partial<Config>) => Mock); | ||
/** | ||
* Verifies if a restored method matches its original implementation | ||
* @template T -The object type containing the method | ||
* @template K -The key of the method to verify | ||
* @param obj -The object containing the method | ||
* @param key -The key of the method to verify | ||
* @returns {boolean} Whether the current implementation matches the original | ||
*/ | ||
export function verifyRestored<T extends object, K extends keyof T>(obj: T, key: K): boolean { | ||
if (!obj) return false; | ||
const originalKey = `${ORIGINAL_PREFIX}${String(key)}`; | ||
const original = (obj as any)[originalKey]; | ||
// Create a global mock instance for shared configuration | ||
const globalMock = new MockImpl(); | ||
// If no original stored, consider it restored | ||
if (!original) return true; | ||
const current = obj[key]; | ||
// Direct reference comparison | ||
if (current === original) return true; | ||
// Function implementation comparison | ||
if (typeof current === 'function' && typeof original === 'function') { | ||
try { | ||
// Compare function bodies | ||
const currentStr = current.toString().replace(/\s+/g, ''); | ||
const originalStr = original.toString().replace(/\s+/g, ''); | ||
return currentStr === originalStr; | ||
} catch { | ||
// If comparison fails, fall back to reference equality | ||
return current === original; | ||
/** | ||
* Creates a new mock instance with optional configuration. | ||
* Provides a unified interface for creating and managing mocks. | ||
* | ||
* @example | ||
* // Create a mock with default configuration | ||
* const defaultMock = mock(); | ||
* | ||
* // Create a mock with custom configuration | ||
* const customMock = mock({ strict: true }); | ||
*/ | ||
export const mock: MockFn = Object.assign( | ||
function mock(config?: Partial<Config>): Mock { | ||
return new MockImpl(config); | ||
}, | ||
{ | ||
fn: <T extends Fn>(options?: Partial<Config>) => globalMock.fn<T>(options), | ||
obj: <T extends object>(target: T | undefined, options: ObjMockOptions<T> = {}) => globalMock.obj(target, options), | ||
cls: <T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>) => globalMock.cls(target, options), | ||
cast: <T extends object>(partial: DeepPartial<T>, options?: Partial<Config>) => globalMock.cast(partial, options), | ||
replace: <T extends object, K extends keyof T>(obj: T, key: K, impl: Fn, options?: Partial<Config>) => | ||
globalMock.replace(obj, key, impl, options), | ||
compose: <T extends object>(target: T | Constructor<T>, options?: any) => globalMock.compose(target, options), | ||
configure: function (config?: Partial<Config>): Config | void { | ||
if (!config) { | ||
return globalMock.configure(); | ||
} | ||
} | ||
globalMock.configure(config); | ||
}, | ||
}, | ||
) as MockFn; | ||
return false; | ||
} | ||
/** | ||
* Handles static method inheritance and mocking for class mocks | ||
*/ | ||
function handleStaticMethodInheritance(mockClass: any, originalClass: any): void { | ||
let currentProto = Object.getPrototypeOf(originalClass); | ||
while (currentProto && currentProto !== Function.prototype) { | ||
Object.getOwnPropertyNames(currentProto) | ||
.filter(prop => typeof currentProto[prop] === 'function') | ||
.forEach(methodName => { | ||
if (!mockClass[methodName]) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(currentProto[methodName].bind(mockClass)); | ||
mockClass[methodName] = spy; | ||
} | ||
}); | ||
currentProto = Object.getPrototypeOf(currentProto); | ||
} | ||
} | ||
/** | ||
* Creates a mock class that maintains the original class's type information and behavior. | ||
* Supports both full monitoring and selective method mocking. | ||
* | ||
* @template T -The class type to mock | ||
* @param originalClass -The original class to create a mock from | ||
* @param options -Configuration options for class mocking | ||
* @returns {ClsMock<T>} A mock class with the same interface as the original | ||
* | ||
* @remarks | ||
* Class mocking provides comprehensive control over class behavior: | ||
* -Constructor interception | ||
* -Method spying | ||
* -Property tracking | ||
* -Static member handling | ||
* -Inheritance support | ||
* | ||
* Features: | ||
* -Maintains original class structure | ||
* -Supports constructor arguments | ||
* -Handles static members | ||
* -Preserves inheritance chain | ||
* -Allows selective mocking | ||
* | ||
* @example | ||
* ```typescript | ||
* // Monitor all methods | ||
* const MockService = mock.cls(DataService); | ||
* | ||
* // Selective monitoring with custom implementation | ||
* const MockService = mock.cls(DataService, { | ||
* selective: true, | ||
* implementation: { | ||
* getData: async () => 'mocked' | ||
* } | ||
* }); | ||
* ``` | ||
* | ||
* @throws {Error} When constructor initialization fails | ||
* @see {@link ClsMockOptions} For available configuration options | ||
* @see {@link ClsMock} For the resulting mock class type | ||
*/ | ||
export function cls<T extends new (...args: any[]) => any>( | ||
originalClass: T, | ||
options: ClsMockOptions<T> = {}, | ||
): ClsMock<T> { | ||
const config = mock.getConfig(); | ||
const {selective = false, implementation = {}} = options; | ||
if (config.debug) { | ||
console.debug('Creating class mock for:', originalClass.name); | ||
} | ||
// Helper to create method spy | ||
const createMethodSpy = (method: Function, context: any) => { | ||
const spy = createSpy(); | ||
if (config.trackCalls) { | ||
spy.mockImplementation((...args: any[]) => { | ||
if (config.debug) { | ||
console.debug(`Called ${method.name} with:`, args); | ||
} | ||
return method.apply(context, args); | ||
}); | ||
} else { | ||
spy.mockImplementation((...args: any[]) => method.apply(context, args)); | ||
} | ||
return spy; | ||
}; | ||
// Helper to handle property descriptor | ||
const handleDescriptor = ( | ||
name: string | symbol, | ||
descriptor: PropertyDescriptor, | ||
context: any, | ||
): PropertyDescriptor => { | ||
// Handle getters/setters | ||
if (descriptor.get || descriptor.set) { | ||
const spies = createPropertySpy(); | ||
const implDescriptor = | ||
typeof name === 'string' ? Object.getOwnPropertyDescriptor(implementation, name) : undefined; | ||
return { | ||
configurable: true, | ||
enumerable: true, | ||
get: | ||
descriptor.get && | ||
(implDescriptor?.get | ||
? spies.get?.mockImplementation(implDescriptor.get) | ||
: !selective | ||
? spies.get?.mockImplementation(descriptor.get.bind(context)) | ||
: descriptor.get.bind(context)), | ||
set: | ||
descriptor.set && | ||
(implDescriptor?.set | ||
? spies.set?.mockImplementation(implDescriptor.set) | ||
: !selective | ||
? spies.set?.mockImplementation(descriptor.set.bind(context)) | ||
: descriptor.set.bind(context)), | ||
}; | ||
} | ||
// Handle methods | ||
if (typeof descriptor.value === 'function') { | ||
const impl = typeof name === 'string' ? (implementation as any)[name] : undefined; | ||
if (impl) { | ||
const spy = createSpy(); | ||
spy.mockImplementation(impl); | ||
return {...descriptor, value: spy}; | ||
} | ||
if (!selective) { | ||
return { | ||
...descriptor, | ||
value: createMethodSpy(descriptor.value, context), | ||
}; | ||
} | ||
} | ||
return { | ||
...descriptor, | ||
value: descriptor.value, | ||
}; | ||
}; | ||
// Create mock constructor | ||
function MockClass(this: any, ...args: any[]) { | ||
// Handle constructor call without new | ||
if (!(this instanceof MockClass)) { | ||
return new (MockClass as any)(...args); | ||
} | ||
// Create instance with proper prototype | ||
const instance = Object.create(originalClass.prototype); | ||
try { | ||
// Initialize instance with constructor | ||
const temp = new originalClass(...args); | ||
Object.assign(instance, temp); | ||
} catch { | ||
// Silently continue if constructor fails | ||
} | ||
// Process instance properties and methods | ||
const processMembers = (target: any, source: any) => { | ||
Object.getOwnPropertyNames(source).forEach(name => { | ||
if (name === 'constructor') return; | ||
if (!target.hasOwnProperty(name)) { | ||
const descriptor = Object.getOwnPropertyDescriptor(source, name); | ||
if (descriptor) { | ||
Object.defineProperty(target, name, handleDescriptor(name, descriptor, instance)); | ||
} | ||
} | ||
}); | ||
}; | ||
// Handle instance members | ||
processMembers(instance, originalClass.prototype); | ||
// Handle inherited members | ||
let proto = Object.getPrototypeOf(originalClass.prototype); | ||
while (proto && proto !== Object.prototype) { | ||
processMembers(instance, proto); | ||
proto = Object.getPrototypeOf(proto); | ||
} | ||
Object.setPrototypeOf(instance, MockClass.prototype); | ||
return instance; | ||
} | ||
// Set up prototype chain | ||
MockClass.prototype = Object.create(originalClass.prototype); | ||
MockClass.prototype.constructor = MockClass; | ||
// Handle static members | ||
Object.getOwnPropertyNames(originalClass).forEach(name => { | ||
if (name === 'length' || name === 'prototype' || name === 'name') return; | ||
const descriptor = Object.getOwnPropertyDescriptor(originalClass, name); | ||
if (descriptor) { | ||
Object.defineProperty(MockClass, name, handleDescriptor(name, descriptor, originalClass)); | ||
} | ||
}); | ||
// Handle static methods inheritance | ||
handleStaticMethodInheritance(MockClass, originalClass); | ||
return MockClass as unknown as ClsMock<T>; | ||
} | ||
/** | ||
* Gets current configuration | ||
*/ | ||
export function getConfig(): Readonly<Config> { | ||
return configManager.get(); | ||
} | ||
/** | ||
* Updates configuration | ||
*/ | ||
export function setConfig(options: Partial<Config>): void { | ||
configManager.set(options); | ||
} | ||
/** | ||
* Resets configuration to defaults | ||
*/ | ||
export function resetConfig(): void { | ||
configManager.reset(); | ||
} | ||
/** | ||
* Creates a scoped configuration | ||
*/ | ||
export function withConfig<T>(options: Partial<Config>, fn: () => T): T { | ||
return configManager.with(options, fn); | ||
} | ||
/** | ||
* Gets configuration statistics | ||
*/ | ||
export function getConfigStats(): ConfigStats { | ||
return configManager.getStats(); | ||
} | ||
} | ||
export default mock; |
246
src/spy.ts
@@ -10,17 +10,2 @@ import {Fn, MockFunction} from './types'; | ||
* @template T - The type of function being spied on | ||
* | ||
* @example | ||
* ```typescript | ||
* // Create a spy for a simple function | ||
* const spy = new UniversalSpy<() => string>(); | ||
* const spyFn = spy.getSpy(); | ||
* | ||
* // Configure spy behavior | ||
* spyFn.mockReturnValue('mocked'); | ||
* | ||
* // Use the spy | ||
* const result = spyFn(); | ||
* expect(result).toBe('mocked'); | ||
* expect(spyFn).toHaveBeenCalled(); | ||
* ``` | ||
*/ | ||
@@ -50,3 +35,3 @@ export class UniversalSpy<T extends Fn = Fn> { | ||
} else if (this.implementation) { | ||
result = this.implementation.apply(spy._this || this, args); | ||
result = this.implementation.apply(spy._this, args); | ||
} else { | ||
@@ -56,9 +41,19 @@ result = undefined as unknown as ReturnType<T>; | ||
// Update mock property | ||
spy.mock.calls.push(args); | ||
spy.mock.results.push({type: 'return', value: result}); | ||
if (spy._this) { | ||
spy.mock.instances.push(spy._this); | ||
spy.mock.contexts.push(spy._this); | ||
} | ||
finishCall(undefined, result); | ||
return result; | ||
} catch (error) { | ||
// Update mock property for errors | ||
spy.mock.results.push({type: 'throw', value: error}); | ||
finishCall(error instanceof Error ? error : new Error(String(error))); | ||
throw error; | ||
} | ||
}) as unknown as MockFunction<T>; | ||
}) as MockFunction<T>; | ||
@@ -81,24 +76,33 @@ // Add Jest mock marker | ||
// Bind the spy to maintain this context | ||
const boundSpy = new Proxy(spy, { | ||
apply: (target, thisArg, argumentsList) => { | ||
spy._this = thisArg; | ||
const result = target.apply(thisArg, argumentsList); | ||
spy._this = null; | ||
return result; | ||
// Add calls property for tracking | ||
spy.calls = { | ||
all: () => this.tracker.getCallsFor('spy').map(call => ({args: call.args as Parameters<T>})), | ||
count: () => this.tracker.getCallsFor('spy').length, | ||
}; | ||
// Add mock property | ||
Object.defineProperty(spy, 'mock', { | ||
value: { | ||
calls: [] as Parameters<T>[], | ||
results: [] as Array<{type: 'return' | 'throw'; value: ReturnType<T>}>, | ||
instances: [] as any[], | ||
contexts: [] as any[], | ||
}, | ||
writable: true, | ||
configurable: true, | ||
enumerable: true, | ||
}); | ||
// Add Jest-like methods | ||
boundSpy.mockReturnValue = (value: ReturnType<T>) => { | ||
spy.mockReturnValue = (value: ReturnType<T>) => { | ||
this.updateState({returnValue: value}); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockResolvedValue = (value: any) => { | ||
spy.mockResolvedValue = (value: any) => { | ||
this.updateState({returnValue: Promise.resolve(value) as unknown as ReturnType<T>}); | ||
return boundSpy as any; | ||
return spy as any; | ||
}; | ||
boundSpy.mockImplementation = (fn: T) => { | ||
spy.mockImplementation = (fn: T) => { | ||
if (fn !== null && fn !== undefined && typeof fn !== 'function') { | ||
@@ -108,6 +112,6 @@ throw new Error('Mock implementation must be a function'); | ||
this.updateState({implementation: fn}); | ||
return boundSpy; | ||
return spy; | ||
}; | ||
boundSpy.mockReset = () => { | ||
spy.mockReset = () => { | ||
this.tracker.reset(); | ||
@@ -117,35 +121,31 @@ this.updateState({}); | ||
boundSpy.mockClear = () => { | ||
spy.mockClear = () => { | ||
this.tracker.reset(); | ||
}; | ||
boundSpy.calls = { | ||
all: () => this.tracker.getCallsFor('spy').map(call => ({args: call.args as Parameters<T>})), | ||
count: () => this.tracker.getCallsFor('spy').length, | ||
}; | ||
// Add Jasmine-like methods | ||
boundSpy.and = { | ||
returnValue: (value: ReturnType<T>) => { | ||
boundSpy.mockReturnValue(value); | ||
return boundSpy; | ||
}, | ||
callFake: (fn: T) => { | ||
boundSpy.mockImplementation(fn); | ||
return boundSpy; | ||
}, | ||
const jasmineApi = { | ||
returnValue: spy.mockReturnValue, | ||
callFake: spy.mockImplementation, | ||
throwError: (error: any) => { | ||
this.updateState({error}); | ||
return boundSpy; | ||
return spy; | ||
}, | ||
stub: () => { | ||
boundSpy.mockImplementation(undefined as any); | ||
return boundSpy; | ||
spy.mockReset(); | ||
return spy; | ||
}, | ||
}; | ||
Object.defineProperty(spy, 'and', { | ||
value: jasmineApi, | ||
writable: false, | ||
enumerable: true, | ||
configurable: true, | ||
}); | ||
// Add common assertions | ||
boundSpy.toHaveBeenCalled = () => this.tracker.getCallsFor('spy').length > 0; | ||
boundSpy.toHaveBeenCalledTimes = (n: number) => this.tracker.getCallsFor('spy').length === n; | ||
boundSpy.toHaveBeenCalledWith = (...args: Parameters<T>) => | ||
spy.toHaveBeenCalled = () => this.tracker.getCallsFor('spy').length > 0; | ||
spy.toHaveBeenCalledTimes = (n: number) => this.tracker.getCallsFor('spy').length === n; | ||
spy.toHaveBeenCalledWith = (...args: Parameters<T>) => | ||
this.tracker | ||
@@ -155,2 +155,24 @@ .getCallsFor('spy') | ||
// Bind the spy to maintain this context | ||
const boundSpy = new Proxy(spy, { | ||
apply: (target, thisArg, argumentsList) => { | ||
spy._this = thisArg; | ||
const result = target.apply(thisArg, argumentsList); | ||
spy._this = null; | ||
return result; | ||
}, | ||
get: (target, prop: keyof MockFunction<T>) => { | ||
const value = target[prop]; | ||
if (typeof value === 'function') { | ||
return function (this: any, ...args: any[]) { | ||
spy._this = this; | ||
const result = value.apply(this, args); | ||
spy._this = null; | ||
return result; | ||
}; | ||
} | ||
return value; | ||
}, | ||
}); | ||
this.spyFunction = boundSpy; | ||
@@ -191,20 +213,4 @@ } | ||
* Creates a new spy function with optional implementation. | ||
* The spy function tracks all calls and provides methods for verifying behavior. | ||
* | ||
* @template T - The type of function to spy on | ||
* @param implementation - Optional implementation for the spy function | ||
* @returns A mock function that tracks calls and provides verification methods | ||
* | ||
* @example | ||
* ```typescript | ||
* // Create a spy with no implementation | ||
* const spy = createSpy<(name: string) => string>(); | ||
* | ||
* // Create a spy with implementation | ||
* const spy = createSpy((name: string) => `Hello ${name}`); | ||
* | ||
* // Use the spy | ||
* spy('John'); | ||
* expect(spy).toHaveBeenCalledWith('John'); | ||
* ``` | ||
* @param implementation - Optional implementation for the spy | ||
*/ | ||
@@ -217,99 +223,13 @@ export function createSpy<T extends Fn>(implementation?: T): MockFunction<T> { | ||
/** | ||
* Replaces a method on an object with a spy while preserving the original implementation. | ||
* This is useful for monitoring method calls while maintaining the original behavior. | ||
* | ||
* @template T - The type of object containing the method | ||
* @param obj - The object containing the method to spy on | ||
* @param key - The key of the method to spy on | ||
* @param fn - The function to use as implementation | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* getData() { return 'data'; } | ||
* } | ||
* | ||
* const service = new Service(); | ||
* replaceFn(service, 'getData', () => 'mock data'); | ||
* | ||
* service.getData(); // Returns 'mock data' | ||
* expect(service.getData).toHaveBeenCalled(); | ||
* ``` | ||
* Creates a spy for a property with getter and/or setter. | ||
* @template T - The type of the property | ||
*/ | ||
export function replaceFn<T extends object>(obj: T, key: keyof T, fn: Fn): void { | ||
const spy = createSpy(fn); | ||
spy.mockImplementation(fn); | ||
Object.defineProperty(obj, key, { | ||
value: spy, | ||
writable: true, | ||
configurable: true, | ||
}); | ||
} | ||
/** | ||
* Creates a spy for an existing method on an object. | ||
* The spy will track all calls while maintaining the original implementation. | ||
* | ||
* @template T - The type of object containing the method | ||
* @param obj - The object containing the method to spy on | ||
* @param key - The key of the method to spy on | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* getData() { return 'data'; } | ||
* } | ||
* | ||
* const service = new Service(); | ||
* spyOn(service, 'getData'); | ||
* | ||
* service.getData(); // Returns 'data' | ||
* expect(service.getData).toHaveBeenCalled(); | ||
* ``` | ||
*/ | ||
export function spyOn<T extends object>(obj: T, key: keyof T): void { | ||
const value = obj[key]; | ||
if (typeof value === 'function') { | ||
const spy = createSpy(value as Fn); | ||
spy.mockImplementation(value as Fn); | ||
Object.defineProperty(obj, key, { | ||
value: spy, | ||
writable: true, | ||
configurable: true, | ||
}); | ||
} | ||
} | ||
/** | ||
* Creates spies for getter/setter properties. | ||
* This is useful for monitoring property access and modifications. | ||
* | ||
* @template T - The type of the property value | ||
* @returns An object containing spy functions for get and set operations | ||
* | ||
* @example | ||
* ```typescript | ||
* class Service { | ||
* private _value: string = ''; | ||
* get value() { return this._value; } | ||
* set value(v: string) { this._value = v; } | ||
* } | ||
* | ||
* const service = new Service(); | ||
* const spy = createPropertySpy<string>(); | ||
* | ||
* Object.defineProperty(service, 'value', { | ||
* get: spy.get, | ||
* set: spy.set | ||
* }); | ||
* | ||
* service.value = 'test'; | ||
* expect(spy.set).toHaveBeenCalledWith('test'); | ||
* ``` | ||
*/ | ||
export function createPropertySpy<T>(): {get?: MockFunction; set?: MockFunction} { | ||
export function createPropertySpy<T>(): { | ||
get?: MockFunction<() => T>; | ||
set?: MockFunction<(value: T) => void>; | ||
} { | ||
return { | ||
get: createSpy(), | ||
set: createSpy(), | ||
get: createSpy<() => T>(), | ||
set: createSpy<(value: T) => void>(), | ||
}; | ||
} |
@@ -0,1 +1,3 @@ | ||
import type {Config} from '../config'; | ||
/** | ||
@@ -24,2 +26,49 @@ * Basic function type | ||
/** | ||
* Object mock builder interface that provides fluent API for building mock objects | ||
*/ | ||
export interface ObjectBuilder<T> { | ||
/** | ||
* Apply stubs and options to create a new mock object | ||
* @param stubs Optional partial implementation | ||
* @param options Optional configuration options | ||
*/ | ||
with(stubs?: DeepPartial<T>, options?: ObjectOptions<T>): T; | ||
/** | ||
* Add spy functionality to specific methods | ||
* @param methods Methods to spy on | ||
*/ | ||
spy(...methods: (keyof T)[]): ObjectBuilder<T>; | ||
/** | ||
* Preserve original implementation of specific properties | ||
* @param properties Properties to preserve | ||
*/ | ||
preserve(...properties: (keyof T)[]): ObjectBuilder<T>; | ||
} | ||
/** | ||
* Object mock options for configuring mock behavior | ||
*/ | ||
export interface ObjectOptions<T> { | ||
/** Whether to automatically create spies for methods */ | ||
autoSpy?: boolean; | ||
/** Whether to handle circular references */ | ||
handleCircular?: boolean; | ||
/** Properties to exclude from mocking */ | ||
exclude?: Array<keyof T>; | ||
/** Whether to include private properties */ | ||
includePrivate?: boolean; | ||
/** Whether to include inherited properties */ | ||
includeInherited?: boolean; | ||
/** Track method calls */ | ||
trackCalls?: boolean; | ||
} | ||
/** | ||
* Combined options type for object mocking | ||
*/ | ||
export type ObjectMockOptions<T> = ObjectOptions<T> & DeepPartial<T> & Partial<Config>; | ||
/** | ||
* Recursive partial type with function handling | ||
@@ -26,0 +75,0 @@ */ |
@@ -0,1 +1,3 @@ | ||
import type {Config} from '../config'; | ||
import {cast, cls, compose, fn, obj, replace} from '../mocks'; | ||
import type { | ||
@@ -24,32 +26,38 @@ AsyncFn, | ||
/** | ||
* Base mock function interface | ||
* Mock function type with Jest-like chaining methods | ||
*/ | ||
interface BaseMockFunction<T extends Fn = Fn> extends Function { | ||
export interface MockFunction<T extends Fn = Fn> extends Function { | ||
(...args: Parameters<T>): ReturnType<T>; | ||
_isMockFunction: boolean; | ||
_this?: any; | ||
mock: { | ||
calls: Parameters<T>[]; | ||
results: Array<{type: 'return' | 'throw'; value: any}>; | ||
instances: any[]; | ||
contexts: any[]; | ||
lastCall?: Parameters<T>; | ||
}; | ||
calls: { | ||
all(): Array<{args: Parameters<T>}>; | ||
count(): number; | ||
all: () => Array<{args: Parameters<T>}>; | ||
count: () => number; | ||
}; | ||
_isMockFunction?: boolean; | ||
_this?: any; | ||
} | ||
/** | ||
* Mock function type with Jest-like chaining methods | ||
*/ | ||
export interface MockFunction<T extends Fn = Fn> extends BaseMockFunction<T> { | ||
// Mock behavior | ||
mockReturnValue(value: ReturnType<T>): this; | ||
mockResolvedValue<U>(value: U): MockFunction<AsyncFn & {(...args: Parameters<T>): Promise<U>}>; | ||
mockImplementation(fn: T): this; | ||
mockReset(): void; | ||
mockClear(): void; | ||
// Jasmine-style methods | ||
mockImplementation: (fn: T) => MockFunction<T>; | ||
mockReturnValue: (value: ReturnType<T>) => MockFunction<T>; | ||
mockResolvedValue: <U>(value: U) => MockFunction<T>; | ||
mockRejectedValue: <U>(value: U) => MockFunction<T>; | ||
mockReset: () => void; | ||
mockClear: () => void; | ||
mockRestore: () => void; | ||
getMockImplementation: () => T | undefined; | ||
getMockName: () => string; | ||
and: MockBehavior<T>; | ||
// Common assertions | ||
toHaveBeenCalled(): boolean; | ||
toHaveBeenCalledTimes(n: number): boolean; | ||
toHaveBeenCalledWith(...args: Parameters<T>): boolean; | ||
toHaveBeenCalled: () => boolean; | ||
toHaveBeenCalledTimes: (n: number) => boolean; | ||
toHaveBeenCalledWith: (...args: Parameters<T>) => boolean; | ||
mockName: (name: string) => MockFunction<T>; | ||
mockReturnThis: () => MockFunction<T>; | ||
mockResolvedValueOnce: <U>(value: U) => MockFunction<T>; | ||
mockRejectedValueOnce: <U>(value: U) => MockFunction<T>; | ||
mockReturnValueOnce: (value: ReturnType<T>) => MockFunction<T>; | ||
mockImplementationOnce: (fn: T) => MockFunction<T>; | ||
} | ||
@@ -60,16 +68,3 @@ | ||
*/ | ||
export interface JestMock<T extends Fn = Fn> extends BaseMockFunction<T> { | ||
mock: { | ||
calls: Parameters<T>[]; | ||
results: Array<{ | ||
type: 'return' | 'throw'; | ||
value: any; | ||
}>; | ||
}; | ||
mockReturnValue(value: ReturnType<T>): this; | ||
mockResolvedValue<U>(value: U): this; | ||
mockImplementation(fn: T): this; | ||
mockReset(): void; | ||
mockClear(): void; | ||
} | ||
export interface JestMock<T extends Fn = Fn> extends MockFunction<T> {} | ||
@@ -79,5 +74,3 @@ /** | ||
*/ | ||
export interface JasmineSpy<T extends Fn = Fn> extends BaseMockFunction<T> { | ||
and: MockBehavior<T>; | ||
} | ||
export interface JasmineSpy<T extends Fn = Fn> extends MockFunction<T> {} | ||
@@ -89,3 +82,2 @@ /** | ||
createSpy<T extends Fn = Fn>(): MockFunction<T>; | ||
spyOn<T extends object>(obj: T, key: keyof T): void; | ||
} | ||
@@ -153,4 +145,2 @@ | ||
preserveConstructor?: boolean; | ||
selective?: boolean; | ||
preserveThis?: boolean; | ||
autoSpy?: boolean; | ||
@@ -161,2 +151,23 @@ handleCircular?: boolean; | ||
/** | ||
* Base mock options interface | ||
*/ | ||
interface BaseMockOptions extends Partial<Config> { | ||
overrides?: DeepPartial<any>; | ||
} | ||
/** | ||
* Options for configuring object mocks | ||
*/ | ||
export interface ObjMockOptions<T> extends BaseMockOptions { | ||
overrides?: DeepPartial<T>; | ||
} | ||
/** | ||
* Options for configuring class mocks | ||
*/ | ||
export interface ClsMockOptions<T> extends BaseMockOptions { | ||
overrides?: DeepPartial<T>; | ||
} | ||
/** | ||
* Mock class options | ||
@@ -225,27 +236,95 @@ */ | ||
*/ | ||
export type ClsMock<T extends new (...args: any[]) => any> = StaticMockOf<T> & { | ||
export type ClsMock<T extends new (...args: any[]) => any> = { | ||
new (...args: ConstructorParameters<T>): MockOf<InstanceType<T>>; | ||
prototype: MockOf<InstanceType<T>>; | ||
} & { | ||
[K in keyof T]: T[K] extends (...args: any[]) => any ? MockFunction<T[K]> : T[K]; | ||
}; | ||
/** | ||
* Options for configuring class mocks | ||
* Partial mock options | ||
*/ | ||
export interface ClsMockOptions<T extends new (...args: any[]) => any> extends Pick<MockOptions, 'selective'> { | ||
implementation?: DeepPartial<InstanceType<T>>; | ||
export interface PartialOptions<T> extends Pick<MockOptions, 'autoSpy' | 'handleCircular'> { | ||
overrides?: DeepPartial<T>; | ||
} | ||
/** | ||
* Partial mock options | ||
* Builder interface for partial mocks | ||
*/ | ||
export interface PartialOptions<T> | ||
extends Pick<MockOptions, 'selective' | 'preserveThis' | 'autoSpy' | 'handleCircular'> {} | ||
export interface PartialBuilder<T extends object> { | ||
/** | ||
* Apply stubs to create the final mock object | ||
* @param stubs Optional partial implementation | ||
* @param options Optional configuration | ||
*/ | ||
with(stubs?: DeepPartial<T>, options?: PartialOptions<T> & Partial<Config>): T; | ||
/** | ||
* Add spy to specific methods | ||
* @param methods Methods to spy on | ||
*/ | ||
spy(...methods: (keyof T)[]): PartialBuilder<T>; | ||
/** | ||
* Preserve original implementation of properties | ||
* @param properties Properties to preserve | ||
*/ | ||
preserve(...properties: (keyof T)[]): PartialBuilder<T>; | ||
} | ||
/** | ||
* Partial mock builder interface | ||
* Instance mock interface for mocking objects and classes | ||
*/ | ||
export interface PartialBuilder<T extends object> { | ||
with(stubs?: DeepPartial<T>, options?: PartialOptions<T>): T; | ||
spy(...methods: (keyof T)[]): this; | ||
preserve(...properties: (keyof T)[]): this; | ||
export interface Mock { | ||
/** | ||
* Gets or sets mock configuration | ||
* @param config - Optional configuration to set | ||
* @returns Current configuration if no arguments, void if setting configuration | ||
*/ | ||
configure(): Config; | ||
configure(config: Partial<Config>): void; | ||
/** | ||
* Creates a mock function | ||
*/ | ||
fn<T extends Fn>(options?: Partial<Config>): MockFunction<T>; | ||
/** | ||
* Creates a mock object | ||
*/ | ||
obj<T extends object>(target: T | undefined, options?: ObjMockOptions<T>): MockObject<T>; | ||
/** | ||
* Creates a mock class with optional implementation | ||
*/ | ||
cls<T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
/** | ||
* Casts a partial implementation to a complete mock | ||
*/ | ||
cast<T extends object>(partial: DeepPartial<T>, options?: Partial<Config>): T; | ||
/** | ||
* Replaces a method with mock implementation | ||
*/ | ||
replace<T extends object, K extends keyof T>(obj: T, key: K, impl: Fn, options?: Partial<Config>): void; | ||
/** | ||
* Creates a mock from a class constructor | ||
*/ | ||
compose<T extends Constructor<any>>(target: T, options?: ClsMockOptions<T>): ClsMock<T>; | ||
compose<T extends object>(target: T, options?: {overrides?: DeepPartial<T>} & Partial<Config>): T; | ||
} | ||
/** | ||
* Mock object interface that includes utility methods | ||
*/ | ||
export type MockObject<T extends object> = T & { | ||
mockClear(): void; | ||
mockReset(): void; | ||
mockRestore(): void; | ||
mockImplementation(implementation: DeepPartial<T>): MockObject<T>; | ||
mockReturnValue(value: any): MockObject<T>; | ||
mockResolvedValue(value: any): MockObject<T>; | ||
mockRejectedValue(value: any): MockObject<T>; | ||
}; |
@@ -1,2 +0,2 @@ | ||
import {MockError, MockErrorCode} from '../../errors'; | ||
import {DEFAULT_CONFIG} from '../../config'; | ||
import {delay, makeAsync, mockReject, mockResolve, retry, withTimeout} from '../async'; | ||
@@ -51,2 +51,47 @@ | ||
}); | ||
it('should handle errors with custom error factory', async () => { | ||
const customError = new Error('Custom error'); | ||
const fn = () => 'result'; | ||
const asyncFn = makeAsync(fn, { | ||
errorRate: 1, // Always throw error | ||
errorFactory: () => customError, | ||
}); | ||
await expect(asyncFn()).rejects.toBe(customError); | ||
}); | ||
it('should respect timeout option with delay', async () => { | ||
const fn = () => 'result'; | ||
const asyncFn = makeAsync(fn, { | ||
delay: 2000, | ||
timeout: 100, | ||
}); | ||
const promise = asyncFn(); | ||
jest.advanceTimersByTime(100); | ||
await expect(promise).rejects.toThrow('Async operation timed out'); | ||
}); | ||
it('should preserve timing when specified', async () => { | ||
const fn = () => 'result'; | ||
const asyncFn = makeAsync(fn, { | ||
delay: 100, | ||
preserveTiming: true, | ||
}); | ||
const promise = asyncFn(); | ||
jest.advanceTimersByTime(100); | ||
await promise; | ||
}); | ||
it('should handle function that throws synchronously', async () => { | ||
const error = new Error('Sync error'); | ||
const fn = () => { | ||
throw error; | ||
}; | ||
const asyncFn = makeAsync(fn); | ||
await expect(asyncFn()).rejects.toBe(error); | ||
}); | ||
}); | ||
@@ -80,2 +125,12 @@ | ||
}); | ||
it('should use default timeout from config', async () => { | ||
const defaultTimeout = DEFAULT_CONFIG.timeout; | ||
const slowPromise = new Promise(resolve => setTimeout(resolve, defaultTimeout + 100)); | ||
const timeoutPromise = withTimeout(slowPromise); | ||
jest.advanceTimersByTime(defaultTimeout + 50); | ||
await expect(timeoutPromise).rejects.toThrow('Operation timed out'); | ||
}); | ||
}); | ||
@@ -116,8 +171,7 @@ | ||
afterEach(async () => { | ||
await jest.runAllTimersAsync(); | ||
await Promise.resolve(); | ||
jest.clearAllTimers(); | ||
jest.useRealTimers(); | ||
}); | ||
it('should retry failed operations', async () => { | ||
it('should retry failed operations with exponential backoff', async () => { | ||
let attempts = 0; | ||
@@ -132,8 +186,19 @@ const operation = jest.fn().mockImplementation(async () => { | ||
const resultPromise = retry(operation, 3, 100); | ||
const promise = retry(operation, { | ||
maxAttempts: 3, | ||
baseDelay: 100, | ||
}); | ||
// Fast-forward until all timers have been executed | ||
await jest.runAllTimersAsync(); | ||
// Initial call | ||
expect(operation).toHaveBeenCalledTimes(1); | ||
const result = await resultPromise; | ||
// First retry (after 100ms) | ||
await jest.advanceTimersByTimeAsync(100); | ||
expect(operation).toHaveBeenCalledTimes(2); | ||
// Second retry (after 200ms more) | ||
await jest.advanceTimersByTimeAsync(200); | ||
expect(operation).toHaveBeenCalledTimes(3); | ||
const result = await promise; | ||
expect(result).toBe('success'); | ||
@@ -143,34 +208,42 @@ expect(attempts).toBe(3); | ||
it('should use exponential backoff', async () => { | ||
const operation = jest.fn().mockRejectedValue(new Error('Failure')); | ||
const resultPromise = retry(operation, 3, 100); | ||
it('should fail after max attempts', async () => { | ||
jest.useRealTimers(); | ||
const operation = jest.fn().mockRejectedValue(new Error('Persistent failure')); | ||
// Initial call executes immediately | ||
expect(operation).toHaveBeenCalledTimes(1); | ||
const promise = retry(operation, { | ||
maxAttempts: 2, | ||
baseDelay: 10, // shorter delay | ||
}); | ||
// First retry (after 100ms) | ||
await jest.advanceTimersByTimeAsync(99); | ||
expect(operation).toHaveBeenCalledTimes(1); | ||
await jest.advanceTimersByTimeAsync(1); | ||
await expect(promise).rejects.toThrow(/Persistent failure \(after 2 attempts\)/); | ||
expect(operation).toHaveBeenCalledTimes(2); | ||
}); | ||
// Second retry (after 200ms) | ||
await jest.advanceTimersByTimeAsync(199); | ||
// TODO: Fix this test. It's not stable enough to check for timeout | ||
it.skip('should respect timeout option', async () => { | ||
jest.useRealTimers(); | ||
const operation = jest.fn().mockRejectedValue(new Error('Failure')); | ||
const promise = retry(operation, { | ||
maxAttempts: 3, | ||
baseDelay: 100, | ||
timeout: 200, | ||
}); | ||
await expect(promise).rejects.toThrow('Operation timed out'); | ||
expect(operation).toHaveBeenCalledTimes(2); | ||
}); | ||
// For the final retry, we must use synchronous timer advancement | ||
// This is due to how Jest handles Promise microtasks: | ||
// 1. advanceTimersByTimeAsync immediately processes all microtasks after timer advancement | ||
// 2. This causes the final MockError to be treated as an unhandled rejection | ||
// 3. Using synchronous advanceTimersByTime + Promise.resolve() gives us control over | ||
// when the Promise microtasks are processed | ||
// 4. This ensures the error is properly caught by our expect assertion | ||
// rather than being caught by Jest as an unhandled rejection | ||
jest.advanceTimersByTime(1); | ||
await Promise.resolve(); | ||
expect(operation).toHaveBeenCalledTimes(3); | ||
it('should succeed on first attempt', async () => { | ||
const operation = jest.fn().mockResolvedValue('immediate success'); | ||
await expect(resultPromise).rejects.toThrow('Failure (after 3 attempts)'); | ||
const promise = retry(operation, { | ||
maxAttempts: 3, | ||
baseDelay: 100, | ||
}); | ||
const result = await promise; | ||
expect(result).toBe('immediate success'); | ||
expect(operation).toHaveBeenCalledTimes(1); | ||
}); | ||
}); | ||
}); |
@@ -51,16 +51,2 @@ import {MockError} from '../../errors'; | ||
it('should handle undefined methods based on options', () => { | ||
const mock = createChainable(target, {allowUndefined: false}); | ||
expect(() => { | ||
(mock as any).undefinedMethod(); | ||
}).toThrow(MockError); | ||
}); | ||
it('should allow undefined methods when configured', () => { | ||
const mock = createChainable(target, {allowUndefined: true}); | ||
expect(() => { | ||
(mock as any).undefinedMethod(); | ||
}).not.toThrow(); | ||
}); | ||
it('should cache method wrappers', () => { | ||
@@ -67,0 +53,0 @@ const mock = createChainable(target); |
@@ -5,5 +5,7 @@ /** | ||
import {mockConfig} from '../config'; | ||
import {MockError, MockErrorCode} from '../errors'; | ||
// Default timeout value in milliseconds | ||
const DEFAULT_TIMEOUT = 5000; | ||
/** | ||
@@ -34,3 +36,3 @@ * Async method options | ||
delay = 0, | ||
timeout = mockConfig.get().timeout, | ||
timeout = DEFAULT_TIMEOUT, | ||
errorRate = 0, | ||
@@ -43,4 +45,5 @@ errorFactory = () => new Error('Async operation failed'), | ||
// Create timeout promise | ||
let timeoutId: NodeJS.Timeout | undefined; | ||
const timeoutPromise = new Promise<never>((_, reject) => { | ||
setTimeout(() => { | ||
timeoutId = setTimeout(() => { | ||
reject(new Error('Async operation timed out')); | ||
@@ -51,2 +54,3 @@ }, timeout); | ||
// Create main operation promise | ||
let delayId: NodeJS.Timeout | undefined; | ||
const operationPromise = new Promise<ReturnType<T>>((resolve, reject) => { | ||
@@ -69,3 +73,3 @@ const execute = () => { | ||
if (delay > 0) { | ||
setTimeout(execute, delay); | ||
delayId = setTimeout(execute, delay); | ||
} else { | ||
@@ -76,4 +80,10 @@ execute(); | ||
// Race between timeout and operation | ||
return Promise.race([operationPromise, timeoutPromise]); | ||
try { | ||
// Race between timeout and operation | ||
return await Promise.race([operationPromise, timeoutPromise]); | ||
} finally { | ||
// Clean up timers | ||
if (timeoutId) clearTimeout(timeoutId); | ||
if (delayId) clearTimeout(delayId); | ||
} | ||
}; | ||
@@ -92,5 +102,6 @@ } | ||
*/ | ||
export function withTimeout<T>(promise: Promise<T>, ms: number = mockConfig.get().timeout): Promise<T> { | ||
export function withTimeout<T>(promise: Promise<T>, ms: number = DEFAULT_TIMEOUT): Promise<T> { | ||
let timeoutId: NodeJS.Timeout | undefined; | ||
const timeoutPromise = new Promise<never>((_, reject) => { | ||
setTimeout(() => { | ||
timeoutId = setTimeout(() => { | ||
reject(new Error('Operation timed out')); | ||
@@ -100,3 +111,10 @@ }, ms); | ||
return Promise.race([promise, timeoutPromise]); | ||
try { | ||
return Promise.race([promise, timeoutPromise]).finally(() => { | ||
if (timeoutId) clearTimeout(timeoutId); | ||
}); | ||
} catch (error) { | ||
if (timeoutId) clearTimeout(timeoutId); | ||
throw error; | ||
} | ||
} | ||
@@ -120,27 +138,77 @@ | ||
/** | ||
* Retries an async operation with exponential backoff | ||
* Retry options for configuring retry behavior | ||
*/ | ||
export async function retry<T>( | ||
operation: () => Promise<T>, | ||
maxAttempts: number = 3, | ||
baseDelay: number = 1000, | ||
): Promise<T> { | ||
export interface RetryOptions { | ||
/** Maximum number of attempts */ | ||
maxAttempts?: number; | ||
/** Base delay between retries in ms */ | ||
baseDelay?: number; | ||
/** Maximum total time for all retries in ms */ | ||
timeout?: number; | ||
/** Whether to include attempt count in error messages */ | ||
includeAttemptCount?: boolean; | ||
} | ||
/** | ||
* Retries an async operation with exponential backoff and timeout | ||
*/ | ||
export async function retry<T>(operation: () => Promise<T>, options: RetryOptions = {}): Promise<T> { | ||
const {maxAttempts = 3, baseDelay = 1000, timeout = DEFAULT_TIMEOUT, includeAttemptCount = true} = options; | ||
let lastError: Error | undefined; | ||
let attempts = 0; | ||
const startTime = Date.now(); | ||
for (let attempt = 1; attempt <= maxAttempts; attempt++) { | ||
try { | ||
return await operation(); | ||
} catch (error) { | ||
lastError = error as Error; | ||
if (attempt < maxAttempts) { | ||
const delay = baseDelay * Math.pow(2, attempt - 1); | ||
await new Promise(resolve => setTimeout(resolve, delay)); | ||
} | ||
} | ||
} | ||
// Create cleanup function for timeouts | ||
const timeouts: NodeJS.Timeout[] = []; | ||
const cleanup = () => { | ||
timeouts.forEach(clearTimeout); | ||
timeouts.length = 0; | ||
}; | ||
if (lastError) { | ||
lastError.message = `${lastError.message} (after ${maxAttempts} attempts)`; | ||
// Ensure cleanup on process exit | ||
const cleanupHandler = () => cleanup(); | ||
process.on('beforeExit', cleanupHandler); | ||
try { | ||
return await withTimeout( | ||
(async () => { | ||
while (attempts < maxAttempts) { | ||
attempts++; | ||
try { | ||
return await operation(); | ||
} catch (error) { | ||
lastError = error instanceof Error ? error : new Error(String(error)); | ||
if (attempts < maxAttempts) { | ||
const elapsedTime = Date.now() - startTime; | ||
const remainingTime = timeout - elapsedTime; | ||
// Check if we have enough time for next retry | ||
if (remainingTime <= 0) { | ||
throw new Error('Operation timed out'); | ||
} | ||
const delayTime = Math.min(baseDelay * Math.pow(2, attempts - 1), remainingTime); | ||
await new Promise<void>(resolve => { | ||
const timeoutId = setTimeout(resolve, delayTime); | ||
timeouts.push(timeoutId); | ||
}); | ||
} | ||
} | ||
} | ||
const finalError = lastError || new Error('Operation failed'); | ||
if (includeAttemptCount) { | ||
finalError.message = `${finalError.message} (after ${maxAttempts} attempts)`; | ||
} | ||
throw finalError; | ||
})(), | ||
timeout, | ||
); | ||
} finally { | ||
cleanup(); | ||
process.off('beforeExit', cleanupHandler); | ||
} | ||
throw lastError || new Error('Operation failed'); | ||
} |
@@ -14,4 +14,2 @@ /** | ||
trackCalls?: boolean; | ||
/** Whether to allow undefined methods */ | ||
allowUndefined?: boolean; | ||
/** Custom method implementation */ | ||
@@ -34,3 +32,3 @@ implementation?: Record<string, Function>; | ||
export function createChainable<T extends object>(target: T, options: ChainableOptions = {}): T { | ||
const {trackCalls = true, allowUndefined = true, implementation = {}} = options; | ||
const {trackCalls = true, implementation = {}} = options; | ||
@@ -100,7 +98,2 @@ const tracker = trackCalls ? new MethodTracker() : undefined; | ||
// Handle undefined methods | ||
if (!allowUndefined && value === undefined) { | ||
throw MockError.invalidImplementation(`Method ${String(prop)} is not implemented`); | ||
} | ||
// Return chainable function for undefined methods | ||
@@ -107,0 +100,0 @@ const chainableMethod = (...args: any[]) => { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
18
47
292533
6159
520
1