1+ /**
2+ * Jest-specific setup for unified testing framework abstraction.
3+ *
4+ * This file provides Jest implementations of the unified testing utilities
5+ * available on the global `runner` object. It creates a consistent interface
6+ * that allows the library's tests to work identically across Jest, Vitest,
7+ * and SWC test environments.
8+ *
9+ * The key pattern here is the SmartSpy system, which wraps framework-specific
10+ * spy objects (like Jest's SpyInstance) to provide a unified API.
11+ */
12+
13+ /**
14+ * Unified spy interface that works consistently across Jest and Vitest.
15+ *
16+ * This interface defines the common spy methods that our library needs,
17+ * providing a consistent API regardless of the underlying testing framework.
18+ * The implementation only includes the methods we actually use in our tests.
19+ */
20+ interface SmartSpy {
21+ /** Mock the implementation of the spied method */
22+ mockImplementation : ( fn : ( ) => void ) => SmartSpy ;
23+ /** Assert that the spy was called with specific arguments */
24+ toHaveBeenCalledWith : ( ...args : unknown [ ] ) => void ;
25+ /** Restore the original method implementation */
26+ mockRestore : ( ) => void ;
27+ /** Allow access to any other spy properties/methods from the underlying framework */
28+ [ key : string ] : unknown ;
29+ }
30+
131function useFakeTimers ( ) {
232 jest . useFakeTimers ( ) ;
333}
@@ -14,32 +44,101 @@ function fn() {
1444 return jest . fn ( ) ;
1545}
1646
17- // Smart proxy that only implements what we need
18- function createSmartSpy ( realSpy : unknown ) {
47+ /**
48+ * Creates a unified spy interface that works consistently across Jest and Vitest.
49+ *
50+ * This function wraps a Jest SpyInstance in a Proxy to provide a consistent API
51+ * that matches the SmartSpy interface expected by the testing framework abstraction.
52+ *
53+ * The proxy intercepts calls to specific methods (mockImplementation, toHaveBeenCalledWith,
54+ * mockRestore) and ensures they work the same way regardless of whether the underlying
55+ * spy is from Jest or Vitest. For all other properties/methods, it passes through
56+ * to the original spy.
57+ *
58+ * This is part of the broader pattern in this library where we create unified interfaces
59+ * across different testing frameworks (Jest, Vitest, SWC) so that the library's mocks
60+ * work consistently in any environment.
61+ *
62+ * @param realSpy - The underlying Jest SpyInstance to wrap
63+ * @returns A proxy that implements the SmartSpy interface
64+ */
65+ function createSmartSpy ( realSpy : jest . SpyInstance ) : SmartSpy {
1966 return new Proxy ( realSpy as object , {
2067 get ( target , prop ) {
21- // Only implement the methods we actually use
22- if ( prop === 'mockImplementation' ) {
23- return ( fn : ( ) => void ) => {
24- ( target as { mockImplementation : ( fn : ( ) => void ) => unknown } ) . mockImplementation ( fn ) ;
25- return createSmartSpy ( target ) ;
26- } ;
27- }
28- if ( prop === 'toHaveBeenCalledWith' ) {
29- return ( target as { toHaveBeenCalledWith : ( ...args : unknown [ ] ) => unknown } ) . toHaveBeenCalledWith . bind ( target ) ;
30- }
31- if ( prop === 'mockRestore' ) {
32- return ( target as { mockRestore : ( ) => void } ) . mockRestore . bind ( target ) ;
68+ // Switch on the specific methods we need to implement
69+ switch ( prop ) {
70+ case 'mockImplementation' :
71+ return ( fn : ( ) => void ) => {
72+ ( target as jest . SpyInstance ) . mockImplementation ( fn ) ;
73+ return createSmartSpy ( target as jest . SpyInstance ) ;
74+ } ;
75+
76+ case 'toHaveBeenCalledWith' :
77+ return ( ...args : unknown [ ] ) => {
78+ // Jest SpyInstance doesn't have toHaveBeenCalledWith as a method,
79+ // but Vitest spies do. For compatibility, we implement it by checking
80+ // the spy's call history manually to match Vitest's behavior.
81+ const jestSpy = target as jest . SpyInstance ;
82+
83+ // Check if any call matches the expected arguments
84+ const calls = jestSpy . mock . calls ;
85+ const hasMatchingCall = calls . some ( ( call : unknown [ ] ) => {
86+ if ( call . length !== args . length ) return false ;
87+ return call . every ( ( arg : unknown , index : number ) => {
88+ // Use Jest's deep equality matching
89+ try {
90+ expect ( arg ) . toEqual ( args [ index ] ) ;
91+ return true ;
92+ } catch {
93+ return false ;
94+ }
95+ } ) ;
96+ } ) ;
97+
98+ if ( ! hasMatchingCall ) {
99+ throw new Error (
100+ `Expected spy to have been called with [${ args . map ( ( arg : unknown ) => JSON . stringify ( arg ) ) . join ( ', ' ) } ], ` +
101+ `but it was called with: ${ calls
102+ . map (
103+ ( call : unknown [ ] ) =>
104+ `[${ call . map ( ( arg : unknown ) => JSON . stringify ( arg ) ) . join ( ', ' ) } ]`
105+ )
106+ . join ( ', ' ) } `
107+ ) ;
108+ }
109+ } ;
110+
111+ case 'mockRestore' :
112+ return ( ) => {
113+ ( target as jest . SpyInstance ) . mockRestore ( ) ;
114+ } ;
115+
116+ default :
117+ // For everything else, just pass through to the real spy
118+ return ( target as Record < string | symbol , unknown > ) [ prop ] ;
33119 }
34-
35- // For everything else, just pass through to the real spy
36- return ( target as Record < string | symbol , unknown > ) [ prop ] ;
37- }
38- } ) ;
120+ } ,
121+ } ) as SmartSpy ;
39122}
40123
41- function spyOn < T extends object , K extends keyof T > ( object : T , method : K ) {
42- const realSpy = ( jest . spyOn as ( obj : T , method : K ) => unknown ) ( object , method ) ;
124+ /**
125+ * Creates a spy on an object method using Jest's spyOn, wrapped with SmartSpy interface.
126+ *
127+ * This is the Jest-specific implementation of the unified spyOn function that's available
128+ * on the global `runner` object. It creates a Jest spy and wraps it with createSmartSpy
129+ * to provide a consistent interface across testing frameworks.
130+ *
131+ * @param object - The object to spy on
132+ * @param method - The method name to spy on
133+ * @returns A SmartSpy that provides unified spy functionality
134+ */
135+ function spyOn < T extends object , K extends keyof T > (
136+ object : T ,
137+ method : K
138+ ) : SmartSpy {
139+ // Use a more specific type assertion to avoid 'any'
140+ const spyFunction = jest . spyOn as ( object : T , method : K ) => jest . SpyInstance ;
141+ const realSpy = spyFunction ( object , method ) ;
43142 return createSmartSpy ( realSpy ) ;
44143}
45144
0 commit comments