Skip to content

Commit

Permalink
Add sinon.promise() implementation (#2369)
Browse files Browse the repository at this point in the history
* Add sinon.promise() implementation

* Add documentation for sinon.promise
  • Loading branch information
mantoni committed May 25, 2021
1 parent 583f034 commit 7f271ff
Show file tree
Hide file tree
Showing 5 changed files with 366 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/release-source/release.md
Expand Up @@ -15,6 +15,7 @@ This page contains the entire Sinon.JS API documentation along with brief introd
- [Stubs](./stubs)
- [Mocks](./mocks)
- [Spy calls](./spy-call)
- [Promises](./promises)
- [Fake timers](./fake-timers)
- [Fake <code>XHR</code> and server](./fake-xhr-and-server)
- [JSON-P](./json-p)
Expand Down
52 changes: 52 additions & 0 deletions docs/release-source/release/promises.md
@@ -0,0 +1,52 @@
---
layout: page
title: Promises - Sinon.JS
breadcrumb: promises
---

### Introduction

`promise` allows to create fake promises that expose their internal state and can be resolved or rejected on demand.

### Creating a promise

```js
var promise = sinon.promise();
```

#### Creating a promise with a fake executor

```js
var executor = sinon.fake();
var promise = sinon.promise(executor);
```

#### Creating a promise with custom executor

```js
var promise = sinon.promise(function (resolve, reject) {
// ...
});
```

### Promise API

#### `promise.status`

The internal status of the promise. One of `pending`, `resolved`, `rejected`.

#### `promise.resolvedValue`

The promise resolved value.

#### `promise.rejectedValue`

The promise rejected value.

#### `promise.resolve(value)`

Resolves the promise with the given value. Throws if the promise is not `pending`.

#### `promise.reject(value)`

Rejects the promise with the given value. Throws if the promise is not `pending`.
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_RESOLVED = "resolved";
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_RESOLVED;
sinonPromise.resolvedValue = value;
})
.catch(function (reason) {
sinonPromise.status = STATUS_REJECTED;
sinonPromise.rejectedValue = reason;
});

/**
* Resolves 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) {
throw new Error(`Promise already ${sinonPromise.status}`);
}

sinonPromise.status = status;
callback(value);
}

sinonPromise.resolve = function (value) {
finalize(STATUS_RESOLVED, 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 = "resolved";
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, "resolved");
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 resolved",
}
);
});

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

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, "resolved");
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, "resolved");
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 7f271ff

Please sign in to comment.