Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add sinon.promise() implementation (#2369)
* Add sinon.promise() implementation * Add documentation for sinon.promise
- Loading branch information
Showing
5 changed files
with
366 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
}); | ||
}); |