Skip to content

Commit d989301

Browse files
authored
fix: inherit original prototype when spying (#42)
* fix: inherit original prorotype when spying * chore: cleaup * chore: update ci * chore: ci * chore: cleanup
1 parent 9469e1e commit d989301

File tree

3 files changed

+67
-12
lines changed

3 files changed

+67
-12
lines changed

.github/workflows/test.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ jobs:
88
matrix:
99
node-version:
1010
- 16
11-
- 14
11+
- 18
1212
name: Node.js ${{ matrix.node-version }} Quick
1313
steps:
1414
- name: Checkout the repository
@@ -17,9 +17,11 @@ jobs:
1717
uses: actions/setup-node@v2
1818
with:
1919
node-version: ${{ matrix.node-version }}
20+
- name: Install pnpm
21+
uses: pnpm/action-setup@v2
2022
- name: Install dependencies
21-
run: yarn install --frozen-lockfile --ignore-engines --ignore-scripts
23+
run: pnpm install --frozen-lockfile
2224
- name: Run unit tests
23-
run: yarn test
25+
run: pnpm test
2426
env:
2527
FORCE_COLOR: 2

src/spyOn.ts

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ type Constructors<T> = {
2323
let getDescriptor = (obj: any, method: string | symbol | number) =>
2424
Object.getOwnPropertyDescriptor(obj, method)
2525

26+
let prototype = (fn: any, val: any) => {
27+
if (val != null && typeof val === 'function' && val.prototype != null) {
28+
// inherit prototype, keep original prototype chain
29+
Object.setPrototypeOf(fn.prototype, val.prototype)
30+
}
31+
}
32+
2633
export function internalSpyOn<T, K extends string & keyof T>(
2734
obj: T,
2835
methodName: K | { getter: K } | { setter: K },
@@ -38,7 +45,10 @@ export function internalSpyOn<T, K extends string & keyof T>(
3845
'cannot spyOn on a primitive value'
3946
)
4047

41-
let getMeta = (): [string | symbol | number, 'value' | 'get' | 'set'] => {
48+
let [accessName, accessType] = ((): [
49+
string | symbol | number,
50+
'value' | 'get' | 'set'
51+
] => {
4252
if (!isType('object', methodName)) {
4353
return [methodName, 'value']
4454
}
@@ -52,9 +62,7 @@ export function internalSpyOn<T, K extends string & keyof T>(
5262
return [methodName.setter, 'set']
5363
}
5464
throw new Error('specify getter or setter to spy on')
55-
}
56-
57-
let [accessName, accessType] = getMeta()
65+
})()
5866
let objDescriptor = getDescriptor(obj, accessName)
5967
let proto = Object.getPrototypeOf(obj)
6068
let protoDescriptor = proto && getDescriptor(proto, accessName)
@@ -92,6 +100,9 @@ export function internalSpyOn<T, K extends string & keyof T>(
92100
if (!mock) mock = origin
93101

94102
let fn = createInternalSpy(mock)
103+
if (accessType === 'value') {
104+
prototype(fn, origin)
105+
}
95106
let reassign = (cb: any) => {
96107
let { value, ...desc } = originalDescriptor || {
97108
configurable: true,
@@ -103,9 +114,10 @@ export function internalSpyOn<T, K extends string & keyof T>(
103114
;(desc as PropertyDescriptor)[accessType] = cb
104115
define(obj, accessName, desc)
105116
}
106-
let restore = () => originalDescriptor
107-
? define(obj, accessName, originalDescriptor)
108-
: reassign(origin)
117+
let restore = () =>
118+
originalDescriptor
119+
? define(obj, accessName, originalDescriptor)
120+
: reassign(origin)
109121
const state = fn[S]
110122
defineValue(state, 'restore', restore)
111123
defineValue(state, 'getOriginal', () => (ssr ? origin() : origin))
@@ -114,7 +126,14 @@ export function internalSpyOn<T, K extends string & keyof T>(
114126
return fn
115127
})
116128

117-
reassign(ssr ? () => fn : fn)
129+
reassign(
130+
ssr
131+
? () => {
132+
prototype(fn, mock)
133+
return fn
134+
}
135+
: fn
136+
)
118137

119138
spies.add(fn as any)
120139
return fn as any

test/class.test.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,49 @@ describe('class mock', () => {
2525
expect(spy2.callCount).toBe(1)
2626
})
2727

28-
test('spy keeps instance', () => {
28+
test('spy keeps instance on a function', () => {
2929
function Test() {}
30+
const method = spy()
31+
Test.prototype.run = method
3032
const obj = {
3133
Test,
3234
}
3335
const fn = spyOn(obj, 'Test')
3436
const instance = new obj.Test()
3537
expect(fn.called).toBe(true)
3638
expect(instance).toBeInstanceOf(obj.Test)
39+
expect(instance.run).toBe(method)
40+
})
41+
42+
test('spy keeps instance on a function getter', () => {
43+
function Test() {}
44+
const method = spy()
45+
Test.prototype.run = method
46+
const obj = {
47+
get Test() {
48+
return Test
49+
},
50+
}
51+
const fn = spyOn(obj, 'Test')
52+
const instance = new obj.Test()
53+
expect(fn.called).toBe(true)
54+
expect(instance).toBeInstanceOf(obj.Test)
55+
expect(instance.run).toBe(method)
56+
})
57+
58+
test('spy keeps instance on a class', () => {
59+
const method = spy()
60+
class Test {
61+
run = method
62+
}
63+
const obj = {
64+
Test,
65+
}
66+
const fn = spyOn(obj, 'Test')
67+
const instance = new obj.Test()
68+
expect(fn.called).toBe(true)
69+
expect(instance).toBeInstanceOf(obj.Test)
70+
expect(instance.run).toBe(method)
3771
})
3872

3973
describe('spying on constructor', () => {

0 commit comments

Comments
 (0)