1
Fork 0
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:
Linus Groh 2021-04-01 22:13:29 +02:00 committed by Andreas Kling
parent 563712abce
commit f418115f1b
32 changed files with 1810 additions and 10 deletions

View 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);
});
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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");
});
});

View file

@ -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");
});
});

View file

@ -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");
});
});