diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 540d4d4..9f5de6e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ jobs: matrix: node-version: - 16 - - 14 + - 18 name: Node.js ${{ matrix.node-version }} Quick steps: - name: Checkout the repository @@ -17,9 +17,11 @@ jobs: uses: actions/setup-node@v2 with: node-version: ${{ matrix.node-version }} + - name: Install pnpm + uses: pnpm/action-setup@v2 - name: Install dependencies - run: yarn install --frozen-lockfile --ignore-engines --ignore-scripts + run: pnpm install --frozen-lockfile - name: Run unit tests - run: yarn test + run: pnpm test env: FORCE_COLOR: 2 diff --git a/src/spyOn.ts b/src/spyOn.ts index bc80520..3c2682e 100644 --- a/src/spyOn.ts +++ b/src/spyOn.ts @@ -23,6 +23,13 @@ type Constructors = { let getDescriptor = (obj: any, method: string | symbol | number) => Object.getOwnPropertyDescriptor(obj, method) +let prototype = (fn: any, val: any) => { + if (val != null && typeof val === 'function' && val.prototype != null) { + // inherit prototype, keep original prototype chain + Object.setPrototypeOf(fn.prototype, val.prototype) + } +} + export function internalSpyOn( obj: T, methodName: K | { getter: K } | { setter: K }, @@ -38,7 +45,10 @@ export function internalSpyOn( 'cannot spyOn on a primitive value' ) - let getMeta = (): [string | symbol | number, 'value' | 'get' | 'set'] => { + let [accessName, accessType] = ((): [ + string | symbol | number, + 'value' | 'get' | 'set' + ] => { if (!isType('object', methodName)) { return [methodName, 'value'] } @@ -52,9 +62,7 @@ export function internalSpyOn( return [methodName.setter, 'set'] } throw new Error('specify getter or setter to spy on') - } - - let [accessName, accessType] = getMeta() + })() let objDescriptor = getDescriptor(obj, accessName) let proto = Object.getPrototypeOf(obj) let protoDescriptor = proto && getDescriptor(proto, accessName) @@ -92,6 +100,9 @@ export function internalSpyOn( if (!mock) mock = origin let fn = createInternalSpy(mock) + if (accessType === 'value') { + prototype(fn, origin) + } let reassign = (cb: any) => { let { value, ...desc } = originalDescriptor || { configurable: true, @@ -103,9 +114,10 @@ export function internalSpyOn( ;(desc as PropertyDescriptor)[accessType] = cb define(obj, accessName, desc) } - let restore = () => originalDescriptor - ? define(obj, accessName, originalDescriptor) - : reassign(origin) + let restore = () => + originalDescriptor + ? define(obj, accessName, originalDescriptor) + : reassign(origin) const state = fn[S] defineValue(state, 'restore', restore) defineValue(state, 'getOriginal', () => (ssr ? origin() : origin)) @@ -114,7 +126,14 @@ export function internalSpyOn( return fn }) - reassign(ssr ? () => fn : fn) + reassign( + ssr + ? () => { + prototype(fn, mock) + return fn + } + : fn + ) spies.add(fn as any) return fn as any diff --git a/test/class.test.ts b/test/class.test.ts index ccb1f3a..4069b06 100644 --- a/test/class.test.ts +++ b/test/class.test.ts @@ -25,8 +25,10 @@ describe('class mock', () => { expect(spy2.callCount).toBe(1) }) - test('spy keeps instance', () => { + test('spy keeps instance on a function', () => { function Test() {} + const method = spy() + Test.prototype.run = method const obj = { Test, } @@ -34,6 +36,38 @@ describe('class mock', () => { const instance = new obj.Test() expect(fn.called).toBe(true) expect(instance).toBeInstanceOf(obj.Test) + expect(instance.run).toBe(method) + }) + + test('spy keeps instance on a function getter', () => { + function Test() {} + const method = spy() + Test.prototype.run = method + const obj = { + get Test() { + return Test + }, + } + const fn = spyOn(obj, 'Test') + const instance = new obj.Test() + expect(fn.called).toBe(true) + expect(instance).toBeInstanceOf(obj.Test) + expect(instance.run).toBe(method) + }) + + test('spy keeps instance on a class', () => { + const method = spy() + class Test { + run = method + } + const obj = { + Test, + } + const fn = spyOn(obj, 'Test') + const instance = new obj.Test() + expect(fn.called).toBe(true) + expect(instance).toBeInstanceOf(obj.Test) + expect(instance.run).toBe(method) }) describe('spying on constructor', () => {