Skip to content

Commit 98acdff

Browse files
committed
feat: implement spyOnProperty method, fixes #5106
1 parent 011951a commit 98acdff

File tree

8 files changed

+159
-24
lines changed

8 files changed

+159
-24
lines changed

flow-typed/npm/jest_v21.x.x.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,7 @@ declare var expect: {
555555
// TODO handle return type
556556
// http://jasmine.github.io/2.4/introduction.html#section-Spies
557557
declare function spyOn(value: mixed, method: string): Object;
558+
declare function spyOnProperty(value: mixed, propertyName: string, accessType: 'get' | 'set'): Object;
558559

559560
/** Holds all functions related to manipulating test runner */
560561
declare var jest: JestObjectType;

packages/jest-jasmine2/src/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,9 +161,10 @@ const addSnapshotData = (results, snapshotState) => {
161161
});
162162

163163
const uncheckedCount = snapshotState.getUncheckedCount();
164-
const uncheckedKeys = snapshotState.getUncheckedKeys();
164+
let uncheckedKeys
165165

166166
if (uncheckedCount) {
167+
uncheckedKeys = snapshotState.getUncheckedKeys();
167168
snapshotState.removeUncheckedKeys();
168169
}
169170

packages/jest-jasmine2/src/jasmine/Env.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,10 @@ export default function(j$) {
285285
return spyRegistry.spyOn.apply(spyRegistry, arguments);
286286
};
287287

