diff --git a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h index 49a9f5ea43..14985da501 100644 --- a/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h +++ b/Userland/Libraries/LibJS/Runtime/CommonPropertyNames.h @@ -314,6 +314,7 @@ namespace JS { P(race) \ P(random) \ P(raw) \ + P(reason) \ P(reduce) \ P(reduceRight) \ P(reject) \ @@ -370,6 +371,7 @@ namespace JS { P(splice) \ P(sqrt) \ P(startsWith) \ + P(status) \ P(sticky) \ P(store) \ P(strike) \ diff --git a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp index 8dc8b6c9b5..76583ba609 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp +++ b/Userland/Libraries/LibJS/Runtime/PromiseConstructor.cpp @@ -151,6 +151,33 @@ static Value perform_promise_all(GlobalObject& global_object, Object& iterator_r }); } +// 27.2.4.2.1 PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability, promiseResolve ), https://tc39.es/ecma262/#sec-performpromiseallsettled +static Value perform_promise_all_settled(GlobalObject& global_object, Object& iterator_record, Value constructor, PromiseCapability result_capability, Value promise_resolve) +{ + auto& vm = global_object.vm(); + + return perform_promise_common( + global_object, iterator_record, constructor, result_capability, promise_resolve, + [&](PromiseValueList& values) -> Value { + auto values_array = Array::create_from(global_object, values.values); + + (void)vm.call(*result_capability.resolve, js_undefined(), values_array); + if (vm.exception()) + return {}; + + return result_capability.promise; + }, + [&](PromiseValueList& values, RemainingElements& remaining_elements_count, Value next_promise, size_t index) { + auto* on_fulfilled = PromiseAllSettledResolveElementFunction::create(global_object, index, values, result_capability, remaining_elements_count); + on_fulfilled->define_direct_property(vm.names.name, js_string(vm, String::empty()), Attribute::Configurable); + + auto* on_rejected = PromiseAllSettledRejectElementFunction::create(global_object, index, values, result_capability, remaining_elements_count); + on_rejected->define_direct_property(vm.names.name, js_string(vm, String::empty()), Attribute::Configurable); + + (void)next_promise.invoke(global_object, vm.names.then, on_fulfilled, on_rejected); + }); +} + // 27.2.4.3.1 PerformPromiseAny ( iteratorRecord, constructor, resultCapability, promiseResolve ), https://tc39.es/ecma262/#sec-performpromiseany static Value perform_promise_any(GlobalObject& global_object, Object& iterator_record, Value constructor, PromiseCapability result_capability, Value promise_resolve) { @@ -190,9 +217,9 @@ void PromiseConstructor::initialize(GlobalObject& global_object) u8 attr = Attribute::Writable | Attribute::Configurable; define_native_function(vm.names.all, all, 1, attr); + define_native_function(vm.names.allSettled, all_settled, 1, attr); define_native_function(vm.names.any, any, 1, attr); // TODO: Implement these functions below and uncomment this. - // define_native_function(vm.names.allSettled, all_settled, 1, attr); // define_native_function(vm.names.race, race, 1, attr); define_native_function(vm.names.reject, reject, 1, attr); define_native_function(vm.names.resolve, resolve, 1, attr); @@ -271,7 +298,32 @@ JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::all) // 27.2.4.2 Promise.allSettled ( iterable ), https://tc39.es/ecma262/#sec-promise.allsettled JS_DEFINE_NATIVE_FUNCTION(PromiseConstructor::all_settled) { - TODO(); + auto* constructor = vm.this_value(global_object).to_object(global_object); + if (!constructor) + return {}; + + auto promise_capability = new_promise_capability(global_object, constructor); + if (vm.exception()) + return {}; + + auto promise_resolve = get_promise_resolve(global_object, constructor); + if (auto abrupt = if_abrupt_reject_promise(global_object, promise_resolve, promise_capability); abrupt.has_value()) + return abrupt.value(); + + auto iterator_record = get_iterator(global_object, vm.argument(0)); + if (auto abrupt = if_abrupt_reject_promise(global_object, iterator_record, promise_capability); abrupt.has_value()) + return abrupt.value(); + + auto result = perform_promise_all_settled(global_object, *iterator_record, constructor, promise_capability, promise_resolve); + if (vm.exception()) { + if (!iterator_record_is_complete(global_object, *iterator_record)) + iterator_close(*iterator_record); + + auto abrupt = if_abrupt_reject_promise(global_object, result, promise_capability); + return abrupt.value(); + } + + return result; } // 27.2.4.3 Promise.any ( iterable ), https://tc39.es/ecma262/#sec-promise.any diff --git a/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.cpp b/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.cpp index 396c9c16f7..94f9b4d348 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.cpp +++ b/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.cpp @@ -73,6 +73,64 @@ Value PromiseAllResolveElementFunction::resolve_element() return js_undefined(); } +PromiseAllSettledResolveElementFunction* PromiseAllSettledResolveElementFunction::create(GlobalObject& global_object, size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements) +{ + return global_object.heap().allocate(global_object, index, values, capability, remaining_elements, *global_object.function_prototype()); +} + +PromiseAllSettledResolveElementFunction::PromiseAllSettledResolveElementFunction(size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements, Object& prototype) + : PromiseResolvingElementFunction(index, values, move(capability), remaining_elements, prototype) +{ +} + +Value PromiseAllSettledResolveElementFunction::resolve_element() +{ + auto& vm = this->vm(); + auto& global_object = this->global_object(); + + auto* object = Object::create(global_object, global_object.object_prototype()); + object->create_data_property_or_throw(vm.names.status, js_string(vm, "fulfilled"sv)); + object->create_data_property_or_throw(vm.names.value, vm.argument(0)); + + m_values.values[m_index] = object; + + if (--m_remaining_elements.value == 0) { + auto values_array = Array::create_from(global_object, m_values.values); + return vm.call(*m_capability.resolve, js_undefined(), values_array); + } + + return js_undefined(); +} + +PromiseAllSettledRejectElementFunction* PromiseAllSettledRejectElementFunction::create(GlobalObject& global_object, size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements) +{ + return global_object.heap().allocate(global_object, index, values, capability, remaining_elements, *global_object.function_prototype()); +} + +PromiseAllSettledRejectElementFunction::PromiseAllSettledRejectElementFunction(size_t index, PromiseValueList& values, PromiseCapability capability, RemainingElements& remaining_elements, Object& prototype) + : PromiseResolvingElementFunction(index, values, move(capability), remaining_elements, prototype) +{ +} + +Value PromiseAllSettledRejectElementFunction::resolve_element() +{ + auto& vm = this->vm(); + auto& global_object = this->global_object(); + + auto* object = Object::create(global_object, global_object.object_prototype()); + object->create_data_property_or_throw(vm.names.status, js_string(vm, "rejected"sv)); + object->create_data_property_or_throw(vm.names.reason, vm.argument(0)); + + m_values.values[m_index] = object; + + if (--m_remaining_elements.value == 0) { + auto values_array = Array::create_from(global_object, m_values.values); + return vm.call(*m_capability.resolve, js_undefined(), values_array); + } + + return js_undefined(); +} + PromiseAnyRejectElementFunction* PromiseAnyRejectElementFunction::create(GlobalObject& global_object, size_t index, PromiseValueList& errors, PromiseCapability capability, RemainingElements& remaining_elements) { return global_object.heap().allocate(global_object, index, errors, capability, remaining_elements, *global_object.function_prototype()); diff --git a/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.h b/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.h index b8d589e0e3..cd0d339f74 100644 --- a/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.h +++ b/Userland/Libraries/LibJS/Runtime/PromiseResolvingElementFunctions.h @@ -74,6 +74,34 @@ private: virtual Value resolve_element() override; }; +// 27.2.4.2.2 Promise.allSettled Resolve Element Functions, https://tc39.es/ecma262/#sec-promise.allsettled-resolve-element-functions +class PromiseAllSettledResolveElementFunction final : public PromiseResolvingElementFunction { + JS_OBJECT(PromiseResolvingFunction, NativeFunction); + +public: + static PromiseAllSettledResolveElementFunction* create(GlobalObject&, size_t, PromiseValueList&, PromiseCapability, RemainingElements&); + + explicit PromiseAllSettledResolveElementFunction(size_t, PromiseValueList&, PromiseCapability, RemainingElements&, Object& prototype); + virtual ~PromiseAllSettledResolveElementFunction() override = default; + +private: + virtual Value resolve_element() override; +}; + +// 27.2.4.2.3 Promise.allSettled Reject Element Functions, https://tc39.es/ecma262/#sec-promise.allsettled-reject-element-functions +class PromiseAllSettledRejectElementFunction final : public PromiseResolvingElementFunction { + JS_OBJECT(PromiseResolvingFunction, NativeFunction); + +public: + static PromiseAllSettledRejectElementFunction* create(GlobalObject&, size_t, PromiseValueList&, PromiseCapability, RemainingElements&); + + explicit PromiseAllSettledRejectElementFunction(size_t, PromiseValueList&, PromiseCapability, RemainingElements&, Object& prototype); + virtual ~PromiseAllSettledRejectElementFunction() override = default; + +private: + virtual Value resolve_element() override; +}; + // 27.2.4.3.2 Promise.any Reject Element Functions, https://tc39.es/ecma262/#sec-promise.any-reject-element-functions class PromiseAnyRejectElementFunction final : public PromiseResolvingElementFunction { JS_OBJECT(PromiseResolvingFunction, NativeFunction); diff --git a/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.allSettled.js b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.allSettled.js new file mode 100644 index 0000000000..56a0ad1422 --- /dev/null +++ b/Userland/Libraries/LibJS/Tests/builtins/Promise/Promise.allSettled.js @@ -0,0 +1,124 @@ +test("length is 1", () => { + expect(Promise.allSettled).toHaveLength(1); +}); + +describe("normal behavior", () => { + test("returns a Promise", () => { + const promise = Promise.allSettled(); + expect(promise).toBeInstanceOf(Promise); + }); + + test("resolve", () => { + const promise1 = Promise.resolve(3); + const promise2 = 42; + const promise3 = new Promise((resolve, reject) => { + resolve("foo"); + }); + + let resolvedValues = null; + let wasRejected = false; + + Promise.allSettled([promise1, promise2, promise3]).then( + values => { + resolvedValues = values; + }, + () => { + wasRejected = true; + } + ); + + runQueuedPromiseJobs(); + expect(resolvedValues).toEqual([ + { status: "fulfilled", value: 3 }, + { status: "fulfilled", value: 42 }, + { status: "fulfilled", value: "foo" }, + ]); + expect(wasRejected).toBeFalse(); + }); + + test("reject", () => { + const promise1 = Promise.resolve(3); + const promise2 = 42; + const promise3 = new Promise((resolve, reject) => { + reject("foo"); + }); + + let resolvedValues = null; + let wasRejected = false; + + Promise.allSettled([promise1, promise2, promise3]).then( + values => { + resolvedValues = values; + }, + () => { + wasRejected = true; + } + ); + + runQueuedPromiseJobs(); + expect(resolvedValues).toEqual([ + { status: "fulfilled", value: 3 }, + { status: "fulfilled", value: 42 }, + { status: "rejected", reason: "foo" }, + ]); + expect(wasRejected).toBeFalse(); + }); +}); + +describe("exceptional behavior", () => { + test("cannot invoke capabilities executor twice", () => { + function fn() {} + + expect(() => { + function promise(executor) { + executor(fn, fn); + executor(fn, fn); + } + + Promise.allSettled.call(promise, []); + }).toThrow(TypeError); + + expect(() => { + function promise(executor) { + executor(fn, undefined); + executor(fn, fn); + } + + Promise.allSettled.call(promise, []); + }).toThrow(TypeError); + + expect(() => { + function promise(executor) { + executor(undefined, fn); + executor(fn, fn); + } + + Promise.allSettled.call(promise, []); + }).toThrow(TypeError); + }); + + test("promise without resolve method", () => { + expect(() => { + function promise(executor) {} + Promise.allSettled.call(promise, []); + }).toThrow(TypeError); + }); + + test("no parameters", () => { + let rejectionReason = null; + Promise.allSettled().catch(reason => { + rejectionReason = reason; + }); + runQueuedPromiseJobs(); + expect(rejectionReason).toBeInstanceOf(TypeError); + }); + + test("non-iterable", () => { + let rejectionReason = null; + Promise.allSettled(1).catch(reason => { + rejectionReason = reason; + }); + runQueuedPromiseJobs(); + expect(rejectionReason).toBeInstanceOf(TypeError); + }); +});