From 24cd5c4e1e0b55ad944b07418b0e6d7c94f83561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Norte?= Date: Thu, 30 Aug 2018 12:24:23 +0100 Subject: [PATCH] Fix mocking inherited static properties and prototype-less objects --- CHANGELOG.md | 2 + .../jest-mock/src/__tests__/jest_mock.test.js | 144 +++++++++++++++++- packages/jest-mock/src/index.js | 106 ++++++------- 3 files changed, 196 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0d7fbd43a29..d3c5a20d4faf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - `[jest-haste-map]` Do not visit again files with the same sha-1 ([#6990](https://github.com/facebook/jest/pull/6990)) - `[jest-jasmine2]` Fix memory leak in Error objects hold by the framework ([#6965](https://github.com/facebook/jest/pull/6965)) - `[jest-haste-map]` Fixed Haste whitelist generation for scoped modules on Windows ([#6980](https://github.com/facebook/jest/pull/6980)) +- `[jest-mock]` Fix inheritance of static properties and methods in mocks ([#7003](https://github.com/facebook/jest/pull/7003)) +- `[jest-mock]` Fix mocking objects without `Object.prototype` in their prototype chain ([#7003](https://github.com/facebook/jest/pull/7003)) ### Chore & Maintenance diff --git a/packages/jest-mock/src/__tests__/jest_mock.test.js b/packages/jest-mock/src/__tests__/jest_mock.test.js index 9b09ce849199..39fbb992337f 100644 --- a/packages/jest-mock/src/__tests__/jest_mock.test.js +++ b/packages/jest-mock/src/__tests__/jest_mock.test.js @@ -12,11 +12,14 @@ const vm = require('vm'); describe('moduleMocker', () => { let moduleMocker; + let mockContext; + let mockGlobals; beforeEach(() => { const mock = require('../'); - const global = vm.runInNewContext('this'); - moduleMocker = new mock.ModuleMocker(global); + mockContext = vm.createContext(); + mockGlobals = vm.runInNewContext('this', mockContext); + moduleMocker = new mock.ModuleMocker(mockGlobals); }); describe('getMetadata', () => { @@ -137,7 +140,7 @@ describe('moduleMocker', () => { expect(typeof foo.nonEnumMethod).toBe('function'); - expect(mock.nonEnumMethod.mock).not.toBeUndefined(); + expect(mock.nonEnumMethod.mock).toBeDefined(); expect(mock.nonEnumGetter).toBeUndefined(); }); @@ -180,9 +183,140 @@ describe('moduleMocker', () => { expect(typeof foo.foo).toBe('function'); expect(typeof instanceFooMock.foo).toBe('function'); - expect(instanceFooMock.foo.mock).not.toBeUndefined(); + expect(instanceFooMock.foo.mock).toBeDefined(); - expect(instanceFooMock.toString.mock).not.toBeUndefined(); + expect(instanceFooMock.toString.mock).toBeDefined(); + }); + + it('mocks ES2015 non-enumerable static properties and methods', () => { + class ClassFoo { + static foo() {} + } + ClassFoo.fooProp = () => {}; + + class ClassBar extends ClassFoo {} + + const ClassBarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(ClassBar), + ); + + expect(typeof ClassBarMock.foo).toBe('function'); + expect(typeof ClassBarMock.fooProp).toBe('function'); + expect(ClassBarMock.foo.mock).toBeDefined(); + expect(ClassBarMock.fooProp.mock).toBeDefined(); + }); + + it('mocks methods in all the prototype chain (null prototype)', () => { + const Foo = Object.assign(Object.create(null), {foo() {}}); + const Bar = Object.assign(Object.create(Foo), {bar() {}}); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar), + ); + expect(typeof BarMock.foo).toBe('function'); + expect(typeof BarMock.bar).toBe('function'); + }); + + it('does not mock methods from Object.prototype', () => { + const Foo = {foo() {}}; + const Bar = Object.assign(Object.create(Foo), {bar() {}}); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar), + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Object); + expect( + Object.prototype.hasOwnProperty.call(BarMock, 'hasOwnProperty'), + ).toBe(false); + expect(BarMock.hasOwnProperty).toBe( + mockGlobals.Object.prototype.hasOwnProperty, + ); + }); + + it('does not mock methods from Object.prototype (in mock context)', () => { + const Bar = vm.runInContext( + ` + const Foo = { foo() {} }; + const Bar = Object.assign(Object.create(Foo), { bar() {} }); + Bar; + `, + mockContext, + ); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar), + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Object); + expect( + Object.prototype.hasOwnProperty.call(BarMock, 'hasOwnProperty'), + ).toBe(false); + expect(BarMock.hasOwnProperty).toBe( + mockGlobals.Object.prototype.hasOwnProperty, + ); + }); + + it('does not mock methods from Function.prototype', () => { + class Foo {} + class Bar extends Foo {} + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar), + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Function); + expect(Object.prototype.hasOwnProperty.call(BarMock, 'bind')).toBe(false); + expect(BarMock.bind).toBe(mockGlobals.Function.prototype.bind); + }); + + it('does not mock methods from Function.prototype (in mock context)', () => { + const Bar = vm.runInContext( + ` + class Foo {} + class Bar extends Foo {} + Bar; + `, + mockContext, + ); + + const BarMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(Bar), + ); + + expect(BarMock).toBeInstanceOf(mockGlobals.Function); + expect(Object.prototype.hasOwnProperty.call(BarMock, 'bind')).toBe(false); + expect(BarMock.bind).toBe(mockGlobals.Function.prototype.bind); + }); + + it('does not mock methods from RegExp.prototype', () => { + const bar = /bar/; + + const barMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(bar), + ); + + expect(barMock).toBeInstanceOf(mockGlobals.RegExp); + expect(Object.prototype.hasOwnProperty.call(barMock, 'test')).toBe(false); + expect(barMock.test).toBe(mockGlobals.RegExp.prototype.test); + }); + + it('does not mock methods from RegExp.prototype (in mock context)', () => { + const bar = vm.runInContext( + ` + const bar = /bar/; + bar; + `, + mockContext, + ); + + const barMock = moduleMocker.generateFromMetadata( + moduleMocker.getMetadata(bar), + ); + + expect(barMock).toBeInstanceOf(mockGlobals.RegExp); + expect(Object.prototype.hasOwnProperty.call(barMock, 'test')).toBe(false); + expect(barMock.test).toBe(mockGlobals.RegExp.prototype.test); }); it('mocks methods that are bound multiple times', () => { diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.js index 9b679c73e4c3..39bb7a8a8d02 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.js @@ -226,32 +226,6 @@ function isReadonlyProp(object: any, prop: string): boolean { ); } -function getSlots(object?: Object): Array { - const slots = {}; - if (!object) { - return []; - } - - let parent = Object.getPrototypeOf(object); - do { - if (object === Object.getPrototypeOf(Function)) { - break; - } - const ownNames = Object.getOwnPropertyNames(object); - for (let i = 0; i < ownNames.length; i++) { - const prop = ownNames[i]; - if (!isReadonlyProp(object, prop)) { - const propDesc = Object.getOwnPropertyDescriptor(object, prop); - if ((propDesc !== undefined && !propDesc.get) || object.__esModule) { - slots[prop] = true; - } - } - } - object = parent; - } while (object && (parent = Object.getPrototypeOf(object)) !== null); - return Object.keys(slots); -} - class ModuleMockerClass { _environmentGlobal: Global; _mockState: WeakMap; @@ -274,6 +248,53 @@ class ModuleMockerClass { this._invocationCallCounter = 1; } + _getSlots(object?: Object): Array { + if (!object) { + return []; + } + + const slots = new Set(); + const EnvObjectProto = this._environmentGlobal.Object.prototype; + const EnvFunctionProto = this._environmentGlobal.Function.prototype; + const EnvRegExpProto = this._environmentGlobal.RegExp.prototype; + + // Also check the builtins in the current context as they leak through + // core node modules. + const ObjectProto = Object.prototype; + const FunctionProto = Function.prototype; + const RegExpProto = RegExp.prototype; + + // Properties of Object.prototype, Function.prototype and RegExp.prototype + // are never reported as slots + while ( + object != null && + object !== EnvObjectProto && + object !== EnvFunctionProto && + object !== EnvRegExpProto && + object !== ObjectProto && + object !== FunctionProto && + object !== RegExpProto + ) { + const ownNames = Object.getOwnPropertyNames(object); + + for (let i = 0; i < ownNames.length; i++) { + const prop = ownNames[i]; + + if (!isReadonlyProp(object, prop)) { + const propDesc = Object.getOwnPropertyDescriptor(object, prop); + + if ((propDesc !== undefined && !propDesc.get) || object.__esModule) { + slots.add(prop); + } + } + } + + object = Object.getPrototypeOf(object); + } + + return Array.from(slots); + } + _ensureMockConfig(f: Mock): MockFunctionConfig { let config = this._mockConfigRegistry.get(f); if (!config) { @@ -336,7 +357,7 @@ class ModuleMockerClass { metadata.members.prototype && metadata.members.prototype.members) || {}; - const prototypeSlots = getSlots(prototype); + const prototypeSlots = this._getSlots(prototype); const mocker = this; const mockConstructor = matchArity(function() { const mockState = mocker._ensureMockState(f); @@ -606,7 +627,7 @@ class ModuleMockerClass { refs[metadata.refID] = mock; } - getSlots(metadata.members).forEach(slot => { + this._getSlots(metadata.members).forEach(slot => { const slotMetadata = (metadata.members && metadata.members[slot]) || {}; if (slotMetadata.ref != null) { callbacks.push(() => (mock[slot] = refs[slotMetadata.ref])); @@ -678,7 +699,7 @@ class ModuleMockerClass { // Leave arrays alone if (type !== 'array') { if (type !== 'undefined') { - getSlots(component).forEach(slot => { + this._getSlots(component).forEach(slot => { if ( type === 'function' && component._isMockFunction && @@ -687,32 +708,15 @@ class ModuleMockerClass { return; } - if ( - (!component.hasOwnProperty && component[slot] !== undefined) || - (component.hasOwnProperty && component.hasOwnProperty(slot)) || - (type === 'object' && component[slot] != Object.prototype[slot]) - ) { - const slotMetadata = this.getMetadata(component[slot], refs); - if (slotMetadata) { - if (!members) { - members = {}; - } - members[slot] = slotMetadata; + const slotMetadata = this.getMetadata(component[slot], refs); + if (slotMetadata) { + if (!members) { + members = {}; } + members[slot] = slotMetadata; } }); } - - // If component is native code function, prototype might be undefined - if (type === 'function' && component.prototype) { - const prototype = this.getMetadata(component.prototype, refs); - if (prototype && prototype.members) { - if (!members) { - members = {}; - } - members.prototype = prototype; - } - } } if (members) {