mirror of
https://github.com/RGBCube/serenity
synced 2025-07-27 16:27:35 +00:00
LibJS: Add initial support for Promises
Almost a year after first working on this, it's finally done: an implementation of Promises for LibJS! :^) The core functionality is working and closely following the spec [1]. I mostly took the pseudo code and transformed it into C++ - if you read and understand it, you will know how the spec implements Promises; and if you read the spec first, the code will look very familiar. Implemented functions are: - Promise() constructor - Promise.prototype.then() - Promise.prototype.catch() - Promise.prototype.finally() - Promise.resolve() - Promise.reject() For the tests I added a new function to test-js's global object, runQueuedPromiseJobs(), which calls vm.run_queued_promise_jobs(). By design, queued jobs normally only run after the script was fully executed, making it improssible to test handlers in individual test() calls by default [2]. Subsequent commits include integrations into LibWeb and js(1) - pretty-printing, running queued promise jobs when necessary. This has an unusual amount of dbgln() statements, all hidden behind the PROMISE_DEBUG flag - I'm leaving them in for now as they've been very useful while debugging this, things can get quite complex with so many asynchronously executed functions. I've not extensively explored use of these APIs for promise-based functionality in LibWeb (fetch(), Notification.requestPermission() etc.), but we'll get there in due time. [1]: https://tc39.es/ecma262/#sec-promise-objects [2]: https://tc39.es/ecma262/#sec-jobs-and-job-queues
This commit is contained in:
parent
563712abce
commit
f418115f1b
32 changed files with 1810 additions and 10 deletions
37
Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.js
Normal file
37
Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
test("length is 1", () => {
|
||||
expect(Promise).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
test("must be called as constructor", () => {
|
||||
expect(() => {
|
||||
Promise();
|
||||
}).toThrowWithMessage(TypeError, "Promise constructor must be called with 'new'");
|
||||
});
|
||||
|
||||
test("executor must be a function", () => {
|
||||
expect(() => {
|
||||
new Promise();
|
||||
}).toThrowWithMessage(TypeError, "Promise executor must be a function");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normal behavior", () => {
|
||||
test("returns a Promise object", () => {
|
||||
const promise = new Promise(() => {});
|
||||
expect(promise).toBeInstanceOf(Promise);
|
||||
expect(typeof promise).toBe("object");
|
||||
});
|
||||
|
||||
test("executor is called with resolve and reject functions", () => {
|
||||
let resolveFunction = null;
|
||||
let rejectFunction = null;
|
||||
new Promise((resolve, reject) => {
|
||||
resolveFunction = resolve;
|
||||
rejectFunction = reject;
|
||||
});
|
||||
expect(typeof resolveFunction).toBe("function");
|
||||
expect(typeof rejectFunction).toBe("function");
|
||||
expect(resolveFunction).not.toBe(rejectFunction);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,55 @@
|
|||
test("length is 1", () => {
|
||||
expect(Promise.prototype.catch).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe("normal behavior", () => {
|
||||
test("returns a Promise object different from the initial Promise", () => {
|
||||
const initialPromise = new Promise(() => {});
|
||||
const catchPromise = initialPromise.catch();
|
||||
expect(catchPromise).toBeInstanceOf(Promise);
|
||||
expect(initialPromise).not.toBe(catchPromise);
|
||||
});
|
||||
|
||||
test("catch() onRejected handler is called when Promise is rejected", () => {
|
||||
let rejectPromise = null;
|
||||
let rejectionReason = null;
|
||||
new Promise((_, reject) => {
|
||||
rejectPromise = reject;
|
||||
}).catch(reason => {
|
||||
rejectionReason = reason;
|
||||
});
|
||||
rejectPromise("Some reason");
|
||||
runQueuedPromiseJobs();
|
||||
expect(rejectionReason).toBe("Some reason");
|
||||
});
|
||||
|
||||
test("returned Promise is rejected with undefined if handler is missing", () => {
|
||||
let rejectPromise = null;
|
||||
let rejectionReason = null;
|
||||
new Promise((_, reject) => {
|
||||
rejectPromise = reject;
|
||||
})
|
||||
.catch()
|
||||
.catch(reason => {
|
||||
rejectionReason = reason;
|
||||
});
|
||||
rejectPromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(rejectionReason).toBeUndefined();
|
||||
});
|
||||
|
||||
test("works with any object", () => {
|
||||
let onFulfilledArg = null;
|
||||
let onRejectedArg = null;
|
||||
const onRejected = () => {};
|
||||
const thenable = {
|
||||
then: (onFulfilled, onRejected) => {
|
||||
onFulfilledArg = onFulfilled;
|
||||
onRejectedArg = onRejected;
|
||||
},
|
||||
};
|
||||
Promise.prototype.catch.call(thenable, onRejected);
|
||||
expect(onFulfilledArg).toBeUndefined();
|
||||
expect(onRejectedArg).toBe(onRejected);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
test("length is 1", () => {
|
||||
expect(Promise.prototype.finally).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe("normal behavior", () => {
|
||||
test("returns a Promise object different from the initial Promise", () => {
|
||||
const initialPromise = new Promise(() => {});
|
||||
const finallyPromise = initialPromise.finally();
|
||||
expect(finallyPromise).toBeInstanceOf(Promise);
|
||||
expect(initialPromise).not.toBe(finallyPromise);
|
||||
});
|
||||
|
||||
test("finally() onFinally handler is called when Promise is resolved", () => {
|
||||
let resolvePromise = null;
|
||||
let finallyWasCalled = false;
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
}).finally(() => {
|
||||
finallyWasCalled = true;
|
||||
});
|
||||
resolvePromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(finallyWasCalled).toBeTrue();
|
||||
});
|
||||
|
||||
test("finally() onFinally handler is called when Promise is rejected", () => {
|
||||
let rejectPromise = null;
|
||||
let finallyWasCalled = false;
|
||||
new Promise((_, reject) => {
|
||||
rejectPromise = reject;
|
||||
}).finally(() => {
|
||||
finallyWasCalled = true;
|
||||
});
|
||||
rejectPromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(finallyWasCalled).toBeTrue();
|
||||
});
|
||||
|
||||
test("works with any object", () => {
|
||||
let thenFinallyArg = null;
|
||||
let catchFinallyArg = null;
|
||||
const onFinally = () => {};
|
||||
const thenable = {
|
||||
then: (thenFinally, catchFinally) => {
|
||||
thenFinallyArg = thenFinally;
|
||||
catchFinallyArg = catchFinally;
|
||||
},
|
||||
};
|
||||
Promise.prototype.finally.call(thenable, onFinally);
|
||||
expect(typeof thenFinallyArg).toBe("function");
|
||||
expect(typeof catchFinallyArg).toBe("function");
|
||||
expect(thenFinallyArg).not.toBe(catchFinallyArg);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,143 @@
|
|||
test("length is 2", () => {
|
||||
expect(Promise.prototype.then).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
test("this value must be a Promise", () => {
|
||||
expect(() => {
|
||||
Promise.prototype.then.call({});
|
||||
}).toThrowWithMessage(TypeError, "Not a Promise");
|
||||
});
|
||||
});
|
||||
|
||||
describe("normal behavior", () => {
|
||||
test("returns a Promise object different from the initial Promise", () => {
|
||||
const initialPromise = new Promise(() => {});
|
||||
const thenPromise = initialPromise.then();
|
||||
expect(thenPromise).toBeInstanceOf(Promise);
|
||||
expect(initialPromise).not.toBe(thenPromise);
|
||||
});
|
||||
|
||||
test("then() onFulfilled handler is called when Promise is fulfilled", () => {
|
||||
let resolvePromise = null;
|
||||
let fulfillmentValue = null;
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
}).then(
|
||||
value => {
|
||||
fulfillmentValue = value;
|
||||
},
|
||||
() => {
|
||||
expect().fail();
|
||||
}
|
||||
);
|
||||
resolvePromise("Some value");
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBe("Some value");
|
||||
});
|
||||
|
||||
test("then() onRejected handler is called when Promise is rejected", () => {
|
||||
let rejectPromise = null;
|
||||
let rejectionReason = null;
|
||||
new Promise((_, reject) => {
|
||||
rejectPromise = reject;
|
||||
}).then(
|
||||
() => {
|
||||
expect().fail();
|
||||
},
|
||||
reason => {
|
||||
rejectionReason = reason;
|
||||
}
|
||||
);
|
||||
rejectPromise("Some reason");
|
||||
runQueuedPromiseJobs();
|
||||
expect(rejectionReason).toBe("Some reason");
|
||||
});
|
||||
|
||||
test("returned Promise is resolved with undefined if handler is missing", () => {
|
||||
let resolvePromise = null;
|
||||
let fulfillmentValue = null;
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
})
|
||||
.then()
|
||||
.then(value => {
|
||||
fulfillmentValue = value;
|
||||
});
|
||||
resolvePromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBeUndefined();
|
||||
});
|
||||
|
||||
test("returned Promise is resolved with return value if handler returns value", () => {
|
||||
let resolvePromise = null;
|
||||
let fulfillmentValue = null;
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
})
|
||||
.then(() => "Some value")
|
||||
.then(value => {
|
||||
fulfillmentValue = value;
|
||||
});
|
||||
resolvePromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBe("Some value");
|
||||
});
|
||||
|
||||
test("returned Promise is rejected with error if handler throws error", () => {
|
||||
let resolvePromise = null;
|
||||
let rejectionReason = null;
|
||||
const error = new Error();
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
})
|
||||
.then(() => {
|
||||
throw error;
|
||||
})
|
||||
.catch(reason => {
|
||||
rejectionReason = reason;
|
||||
});
|
||||
resolvePromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(rejectionReason).toBe(error);
|
||||
});
|
||||
|
||||
test("returned Promise is resolved with Promise result if handler returns fulfilled Promise", () => {
|
||||
let resolvePromise = null;
|
||||
let fulfillmentValue = null;
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
})
|
||||
.then(() => {
|
||||
return Promise.resolve("Some value");
|
||||
})
|
||||
.then(value => {
|
||||
fulfillmentValue = value;
|
||||
});
|
||||
resolvePromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBe("Some value");
|
||||
});
|
||||
|
||||
test("returned Promise is resolved with thenable result if handler returns thenable", () => {
|
||||
let resolvePromise = null;
|
||||
let fulfillmentValue = null;
|
||||
const thenable = {
|
||||
then: onFulfilled => {
|
||||
onFulfilled("Some value");
|
||||
},
|
||||
};
|
||||
new Promise(resolve => {
|
||||
resolvePromise = resolve;
|
||||
})
|
||||
.then(() => {
|
||||
return thenable;
|
||||
})
|
||||
.then(value => {
|
||||
fulfillmentValue = value;
|
||||
});
|
||||
resolvePromise();
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBe("Some value");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
test("length is 1", () => {
|
||||
expect(Promise.reject).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe("normal behavior", () => {
|
||||
test("returns a Promise", () => {
|
||||
const rejectedPromise = Promise.reject();
|
||||
expect(rejectedPromise).toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
test("returned Promise is rejected with given argument", () => {
|
||||
let rejectionReason = null;
|
||||
Promise.reject("Some value").catch(reason => {
|
||||
rejectionReason = reason;
|
||||
});
|
||||
runQueuedPromiseJobs();
|
||||
expect(rejectionReason).toBe("Some value");
|
||||
});
|
||||
|
||||
test("works with subclasses", () => {
|
||||
class CustomPromise extends Promise {}
|
||||
|
||||
const rejectedPromise = CustomPromise.reject("Some value");
|
||||
expect(rejectedPromise).toBeInstanceOf(CustomPromise);
|
||||
|
||||
let rejectionReason = null;
|
||||
rejectedPromise.catch(reason => {
|
||||
rejectionReason = reason;
|
||||
});
|
||||
runQueuedPromiseJobs();
|
||||
expect(rejectionReason).toBe("Some value");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
test("length is 1", () => {
|
||||
expect(Promise.resolve).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe("normal behavior", () => {
|
||||
test("returns a Promise", () => {
|
||||
const resolvedPromise = Promise.resolve();
|
||||
expect(resolvedPromise).toBeInstanceOf(Promise);
|
||||
});
|
||||
|
||||
test("returned Promise is resolved with given argument", () => {
|
||||
let fulfillmentValue = null;
|
||||
Promise.resolve("Some value").then(value => {
|
||||
fulfillmentValue = value;
|
||||
});
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBe("Some value");
|
||||
});
|
||||
|
||||
test("works with subclasses", () => {
|
||||
class CustomPromise extends Promise {}
|
||||
|
||||
const resolvedPromise = CustomPromise.resolve("Some value");
|
||||
expect(resolvedPromise).toBeInstanceOf(CustomPromise);
|
||||
|
||||
let fulfillmentValue = null;
|
||||
resolvedPromise.then(value => {
|
||||
fulfillmentValue = value;
|
||||
});
|
||||
runQueuedPromiseJobs();
|
||||
expect(fulfillmentValue).toBe("Some value");
|
||||
});
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue