Skip to content

Commit

Permalink
feat: implement spyOnProperty method, fixes jestjs#5106
Browse files Browse the repository at this point in the history
  • Loading branch information
phra committed Dec 18, 2017
1 parent 011951a commit ab6e1db
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 24 deletions.
1 change: 1 addition & 0 deletions flow-typed/npm/jest_v21.x.x.js
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ declare var expect: {
// TODO handle return type
// http://jasmine.github.io/2.4/introduction.html#section-Spies
declare function spyOn(value: mixed, method: string): Object;
declare function spyOnProperty(value: mixed, propertyName: string, accessType: 'get' | 'set'): Object;

/** Holds all functions related to manipulating test runner */
declare var jest: JestObjectType;
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-jasmine2/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,10 @@ const addSnapshotData = (results, snapshotState) => {
});

const uncheckedCount = snapshotState.getUncheckedCount();
const uncheckedKeys = snapshotState.getUncheckedKeys();
let uncheckedKeys

if (uncheckedCount) {
uncheckedKeys = snapshotState.getUncheckedKeys();
snapshotState.removeUncheckedKeys();
}

Expand Down
12 changes: 8 additions & 4 deletions packages/jest-jasmine2/src/jasmine/Env.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ export default function(j$) {
return spyRegistry.spyOn.apply(spyRegistry, arguments);
};

this.spyOnProperty = function() {
return spyRegistry.spyOnProperty.apply(spyRegistry, arguments);
};

