UNPKG

sinon

Version:

JavaScript test spies, stubs and mocks.

589 lines (482 loc) 17 kB
'use strict'; var commons = require('@sinonjs/commons'); var samsam = require('@sinonjs/samsam'); var collectOwnMethods = require('./collect-own-methods.js'); var getPropertyDescriptor = require('./util/core/get-property-descriptor.js'); var assert = require('./assert.js'); var fakeTimers = require('./util/fake-timers.js'); var mock = require('./mock.js'); var spy = require('./spy.js'); var stub = require('./stub.js'); var createStubInstance = require('./create-stub-instance.js'); var fake = require('./fake.js'); var extend = require('./util/core/extend.js'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var commons__default = /*#__PURE__*/_interopDefault(commons); var samsam__default = /*#__PURE__*/_interopDefault(samsam); const { array: arrayProto } = commons__default.default.prototypes; const { deprecated: logger, valueToString } = commons__default.default; const { createMatcher: match } = samsam__default.default; const DEFAULT_LEAK_THRESHOLD = 10000; const filter = arrayProto.filter; /** * @callback RestorerFunction * @returns {void} */ const forEach = arrayProto.forEach; const push = arrayProto.push; const reverse = arrayProto.reverse; function applyOnEach(fakes, method) { const matchingFakes = filter(fakes, function (fake) { return typeof fake[method] === "function"; }); forEach(matchingFakes, function (fake) { fake[method](); }); } function throwOnAccessors(descriptor) { if (typeof descriptor.get === "function") { throw new Error("Use sandbox.replaceGetter for replacing getters"); } if (typeof descriptor.set === "function") { throw new Error("Use sandbox.replaceSetter for replacing setters"); } } function verifySameType(object, property, replacement) { const original = object[property]; const originalType = typeof original; const replacementType = typeof replacement; if (originalType !== replacementType) { throw new TypeError( `Cannot replace ${originalType} with ${replacementType}`, ); } } function checkForValidArguments(descriptor, property, replacement) { if (typeof descriptor === "undefined") { throw new TypeError( `Cannot replace non-existent property ${valueToString( property, )}. Perhaps you meant sandbox.define()?`, ); } if (typeof replacement === "undefined") { throw new TypeError("Expected replacement argument to be defined"); } } /** * Creates a sandbox. * * @param {object} [opts] Options for the sandbox * @returns {object} The sandbox object * @class */ function Sandbox(opts = {}) { const sandbox = this; const assertOptions = opts.assertOptions || {}; const fakeRestorers = []; let collection = []; let loggedLeakWarning = false; sandbox.leakThreshold = DEFAULT_LEAK_THRESHOLD; function addToCollection(object) { if ( push(collection, object) > sandbox.leakThreshold && !loggedLeakWarning ) { logger.printWarning( `Sinon sandbox: The number of fakes in the sandbox has exceeded the leak threshold of ${sandbox.leakThreshold}. ` + "To avoid memory leaks, ensure you are restoring the sandbox after each test. " + "To disable this warning, modify the leakThreshold property of your sandbox.", ); loggedLeakWarning = true; } } sandbox.assert = assert.createAssertObject(assertOptions); // this is for testing only sandbox.getFakes = function getFakes() { return collection; }; sandbox.createStubInstance = function createStubInstance$1() { const stubbed = createStubInstance.apply(null, arguments); const ownMethods = collectOwnMethods(stubbed); forEach(ownMethods, function (method) { addToCollection(method); }); return stubbed; }; sandbox.inject = function inject(obj) { obj.spy = function spy() { return sandbox.spy.apply(null, arguments); }; obj.stub = function stub() { return sandbox.stub.apply(null, arguments); }; obj.mock = function mock() { return sandbox.mock.apply(null, arguments); }; obj.createStubInstance = function createStubInstanceWrapper() { return sandbox.createStubInstance.apply(sandbox, arguments); }; obj.fake = function fake() { return sandbox.fake.apply(null, arguments); }; obj.define = function define() { return sandbox.define.apply(null, arguments); }; obj.replace = function replace() { return sandbox.replace.apply(null, arguments); }; obj.replaceSetter = function replaceSetter() { return sandbox.replaceSetter.apply(null, arguments); }; obj.replaceGetter = function replaceGetter() { return sandbox.replaceGetter.apply(null, arguments); }; if (sandbox.clock) { obj.clock = sandbox.clock; } obj.match = match; return obj; }; function commonPostInitSetup( args, spy, isStub, shouldAddToCollection = true, ) { if (isStub && args.length >= 3) { throw new TypeError( "stub(obj, 'meth', fn) has been removed, see documentation", ); } if (shouldAddToCollection) { addToCollection(spy); } return spy; } function addReturnedMethodsToCollection(result) { if ( result && (typeof result === "object" || typeof result === "function") ) { forEach(collectOwnMethods(result), addToCollection); } } sandbox.spy = function () { const createdSpy = spy.apply(spy, arguments); const result = commonPostInitSetup( arguments, createdSpy, false, !(arguments.length === 1 && typeof arguments[0] === "object"), ); addReturnedMethodsToCollection(result); return result; }; Object.defineProperty(sandbox.spy, "name", { value: "spy", configurable: true, }); Object.defineProperty(sandbox.spy, "length", { value: 0, configurable: true, }); extend(sandbox.spy, spy); sandbox.stub = function () { const createdStub = stub.apply(stub, arguments); const result = commonPostInitSetup( arguments, createdStub, true, !(arguments.length === 1 && typeof arguments[0] === "object"), ); addReturnedMethodsToCollection(result); return result; }; Object.defineProperty(sandbox.stub, "name", { value: "stub", configurable: true, }); Object.defineProperty(sandbox.stub, "length", { value: 0, configurable: true, }); extend(sandbox.stub, stub); sandbox.mock = function () { const m = mock.apply(null, arguments); addToCollection(m); return m; }; Object.defineProperty(sandbox.mock, "name", { value: "mock", configurable: true, }); Object.defineProperty(sandbox.mock, "length", { value: 0, configurable: true, }); extend(sandbox.mock, mock); sandbox.reset = function reset() { applyOnEach(collection, "reset"); applyOnEach(collection, "resetHistory"); }; sandbox.resetBehavior = function resetBehavior() { applyOnEach(collection, "resetBehavior"); }; sandbox.resetHistory = function resetHistory() { for (let i = 0; i < collection.length; i++) { const f = collection[i]; const method = f.resetHistory || f.reset; if (typeof method === "function") { method.call(f); } } }; sandbox.verify = function verify() { applyOnEach(collection, "verify"); }; sandbox.verifyAndRestore = function verifyAndRestore() { let exception; try { sandbox.verify(); } catch (e) { exception = e; } sandbox.restore(); if (exception) { throw exception; } }; sandbox.restore = function restore() { if (arguments.length) { throw new Error( "sandbox.restore() does not take any parameters. Perhaps you meant stub.restore()", ); } reverse(fakeRestorers); forEach(fakeRestorers, function (restorer) { restorer(); }); fakeRestorers.length = 0; reverse(collection); applyOnEach(collection, "restore"); collection = []; }; sandbox.restoreContext = function restoreContext() { forEach(sandbox.injectedKeys, function (injectedKey) { delete sandbox.injectInto[injectedKey]; }); sandbox.injectedKeys.length = 0; }; /** * Creates a restorer function for the property * @param {object} object the object containing the property * @param {string} property the name of the property * @param {boolean} [forceAssignment] if true, uses assignment instead of DefineProperty * @returns {RestorerFunction} restorer function */ function getFakeRestorer(object, property, forceAssignment = false) { const descriptor = getPropertyDescriptor(object, property); const value = forceAssignment && object[property]; function restorer() { if (forceAssignment) { object[property] = value; } else if (descriptor?.isOwn) { Object.defineProperty(object, property, descriptor); } else { delete object[property]; } } restorer.sinon = true; restorer.object = object; restorer.property = property; return restorer; } function verifyNotReplaced(object, property) { forEach(fakeRestorers, function (fakeRestorer) { if ( fakeRestorer.object === object && fakeRestorer.property === property ) { throw new TypeError( `Attempted to replace ${valueToString( property, )} which is already replaced`, ); } }); } sandbox.replace = function replace(object, property, replacement) { const descriptor = getPropertyDescriptor(object, property); checkForValidArguments(descriptor, property, replacement); verifyNotReplaced(object, property); throwOnAccessors(descriptor); verifySameType(object, property, replacement); // store a function for restoring the replaced property push(fakeRestorers, getFakeRestorer(object, property)); object[property] = replacement; return replacement; }; sandbox.replace.usingAccessor = function replaceUsingAccessor( object, property, replacement, ) { const descriptor = getPropertyDescriptor(object, property); checkForValidArguments(descriptor, property, replacement); verifyNotReplaced(object, property); verifySameType(object, property, replacement); // store a function for restoring the replaced property push(fakeRestorers, getFakeRestorer(object, property, true)); object[property] = replacement; return replacement; }; sandbox.define = function define(object, property, value) { const descriptor = getPropertyDescriptor(object, property); if (typeof property === "undefined") { throw new TypeError( `Cannot define the already existing property ${valueToString( property, )}. Perhaps you meant sandbox.replace()?`, ); } if (descriptor && descriptor.isOwn) { throw new TypeError( `Cannot define the already existing property ${valueToString( property, )}. Perhaps you meant sandbox.replace()?`, ); } // store a function for restoring the defined property push(fakeRestorers, getFakeRestorer(object, property)); Object.defineProperty(object, property, { value: value, configurable: true, enumerable: true, writable: true, }); return value; }; sandbox.replaceSetter = function replaceSetter( object, property, replacement, ) { const descriptor = getPropertyDescriptor(object, property); if (typeof descriptor === "undefined") { throw new TypeError( `Cannot replace non-existent property ${valueToString( property, )}`, ); } if (typeof replacement !== "function") { throw new TypeError( "Expected replacement argument to be a function", ); } verifyNotReplaced(object, property); if (typeof descriptor.set !== "function") { throw new Error("`object.property` is not a setter"); } if (!descriptor.configurable) { throw new TypeError( `Descriptor for property ${valueToString( property, )} is not configurable`, ); } // store a function for restoring the replaced property push(fakeRestorers, getFakeRestorer(object, property)); // eslint-disable-next-line accessor-pairs Object.defineProperty(object, property, { set: replacement, configurable: true, enumerable: descriptor.enumerable, }); return replacement; }; sandbox.replaceGetter = function replaceGetter( object, property, replacement, ) { const descriptor = getPropertyDescriptor(object, property); if (typeof descriptor === "undefined") { throw new TypeError( `Cannot replace non-existent property ${valueToString( property, )}`, ); } if (typeof replacement !== "function") { throw new TypeError( "Expected replacement argument to be a function", ); } verifyNotReplaced(object, property); if (typeof descriptor.get !== "function") { throw new Error("`object.property` is not a getter"); } if (!descriptor.configurable) { throw new TypeError( `Descriptor for property ${valueToString( property, )} is not configurable`, ); } // store a function for property for restoring the replaced property push(fakeRestorers, getFakeRestorer(object, property)); Object.defineProperty(object, property, { get: replacement, configurable: true, enumerable: descriptor.enumerable, }); return replacement; }; sandbox.useFakeTimers = function useFakeTimers(args) { const clock = fakeTimers.useFakeTimers.call(null, args); sandbox.clock = clock; addToCollection(clock); return clock; }; sandbox.fake = function fake$1() { const createdFake = fake.apply(fake, arguments); const result = commonPostInitSetup(arguments, createdFake, false); addToCollection(result); return result; }; Object.defineProperty(sandbox.fake, "name", { value: "fake", configurable: true, }); Object.defineProperty(sandbox.fake, "length", { value: 1, configurable: true, }); extend(sandbox.fake, fake); Object.defineProperty(sandbox.fake, "name", { value: "fake", configurable: true, }); Object.defineProperty(sandbox.fake, "length", { value: 1, configurable: true, }); function addFakeBehaviorToCollection(method) { const original = sandbox.fake[method]; sandbox.fake[method] = function () { const result = original.apply(fake, arguments); addToCollection(result); return result; }; } addFakeBehaviorToCollection("returns"); addFakeBehaviorToCollection("throws"); addFakeBehaviorToCollection("resolves"); addFakeBehaviorToCollection("rejects"); addFakeBehaviorToCollection("yields"); addFakeBehaviorToCollection("yieldsAsync"); } Sandbox.prototype.match = match; module.exports = Sandbox;