Skip to content

Commit

Permalink
Fix mocking inherited static properties and prototype-less objects (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
rubennorte authored Sep 20, 2018
1 parent a5cdea2 commit 1251008
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 56 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
144 changes: 139 additions & 5 deletions packages/jest-mock/src/__tests__/jest_mock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});

Expand Down Expand Up @@ -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', () => {
Expand Down
106 changes: 55 additions & 51 deletions packages/jest-mock/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,32 +226,6 @@ function isReadonlyProp(object: any, prop: string): boolean {
);
}

function getSlots(object?: Object): Array<string> {
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<Function, MockFunctionState>;
Expand All @@ -274,6 +248,53 @@ class ModuleMockerClass {
this._invocationCallCounter = 1;
}

_getSlots(object?: Object): Array<string> {
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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]));
Expand Down Expand Up @@ -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 &&
Expand All @@ -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) {
Expand Down

0 comments on commit 1251008

Please sign in to comment.