const suiteFactory = function(description) {
const suite = new j$.Suite({
id: getNextSuiteId(),
Expand Down Expand Up @@ -434,10 +438,10 @@ export default function(j$) {
if (currentSpec !== null) {
throw new Error(
'Tests cannot be nested. Test `' +
spec.description +
'` cannot run because it is nested within `' +
currentSpec.description +
'`.',
spec.description +
'` cannot run because it is nested within `' +
currentSpec.description +
'`.',
);
}
currentDeclarationSuite.addChild(spec);
Expand Down
4 changes: 4 additions & 0 deletions packages/jest-jasmine2/src/jasmine/jasmine_light.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ exports.interface = function(jasmine: Jasmine, env: any) {
return env.spyOn(obj, methodName);
},

spyOnProperty: function (obj: Object, methodName: string, accessType = 'get') {
return env.spyOnProperty(obj, methodName, accessType);
},

jsApiReporter: new jasmine.JsApiReporter({
timer: new jasmine.Timer(),
}),
Expand Down
63 changes: 63 additions & 0 deletions packages/jest-jasmine2/src/jasmine/spy_registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,69 @@ export default function SpyRegistry(options: Object) {
return spiedMethod;
};

this.spyOnProperty = function(obj, propertyName, accessType = 'get') {
if (!obj) {
throw new Error('spyOn could not find an object to spy upon for ' + propertyName + '');
}

if (!propertyName) {
throw new Error('No property name supplied');
}

let descriptor;
try {
descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
} catch (e) {
// IE 8 doesn't support `definePropery` on non-DOM nodes
}

if (!descriptor) {
throw new Error(propertyName + ' property does not exist');
}

if (!descriptor.configurable) {
throw new Error(propertyName + ' is not declared configurable');
}

if (!descriptor[accessType]) {
throw new Error('Property ' + propertyName + ' does not have access type ' + accessType);
}

if (obj[propertyName] && isSpy(obj[propertyName])) {
if (this.respy) {
return obj[propertyName];
} else {
throw new Error(
getErrorMsg(propertyName + ' has already been spied upon'),
);
}
}

const originalDescriptor = descriptor;
const spiedProperty = createSpy(propertyName, descriptor[accessType]);
let restoreStrategy;

if (Object.prototype.hasOwnProperty.call(obj, propertyName)) {
restoreStrategy = function () {
Object.defineProperty(obj, propertyName, originalDescriptor);
};
} else {
restoreStrategy = function () {
delete obj[propertyName];
};
}

currentSpies().push({
restoreObjectToOriginalState: restoreStrategy
});

const spiedDescriptor = Object.assign({}, descriptor, { [accessType]: spiedProperty })

Object.defineProperty(obj, propertyName, spiedDescriptor);

return spiedProperty;
};

this.clearSpies = function() {
const spies = currentSpies();
for (let i = spies.length - 1; i >= 0; i--) {
Expand Down
67 changes: 63 additions & 4 deletions packages/jest-mock/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -672,10 +672,10 @@ class ModuleMockerClass {
if (typeof original !== 'function') {
throw new Error(
'Cannot spy the ' +
methodName +
' property because it is not a function; ' +
this._typeOf(original) +
' given instead',
methodName +
' property because it is not a function; ' +
this._typeOf(original) +
' given instead',
);
}

Expand All @@ -691,6 +691,65 @@ class ModuleMockerClass {
return object[methodName];
}

spyOnProperty(object: any, methodName: any, accessType = 'get'): any {
if (typeof object !== 'object' && typeof object !== 'function') {
throw new Error(
'Cannot spyOn on a primitive value; ' + this._typeOf(object) + ' given',
);
}

if (!obj) {
throw new Error('spyOn could not find an object to spy upon for ' + propertyName + '');
}

if (!propertyName) {
throw new Error('No property name supplied');
}

let descriptor;
try {
descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
} catch (e) {
// IE 8 doesn't support `definePropery` on non-DOM nodes
}

if (!descriptor) {
throw new Error(propertyName + ' property does not exist');
}

if (!descriptor.configurable) {
throw new Error(propertyName + ' is not declared configurable');
}

if (!descriptor[accessType]) {
throw new Error('Property ' + propertyName + ' does not have access type ' + accessType);
}

const original = descriptor[accessType]

if (!this.isMockFunction(original)) {
if (typeof original !== 'function') {
throw new Error(
'Cannot spy the ' +
methodName +
' property because it is not a function; ' +
this._typeOf(original) +
' given instead',
);
}

descriptor[accessType] = this._makeComponent({ type: 'function' }, () => {
descriptor[accessType] = original;
});

descriptor[accessType].mockImplementation(function () {
return original.apply(this, arguments);
});
}

return descriptor[accessType];
}

clearAllMocks() {
this._mockState = new WeakMap();
}
Expand Down
32 changes: 17 additions & 15 deletions packages/jest-runtime/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ import {options as cliOptions} from './cli/args';

type Module = {|
children: Array<Module>,
exports: any,
filename: string,
id: string,
loaded: boolean,
parent?: Module,
paths?: Array<Path>,
require?: (id: string) => any,
exports: any,
filename: string,
id: string,
loaded: boolean,
parent?: Module,
paths?: Array<Path>,
require?: (id: string) => any,
|};

type HasteMapOptions = {|
console?: Console,
maxWorkers: number,
resetCache: boolean,
watch?: boolean,
watchman: boolean,
maxWorkers: number,
resetCache: boolean,
watch?: boolean,
watchman: boolean,
|};

type InternalModuleOptions = {|
Expand Down Expand Up @@ -550,7 +550,7 @@ class Runtime {
filename,
// $FlowFixMe
(localModule.require: LocalModuleRequire),
), // jest object
), // jest object
);

this._isCurrentlyExecutingManualMock = origCurrExecutingManualMock;
Expand Down Expand Up @@ -595,7 +595,7 @@ class Runtime {
if (mockMetadata == null) {
throw new Error(
`Failed to get mock metadata: ${modulePath}\n\n` +
`See: http://facebook.github.io/jest/docs/manual-mocks.html#content`,
`See: http://facebook.github.io/jest/docs/manual-mocks.html#content`,
);
}
this._mockMetaDataCache[modulePath] = mockMetadata;
Expand Down Expand Up @@ -763,13 +763,14 @@ class Runtime {
};
const fn = this._moduleMocker.fn.bind(this._moduleMocker);
const spyOn = this._moduleMocker.spyOn.bind(this._moduleMocker);
const spyOnProperty = this._moduleMocker.spyOnProperty.bind(this._moduleMocker);

const setTimeout = (timeout: number) => {
this._environment.global.jasmine
? (this._environment.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout)
: (this._environment.global[
Symbol.for('TEST_TIMEOUT_SYMBOL')
] = timeout);
Symbol.for('TEST_TIMEOUT_SYMBOL')
] = timeout);
return jestObject;
};

Expand Down Expand Up @@ -811,6 +812,7 @@ class Runtime {
setMockFactory(moduleName, () => mock),
setTimeout,
spyOn,
spyOnProperty,
unmock,
useFakeTimers,
useRealTimers,
Expand Down
1 change: 1 addition & 0 deletions types/Jest.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type Jest = {|
setMock(moduleName: string, moduleExports: any): Jest,
setTimeout(timeout: number): Jest,
spyOn(object: Object, methodName: string): JestMockFn,
spyOnProperty(object: Object, methodName: string, accessType: string): JestMockFn,
unmock(moduleName: string): Jest,
useFakeTimers(): Jest,
useRealTimers(): Jest,
Expand Down

0 comments on commit ab6e1db

Please sign in to comment.