288+
this.spyOnProperty = function() {
289+
return spyRegistry.spyOnProperty.apply(spyRegistry, arguments);
290+
};
291+
288292
const suiteFactory = function(description) {
289293
const suite = new j$.Suite({
290294
id: getNextSuiteId(),
@@ -434,10 +438,10 @@ export default function(j$) {
434438
if (currentSpec !== null) {
435439
throw new Error(
436440
'Tests cannot be nested. Test `' +
437-
spec.description +
438-
'` cannot run because it is nested within `' +
439-
currentSpec.description +
440-
'`.',
441+
spec.description +
442+
'` cannot run because it is nested within `' +
443+
currentSpec.description +
444+
'`.',
441445
);
442446
}
443447
currentDeclarationSuite.addChild(spec);

packages/jest-jasmine2/src/jasmine/jasmine_light.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ exports.interface = function(jasmine: Jasmine, env: any) {
120120
return env.spyOn(obj, methodName);
121121
},
122122

123+
spyOnProperty: function (obj: Object, methodName: string, accessType = 'get') {
124+
return env.spyOnProperty(obj, methodName, accessType);
125+
},
126+
123127
jsApiReporter: new jasmine.JsApiReporter({
124128
timer: new jasmine.Timer(),
125129
}),

packages/jest-jasmine2/src/jasmine/spy_registry.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,69 @@ export default function SpyRegistry(options: Object) {
129129
return spiedMethod;
130130
};
131131

132+
this.spyOnProperty = function(obj, propertyName, accessType = 'get') {
133+
if (!obj) {
134+
throw new Error('spyOn could not find an object to spy upon for ' + propertyName + '');
135+
}
136+
137+
if (!propertyName) {
138+
throw new Error('No property name supplied');
139+
}
140+
141+
let descriptor;
142+
try {
143+
descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
144+
} catch (e) {
145+
// IE 8 doesn't support `definePropery` on non-DOM nodes
146+
}
147+
148+
if (!descriptor) {
149+
throw new Error(propertyName + ' property does not exist');
150+
}
151+
152+
if (!descriptor.configurable) {
153+
throw new Error(propertyName + ' is not declared configurable');
154+
}
155+
156+
if (!descriptor[accessType]) {
157+
throw new Error('Property ' + propertyName + ' does not have access type ' + accessType);
158+
}
159+
160+
if (obj[propertyName] && isSpy(obj[propertyName])) {
161+
if (this.respy) {
162+
return obj[propertyName];
163+
} else {
164+
throw new Error(
165+
getErrorMsg(propertyName + ' has already been spied upon'),
166+
);
167+
}
168+
}
169+
170+
const originalDescriptor = descriptor;
171+
const spiedProperty = createSpy(propertyName, descriptor[accessType]);
172+
let restoreStrategy;
173+
174+
if (Object.prototype.hasOwnProperty.call(obj, propertyName)) {
175+
restoreStrategy = function () {
176+
Object.defineProperty(obj, propertyName, originalDescriptor);
177+
};
178+
} else {
179+
restoreStrategy = function () {
180+
delete obj[propertyName];
181+
};
182+
}
183+
184+
currentSpies().push({
185+
restoreObjectToOriginalState: restoreStrategy
186+
});
187+
188+
const spiedDescriptor = Object.assign({}, descriptor, { [accessType]: spiedProperty })
189+
190+
Object.defineProperty(obj, propertyName, spiedDescriptor);
191+
192+
return spiedProperty;
193+
};
194+
132195
this.clearSpies = function() {
133196
const spies = currentSpies();
134197
for (let i = spies.length - 1; i >= 0; i--) {

packages/jest-mock/src/index.js

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -672,10 +672,10 @@ class ModuleMockerClass {
672672
if (typeof original !== 'function') {
673673
throw new Error(
674674
'Cannot spy the ' +
675-
methodName +
676-
' property because it is not a function; ' +
677-
this._typeOf(original) +
678-
' given instead',
675+
methodName +
676+
' property because it is not a function; ' +
677+
this._typeOf(original) +
678+
' given instead',
679679
);
680680
}
681681

@@ -691,6 +691,65 @@ class ModuleMockerClass {
691691
return object[methodName];
692692
}
693693

694+
spyOnProperty(object: any, propertyName: any, accessType = 'get'): any {
695+
if (typeof object !== 'object' && typeof object !== 'function') {
696+
throw new Error(
697+
'Cannot spyOn on a primitive value; ' + this._typeOf(object) + ' given',
698+
);
699+
}
700+
701+
if (!obj) {
702+
throw new Error('spyOn could not find an object to spy upon for ' + propertyName + '');
703+
}
704+
705+
if (!propertyName) {
706+
throw new Error('No property name supplied');
707+
}
708+
709+
let descriptor;
710+
try {
711+
descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
712+
} catch (e) {
713+
// IE 8 doesn't support `definePropery` on non-DOM nodes
714+
}
715+
716+
if (!descriptor) {
717+
throw new Error(propertyName + ' property does not exist');
718+
}
719+
720+
if (!descriptor.configurable) {
721+
throw new Error(propertyName + ' is not declared configurable');
722+
}
723+
724+
if (!descriptor[accessType]) {
725+
throw new Error('Property ' + propertyName + ' does not have access type ' + accessType);
726+
}
727+
728+
const original = descriptor[accessType]
729+
730+
if (!this.isMockFunction(original)) {
731+
if (typeof original !== 'function') {
732+
throw new Error(
733+
'Cannot spy the ' +
734+
methodName +
735+
' property because it is not a function; ' +
736+
this._typeOf(original) +
737+
' given instead',
738+
);
739+
}
740+
741+
descriptor[accessType] = this._makeComponent({ type: 'function' }, () => {
742+
descriptor[accessType] = original;
743+
});
744+
745+
descriptor[accessType].mockImplementation(function () {
746+
return original.apply(this, arguments);
747+
});
748+
}
749+
750+
return descriptor[accessType];
751+
}
752+
694753
clearAllMocks() {
695754
this._mockState = new WeakMap();
696755
}

packages/jest-runtime/src/index.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,21 @@ import {options as cliOptions} from './cli/args';
3030

3131
type Module = {|
3232
children: Array<Module>,
33-
exports: any,
34-
filename: string,
35-
id: string,
36-
loaded: boolean,
37-
parent?: Module,
38-
paths?: Array<Path>,
39-
require?: (id: string) => any,
33+
exports: any,
34+
filename: string,
35+
id: string,
36+
loaded: boolean,
37+
parent?: Module,
38+
paths?: Array<Path>,
39+
require?: (id: string) => any,
4040
|};
4141

4242
type HasteMapOptions = {|
4343
console?: Console,
44-
maxWorkers: number,
45-
resetCache: boolean,
46-
watch?: boolean,
47-
watchman: boolean,
44+
maxWorkers: number,
45+
resetCache: boolean,
46+
watch?: boolean,
47+
watchman: boolean,
4848
|};
4949

5050
type InternalModuleOptions = {|
@@ -550,7 +550,7 @@ class Runtime {
550550
filename,
551551
// $FlowFixMe
552552
(localModule.require: LocalModuleRequire),
553-
), // jest object
553+
), // jest object
554554
);
555555

556556
this._isCurrentlyExecutingManualMock = origCurrExecutingManualMock;
@@ -595,7 +595,7 @@ class Runtime {
595595
if (mockMetadata == null) {
596596
throw new Error(
597597
`Failed to get mock metadata: ${modulePath}\n\n` +
598-
`See: http://facebook.github.io/jest/docs/manual-mocks.html#content`,
598+
`See: http://facebook.github.io/jest/docs/manual-mocks.html#content`,
599599
);
600600
}
601601
this._mockMetaDataCache[modulePath] = mockMetadata;
@@ -763,13 +763,14 @@ class Runtime {
763763
};
764764
const fn = this._moduleMocker.fn.bind(this._moduleMocker);
765765
const spyOn = this._moduleMocker.spyOn.bind(this._moduleMocker);
766+
const spyOnProperty = this._moduleMocker.spyOnProperty.bind(this._moduleMocker);
766767

767768
const setTimeout = (timeout: number) => {
768769
this._environment.global.jasmine
769770
? (this._environment.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout)
770771
: (this._environment.global[
771-
Symbol.for('TEST_TIMEOUT_SYMBOL')
772-
] = timeout);
772+
Symbol.for('TEST_TIMEOUT_SYMBOL')
773+
] = timeout);
773774
return jestObject;
774775
};
775776

@@ -811,6 +812,7 @@ class Runtime {
811812
setMockFactory(moduleName, () => mock),
812813
setTimeout,
813814
spyOn,
815+
spyOnProperty,
814816
unmock,
815817
useFakeTimers,
816818
useRealTimers,

types/Jest.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export type Jest = {|
4444
setMock(moduleName: string, moduleExports: any): Jest,
4545
setTimeout(timeout: number): Jest,
4646
spyOn(object: Object, methodName: string): JestMockFn,
47+
spyOnProperty(object: Object, methodName: string, accessType: string): JestMockFn,
4748
unmock(moduleName: string): Jest,
4849
useFakeTimers(): Jest,
4950
useRealTimers(): Jest,

0 commit comments

Comments
 (0)