Skip to content

Commit

Permalink
Refactor fake (#2389)
Browse files Browse the repository at this point in the history
* Assume Promise

Since we've dropped support for legacy runtimes (hello IE11), we can
assume `Promise` to be implemented in the runtime and don't need to
detect it.

See https://caniuse.com/promises

* Remove factory function

This was added in 3b80dbb to allow
overriding promise library.

Using a factory function for this purpose was not strictly necessary
then and certainly isn't now.

* Put the exports statement up top

* Re-order code

Code generally reads easier when it goes from high abstraction to low,
like a newspaper article.

* Remove boolean trap

Prefer duplication over adding boolean traps to functions.

See: https://ariya.io/2011/08/hall-of-api-shame-boolean-trap

* Ignore jsdoc's out folder

* Add JSDoc for sinon.fake

* Update lib/sinon/fake.js

Co-authored-by: Kristian Hamilton <krisham87@hotmail.com>
  • Loading branch information
mroderick and khamiltonuk committed Jul 20, 2021
1 parent 57c6c63 commit 15c9539
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 82 deletions.
1 change: 1 addition & 0 deletions .eslintignore
@@ -1,4 +1,5 @@
coverage/
out/
pkg/
tmp/
docs/_site/
Expand Down
1 change: 1 addition & 0 deletions .gitignore
@@ -1,3 +1,4 @@
out/
pkg
tmp/
node_modules
Expand Down
1 change: 1 addition & 0 deletions .prettierignore
@@ -1,4 +1,5 @@
coverage/
out/
pkg/
tmp/
.sass-cache
Expand Down
330 changes: 248 additions & 82 deletions lib/sinon/fake.js
Expand Up @@ -5,117 +5,283 @@ var createProxy = require("./proxy");
var nextTick = require("./util/core/next-tick");

var slice = arrayProto.slice;
var promiseLib = Promise;

function getError(value) {
return value instanceof Error ? value : new Error(value);
}

var uuid = 0;
function wrapFunc(f) {
var proxy;
var fakeInstance = function () {
var firstArg, lastArg;
module.exports = fake;

if (arguments.length > 0) {
firstArg = arguments[0];
lastArg = arguments[arguments.length - 1];
}
/**
* Returns a `fake` that records all calls, arguments and return values.
*
* When an `f` argument is supplied, this implementation will be used.
*
* @example
* // create an empty fake
* var f1 = sinon.fake();
*
* f1();
*
* f1.calledOnce()
* // true
*
* @example
* function greet(greeting) {
* console.log(`Hello ${greeting}`);
* }
*
* // create a fake with implementation
* var f2 = sinon.fake(greet);
*
* // Hello world
* f2("world");
*
* f2.calledWith("world");
* // true
*
* @param {Function|undefined} f
* @returns {Function}
* @namespace
*/
function fake(f) {
if (arguments.length > 0 && typeof f !== "function") {
throw new TypeError("Expected f argument to be a Function");
}

var callback =
lastArg && typeof lastArg === "function" ? lastArg : undefined;
return wrapFunc(f);
}

proxy.firstArg = firstArg;
proxy.lastArg = lastArg;
proxy.callback = callback;
/**
* Creates a `fake` that returns the provided `value`, as well as recording all
* calls, arguments and return values.
*
* @example
* var f1 = sinon.fake.returns(42);
*
* f1();
* // 42
*
* @memberof fake
* @param {*} value
* @returns {Function}
*/
fake.returns = function returns(value) {
// eslint-disable-next-line jsdoc/require-jsdoc
function f() {
return value;
}

return f && f.apply(this, arguments);
};
proxy = createProxy(fakeInstance, f || fakeInstance);
return wrapFunc(f);
};

proxy.displayName = "fake";
proxy.id = `fake#${uuid++}`;
/**
* Creates a `fake` that throws an Error.
* If the `value` argument does not have Error in its prototype chain, it will
* be used for creating a new error.
*
* @example
* var f1 = sinon.fake.throws("hello");
*
* f1();
* // Uncaught Error: hello
*
* @example
* var f2 = sinon.fake.throws(new TypeError("Invalid argument"));
*
* f2();
* // Uncaught TypeError: Invalid argument
*
* @memberof fake
* @param {*|Error} value
* @returns {Function}
*/
fake.throws = function throws(value) {
// eslint-disable-next-line jsdoc/require-jsdoc
function f() {
throw getError(value);
}

return proxy;
}
return wrapFunc(f);
};

function fakeClass() {
var promiseLib = null;
if (typeof Promise === "function") {
promiseLib = Promise;
/**
* Creates a `fake` that returns a promise that resolves to the passed `value`
* argument.
*
* @example
* var f1 = sinon.fake.resolves("apple pie");
*
* await f1();
* // "apple pie"
*
* @memberof fake
* @param {*} value
* @returns {Function}
*/
fake.resolves = function resolves(value) {
// eslint-disable-next-line jsdoc/require-jsdoc
function f() {
return promiseLib.resolve(value);
}

function fake(f) {
if (arguments.length > 0 && typeof f !== "function") {
throw new TypeError("Expected f argument to be a Function");
}
return wrapFunc(f);
};

return wrapFunc(f);
/**
* Creates a `fake` that returns a promise that rejects to the passed `value`
* argument. When `value` does not have Error in its prototype chain, it will be
* wrapped in an Error.
*
* @example
* var f1 = sinon.fake.rejects(":(");
*
* try {
* await ft();
* } catch (error) {
* console.log(error);
* // ":("
* }
*
* @memberof fake
* @param {*} value
* @returns {Function}
*/
fake.rejects = function rejects(value) {
// eslint-disable-next-line jsdoc/require-jsdoc
function f() {
return promiseLib.reject(getError(value));
}

fake.returns = function returns(value) {
function f() {
return value;
}
return wrapFunc(f);
};

return wrapFunc(f);
};
/**
* Causes `fake` to use a custom Promise implementation, instead of the native
* Promise implementation.
*
* @example
* const bluebird = require("bluebird");
* sinon.fake.usingPromise(bluebird);
*
* @memberof fake
* @param {*} promiseLibrary
* @returns {Function}
*/
fake.usingPromise = function usingPromise(promiseLibrary) {
promiseLib = promiseLibrary;
return fake;
};

fake.throws = function throws(value) {
function f() {
throw getError(value);
/**
* Returns a `fake` that calls the callback with the defined arguments.
*
* @example
* function callback() {
* console.log(arguments.join("*"));
* }
*
* const f1 = sinon.fake.yields("apple", "pie");
*
* f1(callback);
* // "apple*pie"
*
* @memberof fake
* @returns {Function}
*/
fake.yields = function yields() {
var values = slice(arguments);

// eslint-disable-next-line jsdoc/require-jsdoc
function f() {
var callback = arguments[arguments.length - 1];
if (typeof callback !== "function") {
throw new TypeError("Expected last argument to be a function");
}

return wrapFunc(f);
};
callback.apply(null, values);
}

fake.resolves = function resolves(value) {
function f() {
return promiseLib.resolve(value);
}
return wrapFunc(f);
};

return wrapFunc(f);
};
/**
* Returns a `fake` that calls the callback **asynchronously** with the
* defined arguments.
*
* @example
* function callback() {
* console.log(arguments.join("*"));
* }
*
* const f1 = sinon.fake.yields("apple", "pie");
*
* f1(callback);
*
* setTimeout(() => {
* // "apple*pie"
* });
*
* @memberof fake
* @returns {Function}
*/
fake.yieldsAsync = function yieldsAsync() {
var values = slice(arguments);

fake.rejects = function rejects(value) {
function f() {
return promiseLib.reject(getError(value));
// eslint-disable-next-line jsdoc/require-jsdoc
function f() {
var callback = arguments[arguments.length - 1];
if (typeof callback !== "function") {
throw new TypeError("Expected last argument to be a function");
}
nextTick(function () {
callback.apply(null, values);
});
}

return wrapFunc(f);
};
return wrapFunc(f);
};

fake.usingPromise = function usingPromise(promiseLibrary) {
promiseLib = promiseLibrary;
return fake;
};
var uuid = 0;
/**
* Creates a proxy (sinon concept) from the passed function.
*
* @private
* @param {Function} f
* @returns {Function}
*/
function wrapFunc(f) {
var proxy;
var fakeInstance = function () {
var firstArg, lastArg;

function yieldInternal(async, values) {
function f() {
var callback = arguments[arguments.length - 1];
if (typeof callback !== "function") {
throw new TypeError("Expected last argument to be a function");
}
if (async) {
nextTick(function () {
callback.apply(null, values);
});
} else {
callback.apply(null, values);
}
if (arguments.length > 0) {
firstArg = arguments[0];
lastArg = arguments[arguments.length - 1];
}

return wrapFunc(f);
}
var callback =
lastArg && typeof lastArg === "function" ? lastArg : undefined;

fake.yields = function yields() {
return yieldInternal(false, slice(arguments));
};
proxy.firstArg = firstArg;
proxy.lastArg = lastArg;
proxy.callback = callback;

fake.yieldsAsync = function yieldsAsync() {
return yieldInternal(true, slice(arguments));
return f && f.apply(this, arguments);
};
proxy = createProxy(fakeInstance, f || fakeInstance);

return fake;
proxy.displayName = "fake";
proxy.id = `fake#${uuid++}`;

return proxy;
}

module.exports = fakeClass();
/**
* Returns an Error instance from the passed value, if the value is not
* already an Error instance.
*
* @private
* @param {*} value [description]
* @returns {Error} [description]
*/
function getError(value) {
return value instanceof Error ? value : new Error(value);
}

0 comments on commit 15c9539

Please sign in to comment.