Skip to content

Commit 75e7fcc

Browse files
authored
fix(spy): copy static properties if spy is initialised with vi.fn(), fix types for vi.spyOn(obj, class) (#8956)
1 parent bcb132f commit 75e7fcc

File tree

4 files changed

+87
-41
lines changed

4 files changed

+87
-41
lines changed

packages/spy/src/index.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ export function createMockInstance(options: MockInstanceOption = {}): Mock<Proce
194194
export function fn<T extends Procedure | Constructable = Procedure>(
195195
originalImplementation?: T,
196196
): Mock<T> {
197+
// if the function is already a mock, just return the same function,
198+
// simillarly to how vi.spyOn() works
199+
if (originalImplementation != null && isMockFunction(originalImplementation)) {
200+
return originalImplementation as Mock<T>
201+
}
202+
197203
return createMockInstance({
198204
// we pass this down so getMockImplementation() always returns the value
199205
mockImplementation: originalImplementation,
@@ -216,11 +222,9 @@ export function spyOn<T extends object, G extends Properties<Required<T>>>(
216222
export function spyOn<T extends object, M extends Classes<Required<T>> | Methods<Required<T>>>(
217223
object: T,
218224
key: M
219-
): Required<T>[M] extends { new (...args: infer A): infer R }
220-
? Mock<{ new (...args: A): R }>
221-
: Required<T>[M] extends Procedure
222-
? Mock<Required<T>[M]>
223-
: never
225+
): Required<T>[M] extends Constructable | Procedure
226+
? Mock<Required<T>[M]>
227+
: never
224228
export function spyOn<T extends object, K extends keyof T>(
225229
object: T,
226230
key: K,
@@ -374,13 +378,15 @@ function createMock(
374378
prototypeState,
375379
prototypeConfig,
376380
keepMembersImplementation,
381+
mockImplementation,
377382
prototypeMembers = [],
378383
}: MockInstanceOption & {
379384
state: MockContext
380385
config: MockConfig
381386
},
382387
) {
383-
const original = config.mockOriginal
388+
const original = config.mockOriginal // init with vi.spyOn(obj, 'Klass')
389+
const pseudoOriginal = mockImplementation // init with vi.fn(Klass)
384390
const name = (mockName || original?.name || 'Mock') as string
385391
const namedObject: Record<string, Mock<Procedure | Constructable>> = {
386392
// to keep the name of the function intact
@@ -499,8 +505,9 @@ function createMock(
499505
}) as Mock,
500506
}
501507
const mock = namedObject[name] as Mock<Procedure | Constructable>
502-
if (original) {
503-
copyOriginalStaticProperties(mock, original)
508+
const copyPropertiesFrom = original || pseudoOriginal
509+
if (copyPropertiesFrom) {
510+
copyOriginalStaticProperties(mock, copyPropertiesFrom)
504511
}
505512
return mock
506513
}

test/core/test/mocking/vi-fn.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,37 @@ test('vi.fn().mock cannot be overriden', () => {
2020
}).toThrowError()
2121
})
2222

23+
describe('vi.fn() copies static properties', () => {
24+
test('vi.fn() copies static properties from functions', () => {
25+
function Example() {}
26+
Example.HELLO_WORLD = true
27+
28+
const spy = vi.fn(Example)
29+
expect(Example.HELLO_WORLD).toBe(true)
30+
expect(spy.HELLO_WORLD).toBe(true)
31+
})
32+
33+
test('vi.fn() copies static properties from classes', () => {
34+
class Example {
35+
static HELLO_WORLD = true
36+
}
37+
38+
const spy = vi.fn(Example)
39+
expect(Example.HELLO_WORLD).toBe(true)
40+
expect(spy.HELLO_WORLD).toBe(true)
41+
})
42+
43+
test('vi.fn() ignores "node.js.promisify" symbol', () => {
44+
const promisifySymbol = Symbol.for('nodejs.util.promisify.custom')
45+
class Example {
46+
static [promisifySymbol] = () => Promise.resolve(42)
47+
}
48+
49+
const spy = vi.fn(Example)
50+
expect(spy[promisifySymbol]).toBe(undefined)
51+
})
52+
})
53+
2354
describe('fn.length is consistent', () => {
2455
test('vi.fn() has correct length', () => {
2556
const fn0 = vi.fn(() => {})

test/core/test/mocking/vi-spyOn.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,47 @@ test('vi.fn() has correct length', () => {
1818
expect(fn3.length).toBe(3)
1919
})
2020

21+
describe('vi.spyOn() copies static properties', () => {
22+
test('vi.spyOn() copies properties from functions', () => {
23+
function a() {}
24+
a.HELLO_WORLD = true
25+
const obj = {
26+
a,
27+
}
28+
29+
const spy = vi.spyOn(obj, 'a')
30+
31+
expect(obj.a.HELLO_WORLD).toBe(true)
32+
expect(spy.HELLO_WORLD).toBe(true)
33+
})
34+
35+
test('vi.spyOn() copies properties from classes', () => {
36+
class A {
37+
static HELLO_WORLD = true
38+
}
39+
const obj = {
40+
A,
41+
}
42+
43+
const spy = vi.spyOn(obj, 'A')
44+
45+
expect(obj.A.HELLO_WORLD).toBe(true)
46+
expect(spy.HELLO_WORLD).toBe(true)
47+
})
48+
49+
test('vi.spyOn() ignores node.js.promisify symbol', () => {
50+
const promisifySymbol = Symbol.for('nodejs.util.promisify.custom')
51+
class Example {
52+
static [promisifySymbol] = () => Promise.resolve(42)
53+
}
54+
const obj = { Example }
55+
56+
const spy = vi.spyOn(obj, 'Example')
57+
58+
expect(spy[promisifySymbol]).toBe(undefined)
59+
})
60+
})
61+
2162
describe('vi.spyOn() state', () => {
2263
test('vi.spyOn() spies on an object and tracks the calls', () => {
2364
const object = createObject()

test/core/test/spy.test.ts

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -30,37 +30,4 @@ describe('spyOn', () => {
3030

3131
expect(hw.hello()).toEqual('hello world')
3232
})
33-
34-
test('spying copies properties from functions', () => {
35-
function a() {}
36-
a.HELLO_WORLD = true
37-
const obj = {
38-
a,
39-
}
40-
const spy = vi.spyOn(obj, 'a')
41-
expect(obj.a.HELLO_WORLD).toBe(true)
42-
expect((spy as any).HELLO_WORLD).toBe(true)
43-
})
44-
45-
test('spying copies properties from classes', () => {
46-
class A {
47-
static HELLO_WORLD = true
48-
}
49-
const obj = {
50-
A,
51-
}
52-
const spy = vi.spyOn(obj, 'A')
53-
expect(obj.A.HELLO_WORLD).toBe(true)
54-
expect((spy as any).HELLO_WORLD).toBe(true)
55-
})
56-
57-
test('ignores node.js.promisify symbol', () => {
58-
const promisifySymbol = Symbol.for('nodejs.util.promisify.custom')
59-
class Example {
60-
static [promisifySymbol] = () => Promise.resolve(42)
61-
}
62-
const obj = { Example }
63-
const spy = vi.spyOn(obj, 'Example')
64-
expect((spy as any)[promisifySymbol]).toBe(undefined)
65-
})
6633
})

0 commit comments

Comments
 (0)