Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sinon.promise() implementation #2369

Merged
merged 2 commits into from May 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
});
});
});