From ab6e1db215465451c7f0eeac95a4101e5605b68d Mon Sep 17 00:00:00 2001 From: phra Date: Mon, 18 Dec 2017 14:35:40 +0100 Subject: [PATCH] feat: implement spyOnProperty method, fixes #5106 --- flow-typed/npm/jest_v21.x.x.js | 1 + packages/jest-jasmine2/src/index.js | 3 +- packages/jest-jasmine2/src/jasmine/Env.js | 12 ++-- .../src/jasmine/jasmine_light.js | 4 ++ .../jest-jasmine2/src/jasmine/spy_registry.js | 63 +++++++++++++++++ packages/jest-mock/src/index.js | 67 +++++++++++++++++-- packages/jest-runtime/src/index.js | 32 ++++----- types/Jest.js | 1 + 8 files changed, 159 insertions(+), 24 deletions(-) diff --git a/flow-typed/npm/jest_v21.x.x.js b/flow-typed/npm/jest_v21.x.x.js index 4a467880c095..8dc63523cd46 100644 --- a/flow-typed/npm/jest_v21.x.x.js +++ b/flow-typed/npm/jest_v21.x.x.js @@ -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; diff --git a/packages/jest-jasmine2/src/index.js b/packages/jest-jasmine2/src/index.js index 8dc13639b70e..b8bb08448b96 100644 --- a/packages/jest-jasmine2/src/index.js +++ b/packages/jest-jasmine2/src/index.js @@ -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(); } diff --git a/packages/jest-jasmine2/src/jasmine/Env.js b/packages/jest-jasmine2/src/jasmine/Env.js index c0cc29f7f7cc..b542262f193f 100644 --- a/packages/jest-jasmine2/src/jasmine/Env.js +++ b/packages/jest-jasmine2/src/jasmine/Env.js @@ -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(), @@ -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); diff --git a/packages/jest-jasmine2/src/jasmine/jasmine_light.js b/packages/jest-jasmine2/src/jasmine/jasmine_light.js index 68f4ac1122dd..b9ebb0c1b28f 100644 --- a/packages/jest-jasmine2/src/jasmine/jasmine_light.js +++ b/packages/jest-jasmine2/src/jasmine/jasmine_light.js @@ -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(), }), diff --git a/packages/jest-jasmine2/src/jasmine/spy_registry.js b/packages/jest-jasmine2/src/jasmine/spy_registry.js index 31d156a7fadb..74ee51265c13 100644 --- a/packages/jest-jasmine2/src/jasmine/spy_registry.js +++ b/packages/jest-jasmine2/src/jasmine/spy_registry.js @@ -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--) { diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.js index 33019e3edb51..493eef0cf812 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.js @@ -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', ); } @@ -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(); } diff --git a/packages/jest-runtime/src/index.js b/packages/jest-runtime/src/index.js index 9a322f6fc32d..01c3e86a675b 100644 --- a/packages/jest-runtime/src/index.js +++ b/packages/jest-runtime/src/index.js @@ -30,21 +30,21 @@ import {options as cliOptions} from './cli/args'; type Module = {| children: Array, - exports: any, - filename: string, - id: string, - loaded: boolean, - parent?: Module, - paths?: Array, - require?: (id: string) => any, + exports: any, + filename: string, + id: string, + loaded: boolean, + parent?: Module, + paths?: Array, + 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 = {| @@ -550,7 +550,7 @@ class Runtime { filename, // $FlowFixMe (localModule.require: LocalModuleRequire), - ), // jest object + ), // jest object ); this._isCurrentlyExecutingManualMock = origCurrExecutingManualMock; @@ -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; @@ -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; }; @@ -811,6 +812,7 @@ class Runtime { setMockFactory(moduleName, () => mock), setTimeout, spyOn, + spyOnProperty, unmock, useFakeTimers, useRealTimers, diff --git a/types/Jest.js b/types/Jest.js index 3b04e043e361..6007812c1dc3 100644 --- a/types/Jest.js +++ b/types/Jest.js @@ -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,