@@ -27,6 +27,12 @@ interface GlobalConstructors {
2727 RegExp : typeof RegExp ;
2828}
2929
30+ interface PropertySnapshot {
31+ props : Key [ ] ;
32+ descriptors : Map < Key , PropertyDescriptor > ;
33+ isModule : boolean ;
34+ }
35+
3036/**
3137 * Get the type name of a value using Object.prototype.toString
3238 */
@@ -52,6 +58,10 @@ function isFunction(value: unknown): value is (...args: any[]) => any {
5258 return typeof value === 'function' ;
5359}
5460
61+ function isModuleObject ( value : Record < Key , any > ) : boolean {
62+ return getTypeName ( value ) === 'Module' || value . __esModule ;
63+ }
64+
5565/**
5666 * Check if a property is a built-in readonly property (e.g., function's name, length)
5767 */
@@ -149,13 +159,36 @@ function getEnumerableProperties(
149159 return [ ...props ] ;
150160}
151161
162+ function getPropertyDescriptor (
163+ obj : any ,
164+ prop : Key ,
165+ ) : PropertyDescriptor | undefined {
166+ let current = obj ;
167+ const visited = new WeakSet ( ) ;
168+
169+ while ( current ) {
170+ if ( visited . has ( current ) ) {
171+ return undefined ;
172+ }
173+ visited . add ( current ) ;
174+
175+ const descriptor = Object . getOwnPropertyDescriptor ( current , prop ) ;
176+ if ( descriptor ) {
177+ return descriptor ;
178+ }
179+ current = Object . getPrototypeOf ( current ) ;
180+ }
181+
182+ return undefined ;
183+ }
184+
152185/**
153186 * Deeply mock an object
154187 *
155188 * Core algorithm:
156189 * 1. Iterate over all properties of the object
157190 * 2. Functions → replace with mock functions (automock) or wrap as spy (autospy)
158- * 3. Plain objects → process recursively
191+ * 3. Plain objects → process properties lazily
159192 * 4. Arrays → empty in automock mode, process elements recursively in autospy mode
160193 * 5. Primitive values → keep unchanged
161194 * 6. Use WeakMap to track processed objects and avoid circular references
@@ -170,8 +203,44 @@ export function mockObject<T extends Record<Key, any>>(
170203
171204 // Track processed object references to prevent infinite recursion from circular references
172205 const processedRefs = new WeakMap ( ) ;
173- // Deferred assignment queue for handling circular references
174- const deferredAssignments : ( ( ) => void ) [ ] = [ ] ;
206+ const snapshotRefs = new WeakMap ( ) ;
207+
208+ const snapshotProperties = ( value : any ) : PropertySnapshot | undefined => {
209+ if (
210+ ! isSpyMode ||
211+ value === null ||
212+ value === undefined ||
213+ ( typeof value !== 'object' && typeof value !== 'function' ) ||
214+ ( ! isPlainObject ( value ) && ! isFunction ( value ) )
215+ ) {
216+ return undefined ;
217+ }
218+
219+ if ( snapshotRefs . has ( value ) ) {
220+ return snapshotRefs . get ( value ) ;
221+ }
222+
223+ const props = getEnumerableProperties ( value , globalConstructors ) ;
224+ const descriptors = new Map < Key , PropertyDescriptor > ( ) ;
225+ const snapshot : PropertySnapshot = {
226+ props,
227+ descriptors,
228+ isModule : ! isFunction ( value ) && isModuleObject ( value ) ,
229+ } ;
230+ snapshotRefs . set ( value , snapshot ) ;
231+
232+ const isModule = snapshot . isModule ;
233+ for ( const prop of props ) {
234+ const descriptor = isModule
235+ ? Object . getOwnPropertyDescriptor ( value , prop )
236+ : getPropertyDescriptor ( value , prop ) ;
237+ if ( descriptor ) {
238+ descriptors . set ( prop , descriptor ) ;
239+ }
240+ }
241+
242+ return snapshot ;
243+ } ;
175244
176245 /**
177246 * Create a mock instance for a function
@@ -192,7 +261,7 @@ export function mockObject<T extends Record<Key, any>>(
192261 /**
193262 * Process a single value and return the mocked value
194263 */
195- const processValue = ( value : any ) : any => {
264+ const processValue = ( value : any , snapshot ?: PropertySnapshot ) : any => {
196265 // Return primitive values as-is
197266 if ( value === null || value === undefined ) {
198267 return value ;
@@ -216,7 +285,7 @@ export function mockObject<T extends Record<Key, any>>(
216285 if ( isFunction ( value ) ) {
217286 const mock = createFunctionMock ( value ) ;
218287 // Functions may also have properties, process them recursively
219- processProperties ( value , mock ) ;
288+ processProperties ( value , mock , snapshot ) ;
220289 return mock ;
221290 }
222291
@@ -227,14 +296,14 @@ export function mockObject<T extends Record<Key, any>>(
227296 return [ ] ;
228297 }
229298 // autospy mode: process array elements recursively
230- return value . map ( processValue ) ;
299+ return value . map ( ( item ) => processValue ( item ) ) ;
231300 }
232301
233302 // Handle plain objects
234303 if ( isPlainObject ( value ) ) {
235304 const result : Record < Key , any > = { } ;
236305 processedRefs . set ( value , result ) ;
237- processProperties ( value , result ) ;
306+ processProperties ( value , result , snapshot ) ;
238307 return result ;
239308 }
240309
@@ -248,17 +317,20 @@ export function mockObject<T extends Record<Key, any>>(
248317 const processProperties = (
249318 source : Record < Key , any > ,
250319 target : Record < Key , any > ,
320+ snapshot ?: PropertySnapshot ,
251321 ) : void => {
252- const props = getEnumerableProperties ( source , globalConstructors ) ;
253- const isModule = getTypeName ( source ) === 'Module' || source . __esModule ;
322+ const props =
323+ snapshot ?. props ?? getEnumerableProperties ( source , globalConstructors ) ;
324+ const isModule = snapshot ?. isModule ?? isModuleObject ( source ) ;
254325
255326 for ( const prop of props ) {
256327 // Skip built-in readonly properties
257328 if ( isBuiltinReadonly ( source , prop ) ) {
258329 continue ;
259330 }
260331
261- const descriptor = Object . getOwnPropertyDescriptor ( source , prop ) ;
332+ const descriptor =
333+ snapshot ?. descriptors . get ( prop ) ?? getPropertyDescriptor ( source , prop ) ;
262334 if ( ! descriptor ) continue ;
263335
264336 // Handle getter/setter (non-module)
@@ -281,22 +353,58 @@ export function mockObject<T extends Record<Key, any>>(
281353 continue ;
282354 }
283355
284- const value = source [ prop ] ;
285-
286- // Check for circular references
287- if ( value && typeof value === 'object' && processedRefs . has ( value ) ) {
288- // Defer assignment until all objects are processed
289- deferredAssignments . push ( ( ) => {
290- target [ prop ] = processedRefs . get ( value ) ;
291- } ) ;
356+ const hasValue = 'value' in descriptor ;
357+ const value = hasValue ? descriptor . value : undefined ;
358+ const sourceValue =
359+ hasValue && isSpyMode && Array . isArray ( value ) ? value . slice ( ) : value ;
360+ const sourceSnapshot = hasValue ? snapshotProperties ( value ) : undefined ;
361+ const recursiveFunctionMock =
362+ hasValue && isFunction ( source ) && value === source ? target : undefined ;
363+ const canInstallLazyProperty = ! (
364+ isFunction ( target ) &&
365+ prop === 'prototype' &&
366+ value &&
367+ typeof value === 'object'
368+ ) ;
369+
370+ if ( ! canInstallLazyProperty ) {
371+ try {
372+ target [ prop ] =
373+ recursiveFunctionMock ?? processValue ( sourceValue , sourceSnapshot ) ;
374+ } catch {
375+ // Ignore assignment failures (some properties may be readonly)
376+ }
292377 continue ;
293378 }
294379
295- // Process and assign the value
296380 try {
297- target [ prop ] = processValue ( value ) ;
381+ let initialized = false ;
382+ let mockedValue : any ;
383+ const getSourceValue = ( ) => ( hasValue ? sourceValue : source [ prop ] ) ;
384+
385+ Object . defineProperty ( target , prop , {
386+ configurable : descriptor . configurable ,
387+ enumerable : descriptor . enumerable ,
388+ get : ( ) => {
389+ if ( ! initialized ) {
390+ initialized = true ;
391+ try {
392+ mockedValue =
393+ recursiveFunctionMock ??
394+ processValue ( getSourceValue ( ) , sourceSnapshot ) ;
395+ } catch {
396+ mockedValue = undefined ;
397+ }
398+ }
399+ return mockedValue ;
400+ } ,
401+ set : ( newValue ) => {
402+ initialized = true ;
403+ mockedValue = newValue ;
404+ } ,
405+ } ) ;
298406 } catch {
299- // Ignore assignment failures (some properties may be readonly)
407+ // Ignore definition failures
300408 }
301409 }
302410 } ;
@@ -305,10 +413,5 @@ export function mockObject<T extends Record<Key, any>>(
305413 processedRefs . set ( object , mockExports ) ;
306414 processProperties ( object , mockExports ) ;
307415
308- // Execute deferred assignments to resolve circular references
309- for ( const assign of deferredAssignments ) {
310- assign ( ) ;
311- }
312-
313416 return mockExports as T ;
314417}
0 commit comments