From 9c479c31b106450d87fecb472f366f866eff8f77 Mon Sep 17 00:00:00 2001 From: Maximilian Antoni Date: Thu, 6 May 2021 20:24:18 +0200 Subject: [PATCH] Add sinon.promise() implementation --- lib/sinon.js | 4 + lib/sinon/promise.js | 77 +++++++++++++++ test/promise-test.js | 221 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 302 insertions(+) create mode 100644 lib/sinon/promise.js create mode 100644 test/promise-test.js diff --git a/lib/sinon.js b/lib/sinon.js index 045b04b66..8f1dedc8c 100644 --- a/lib/sinon.js +++ b/lib/sinon.js @@ -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, @@ -38,6 +39,9 @@ var apiMethods = { addBehavior: function (name, fn) { behavior.addBehavior(stub, name, fn); }, + + // fake promise + promise: promise, }; var sandbox = new Sandbox(); diff --git a/lib/sinon/promise.js b/lib/sinon/promise.js new file mode 100644 index 000000000..f86d3f365 --- /dev/null +++ b/lib/sinon/promise.js @@ -0,0 +1,77 @@ +"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]); + }; + sinonPromise.reject = function (reason) { + finalize(STATUS_REJECTED, reason, fakeExecutor.firstCall.args[1]); + }; + + return sinonPromise; +} + +module.exports = promise; diff --git a/test/promise-test.js b/test/promise-test.js new file mode 100644 index 000000000..a74bdbc27 --- /dev/null +++ b/test/promise-test.js @@ -0,0 +1,221 @@ +"use strict"; + +var sinon = require("../lib/sinon.js"); +var { assert } = 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(); + + 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); + }); + + it("rejects the promise", async function () { + var error = new Error("promise error"); + var promise = sinon.promise(); + + 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); + }); + + 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); + }); + }); +});