Skip to content

Commit

Permalink
[enzyme-adapter-utils] [new] add spyMethod and spyProperty
Browse files Browse the repository at this point in the history
This is duplicated from `enzyme` itself, so that adapters can use them without needing to require enzyme v3.12.0

As soon as the adapters have a semver-major bump, and can require v3.12.0 of enzyme, these will be removed
  • Loading branch information
ljharb committed Nov 24, 2020
1 parent c68aff6 commit a3705cf
Show file tree
Hide file tree
Showing 3 changed files with 238 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/enzyme-adapter-utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dependencies": {
"airbnb-prop-types": "^2.16.0",
"function.prototype.name": "^1.1.2",
"has": "^1.0.3",
"object.assign": "^4.1.0",
"object.fromentries": "^2.0.2",
"prop-types": "^15.7.2",
Expand Down
95 changes: 95 additions & 0 deletions packages/enzyme-adapter-utils/src/Utils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functionName from 'function.prototype.name';
import fromEntries from 'object.fromentries';
import has from 'has';
import createMountWrapper from './createMountWrapper';
import createRenderWrapper from './createRenderWrapper';
import wrap from './wrapWithSimpleWrapper';
Expand Down Expand Up @@ -374,3 +375,97 @@ export function compareNodeTypeOf(node, matchingTypeOf) {
}
return node.$$typeof === matchingTypeOf;
}

// TODO: when enzyme v3.12.0 is required, delete this
export function spyMethod(instance, methodName, getStub = () => {}) {
let lastReturnValue;
const originalMethod = instance[methodName];
const hasOwn = has(instance, methodName);
let descriptor;
if (hasOwn) {
descriptor = Object.getOwnPropertyDescriptor(instance, methodName);
}
Object.defineProperty(instance, methodName, {
configurable: true,
enumerable: !descriptor || !!descriptor.enumerable,
value: getStub(originalMethod) || function spied(...args) {
const result = originalMethod.apply(this, args);
lastReturnValue = result;
return result;
},
});
return {
restore() {
if (hasOwn) {
if (descriptor) {
Object.defineProperty(instance, methodName, descriptor);
} else {
/* eslint-disable no-param-reassign */
instance[methodName] = originalMethod;
/* eslint-enable no-param-reassign */
}
} else {
/* eslint-disable no-param-reassign */
delete instance[methodName];
/* eslint-enable no-param-reassign */
}
},
getLastReturnValue() {
return lastReturnValue;
},
};
}

// TODO: when enzyme v3.12.0 is required, delete this
export function spyProperty(instance, propertyName, handlers = {}) {
const originalValue = instance[propertyName];
const hasOwn = has(instance, propertyName);
let descriptor;
if (hasOwn) {
descriptor = Object.getOwnPropertyDescriptor(instance, propertyName);
}
let wasAssigned = false;
let holder = originalValue;
const getV = handlers.get ? () => {
const value = descriptor && descriptor.get ? descriptor.get.call(instance) : holder;
return handlers.get.call(instance, value);
} : () => holder;
const set = handlers.set ? (newValue) => {
wasAssigned = true;
const handlerNewValue = handlers.set.call(instance, holder, newValue);
holder = handlerNewValue;
if (descriptor && descriptor.set) {
descriptor.set.call(instance, holder);
}
} : (v) => {
wasAssigned = true;
holder = v;
};
Object.defineProperty(instance, propertyName, {
configurable: true,
enumerable: !descriptor || !!descriptor.enumerable,
get: getV,
set,
});

return {
restore() {
if (hasOwn) {
if (descriptor) {
Object.defineProperty(instance, propertyName, descriptor);
} else {
/* eslint-disable no-param-reassign */
instance[propertyName] = holder;
/* eslint-enable no-param-reassign */
}
} else {
/* eslint-disable no-param-reassign */
delete instance[propertyName];
/* eslint-enable no-param-reassign */
}
},
wasAssigned() {
return wasAssigned;
},
};
}
142 changes: 142 additions & 0 deletions packages/enzyme-test-suite/test/adapter-utils-spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
assertDomAvailable,
createMountWrapper,
simulateError,
spyMethod,
spyProperty,
} from 'enzyme-adapter-utils';
import wrap from 'mocha-wrap';

Expand Down Expand Up @@ -616,4 +618,144 @@ describe('enzyme-adapter-utils', () => {
expect(getDerivedStateFromError).to.have.property('callCount', 1);
});
});

describe('spyMethod', () => {
it('can spy last return value and restore it', () => {
class Counter {
constructor() {
this.count = 1;
}

incrementAndGet() {
this.count += 1;
return this.count;
}
}
const instance = new Counter();
const obj = {
count: 1,
incrementAndGet() {
this.count += 1;
return this.count;
},
};

// test an instance method and an object property function
const targets = [instance, obj];
targets.forEach((target) => {
const original = target.incrementAndGet;
const spy = spyMethod(target, 'incrementAndGet');
target.incrementAndGet();
target.incrementAndGet();
expect(spy.getLastReturnValue()).to.equal(3);
spy.restore();
expect(target.incrementAndGet).to.equal(original);
expect(target.incrementAndGet()).to.equal(4);
});
});

it('restores the property descriptor', () => {
const obj = {};
const descriptor = {
configurable: true,
enumerable: true,
writable: true,
value: () => {},
};
Object.defineProperty(obj, 'method', descriptor);
const spy = spyMethod(obj, 'method');
spy.restore();
expect(Object.getOwnPropertyDescriptor(obj, 'method')).to.deep.equal(descriptor);
});

it('accepts an optional `getStub` argument', () => {
const obj = {};
const descriptor = {
configurable: true,
enumerable: true,
writable: true,
value: () => {},
};
Object.defineProperty(obj, 'method', descriptor);
let stub;
let original;
spyMethod(obj, 'method', (originalMethod) => {
original = originalMethod;
stub = () => { throw new EvalError('stubbed'); };
return stub;
});
expect(original).to.equal(descriptor.value);
expect(obj).to.have.property('method', stub);
expect(() => obj.method()).to.throw(EvalError);
});
});

describe('spyProperty', () => {
it('can spy "was assigned" status and restore it', () => {
let holder = 1;
const obj = {
count: 1,
get accessor() {
return holder;
},
set accessor(v) {
holder = v;
},
};

// test an instance method and an object property function
const targets = ['count', 'accessor'];
targets.forEach((target) => {
const originalValue = obj[target];

const spy = spyProperty(obj, target);

obj[target] += 1;

expect(spy.wasAssigned()).to.equal(true);

spy.restore();

expect(obj[target]).to.equal(originalValue);
});
});

it('restores the property descriptor', () => {
const obj = {};
const descriptor = {
configurable: true,
enumerable: true,
writable: true,
value: () => {},
};
const propertyName = 'foo';
Object.defineProperty(obj, propertyName, descriptor);
const spy = spyMethod(obj, propertyName);
spy.restore();
expect(Object.getOwnPropertyDescriptor(obj, propertyName)).to.deep.equal(descriptor);
});

it('accepts an optional `handlers` argument', () => {
const getSpy = sinon.stub().returns(1);
const setSpy = sinon.stub().returns(2);

const propertyName = 'foo';
const obj = {
[propertyName]: 1,
};

const spy = spyProperty(obj, propertyName, { get: getSpy, set: setSpy });

obj[propertyName] += 1;

spy.restore();

expect(getSpy.args).to.deep.equal([
[1],
]);
expect(setSpy.args).to.deep.equal([
[1, 2],
]);
});
});
});

0 comments on commit a3705cf

Please sign in to comment.