Skip to content

Commit

Permalink
Add sinon.promise() implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mantoni committed May 6, 2021
1 parent 6b538b6 commit a38ea2c
Show file tree
Hide file tree
Showing 3 changed files with 313 additions and 0 deletions.
4 changes: 4 additions & 0 deletions lib/sinon.js
Expand Up @@ -8,6 +8,7 @@ var format = require("./sinon/util/core/format");
var nise = require("nise");
var Sandbox = require("./sinon/sandbox");
var stub = require("./sinon/stub");
var promise = require("./sinon/promise");

var apiMethods = {
createSandbox: createSandbox,
Expand Down Expand Up @@ -38,6 +39,9 @@ var apiMethods = {
addBehavior: function (name, fn) {
behavior.addBehavior(stub, name, fn);
},

// fake promise
promise: promise,
};

var sandbox = new Sandbox();
Expand Down
84 changes: 84 additions & 0 deletions lib/sinon/promise.js
@@ -0,0 +1,84 @@
"use strict";

var fake = require("./fake");
var isRestorable = require("./util/core/is-restorable");

var STATUS_PENDING = "pending";
var STATUS_FULFILLED = "fulfilled";
var STATUS_REJECTED = "rejected";

/**
* Returns a fake for a given function or undefined. If no functino is given, a
* new fake is returned. If the given function is already a fake, it is
* returned as is. Otherwise the given function is wrapped in a new fake.
*
* @param {Function} [executor] The optional executor function.
* @returns {Function}
*/
function getFakeExecutor(executor) {
if (isRestorable(executor)) {
return executor;
}
if (executor) {
return fake(executor);
}
return fake();
}

/**
* Returns a new promise that exposes it's internal `status`, `resolvedValue`
* and `rejectedValue` and can be resolved or rejected from the outside by
* calling `resolve(value)` or `reject(reason)`.
*
* @param {Function} [executor] The optional executor function.
* @returns {Promise}
*/
function promise(executor) {
var fakeExecutor = getFakeExecutor(executor);
var sinonPromise = new Promise(fakeExecutor);

sinonPromise.status = STATUS_PENDING;
sinonPromise
.then(function (value) {
sinonPromise.status = STATUS_FULFILLED;
sinonPromise.resolvedValue = value;
})
.catch(function (reason) {
sinonPromise.status = STATUS_REJECTED;
sinonPromise.rejectedValue = reason;
});

/**
* Fulfills or rejects the promise with the given status and value.
*
* @param {string} status
* @param {*} value
* @param {Function} callback
*/
function finalize(status, value, callback) {
if (sinonPromise.status === STATUS_PENDING) {
sinonPromise.status = status;
callback(value);
return;
}
throw new Error(`Promise already ${sinonPromise.status}`);
}

sinonPromise.resolve = function (value) {
finalize(STATUS_FULFILLED, value, fakeExecutor.firstCall.args[0]);
// Return the promise so that callers can await it:
return sinonPromise;
};
sinonPromise.reject = function (reason) {
finalize(STATUS_REJECTED, reason, fakeExecutor.firstCall.args[1]);
// Return a new promise that resolves when the sinon promise was
// rejected, so that callers can await it:
return new Promise(function (resolve) {
sinonPromise.catch(() => resolve());
});
};

return sinonPromise;
}

module.exports = promise;
225 changes: 225 additions & 0 deletions test/promise-test.js
@@ -0,0 +1,225 @@
"use strict";

var sinon = require("../lib/sinon.js");
var { assert, refute } = require("@sinonjs/referee");

async function getPromiseStatus(promise) {
var status = "pending";
var value = null;
promise
.then(function (val) {
status = "fulfilled";
value = val;
})
.catch(function (reason) {
status = "rejected";
value = reason;
});
await new Promise(function (resolve) {
setTimeout(resolve, 0);
});
return { status, value };
}

describe("promise", function () {
context("with default executor", function () {
it("returns an unresolved promise", async function () {
var promise = sinon.promise();

var { status, value } = await getPromiseStatus(promise);
assert.equals(promise.toString(), "[object Promise]");
assert.equals(status, "pending");
assert.isNull(value);
assert.equals(promise.status, status);
assert.isUndefined(promise.resolvedValue);
assert.isUndefined(promise.rejectedValue);
});

it("resolves the promise", async function () {
var result = Symbol("promise result");
var promise = sinon.promise();

var returnValue = promise.resolve(result);

var { status, value } = await getPromiseStatus(promise);
assert.equals(status, "fulfilled");
assert.same(value, result);
assert.equals(promise.status, status);
assert.same(promise.resolvedValue, result);
assert.isUndefined(promise.rejectedValue);
assert.same(returnValue, promise);
});

it("rejects the promise", async function () {
var error = new Error("promise error");
var promise = sinon.promise();

var returnValue = promise.reject(error);

var { status, value } = await getPromiseStatus(promise);
assert.equals(status, "rejected");
assert.same(value, error);
assert.equals(promise.status, status);
assert.isUndefined(promise.resolvedValue);
assert.same(promise.rejectedValue, error);
refute.same(returnValue, promise);
assert.equals(returnValue.toString(), "[object Promise]");
await assert.resolves(returnValue, undefined);
});

context("with resolved promise", function () {
var promise;

beforeEach(function () {
promise = sinon.promise();
promise.resolve(1);
});

it("fails to resolve again", function () {
assert.exception(
() => {
promise.resolve(2);
},
{
name: "Error",
message: "Promise already fulfilled",
}
);
});

it("fails to reject", function () {
assert.exception(
() => {
promise.reject(2);
},
{
name: "Error",
message: "Promise already fulfilled",
}
);
});
});

context("with rejected promise", function () {
var promise;

beforeEach(function () {
promise = sinon.promise();
promise.reject(1);
});

it("fails to reject again", function () {
assert.exception(
() => {
promise.reject(2);
},
{
name: "Error",
message: "Promise already rejected",
}
);
});

it("fails to resolve", function () {
assert.exception(
() => {
promise.resolve(2);
},
{
name: "Error",
message: "Promise already rejected",
}
);
});
});
});

context("with custom executor", function () {
it("accepts a fake as the custom executor", function () {
var executor = sinon.fake();

sinon.promise(executor);

assert.equals(executor.callCount, 1);
assert.equals(executor.firstCall.args.length, 2);
assert.isFunction(executor.firstCall.firstArg);
assert.isFunction(executor.firstCall.lastArg);
});

it("accepts a stub as the custom executor", function () {
var executor = sinon.stub();

sinon.promise(executor);

assert.equals(executor.callCount, 1);
assert.equals(executor.firstCall.args.length, 2);
assert.isFunction(executor.firstCall.firstArg);
assert.isFunction(executor.firstCall.lastArg);
});

it("accepts a function as the custom executor", function () {
var args;
function executor(resolve, reject) {
args = [resolve, reject];
}

sinon.promise(executor);

assert.equals(args.length, 2);
assert.isFunction(args[0]);
assert.isFunction(args[1]);
});

it("sets resolvedValue when custom executor resolves", async function () {
var result = Symbol("executor result");
function executor(resolve) {
resolve(result);
}

var promise = sinon.promise(executor);

await assert.resolves(promise, result);
assert.equals(promise.status, "fulfilled");
assert.same(promise.resolvedValue, result);
assert.isUndefined(promise.rejectedValue);
});

it("sets rejectedValue when custom executor fails", async function () {
var reason = new Error("executor failure");
function executor(resolve, reject) {
reject(reason);
}

var promise = sinon.promise(executor);

await assert.rejects(promise, reason);
assert.equals(promise.status, "rejected");
assert.same(promise.rejectedValue, reason);
assert.isUndefined(promise.resolvedValue);
});

it("resolves the promise", async function () {
var result = Symbol("promise result");
var promise = sinon.promise(sinon.fake());

promise.resolve(result);

await assert.resolves(promise, result);
assert.equals(promise.status, "fulfilled");
assert.same(promise.resolvedValue, result);
assert.isUndefined(promise.rejectedValue);
});

it("rejects the promise", async function () {
var error = new Error("promise error");
var promise = sinon.promise(sinon.fake());

promise.reject(error);

await assert.rejects(promise, error);
assert.equals(promise.status, "rejected");
assert.isUndefined(promise.resolvedValue);
assert.same(promise.rejectedValue, error);
});
});
});

0 comments on commit a38ea2c

Please sign in to comment.