From e3990699676251b8a50c73312b7c25c3e353782f Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Wed, 3 Jun 2026 16:23:39 +0200 Subject: [PATCH] fix: handle any sequence in AbortSignal Treat `AbortSignal.any()` as sequence and validate entries before creating dependent signal. Also I'm not sure why dom AbortSignal tests were not enabled, they would certainly catch that, but they seem to be passing now. --- builtins/web/abort/abort-signal.cpp | 56 +++++++++++++++---- builtins/web/event/event-target.cpp | 7 +-- builtins/web/event/event-target.h | 2 +- .../dom/abort/AbortSignal.any.js.json | 17 ++++++ .../dom/abort/abort-signal-any.any.js.json | 44 +++++++++++++++ .../expectations/dom/abort/event.any.js.json | 50 +++++++++++++++++ tests/wpt-harness/tests.json | 3 + 7 files changed, 162 insertions(+), 17 deletions(-) create mode 100644 tests/wpt-harness/expectations/dom/abort/AbortSignal.any.js.json create mode 100644 tests/wpt-harness/expectations/dom/abort/abort-signal-any.any.js.json create mode 100644 tests/wpt-harness/expectations/dom/abort/event.any.js.json diff --git a/builtins/web/abort/abort-signal.cpp b/builtins/web/abort/abort-signal.cpp index 2bc6a3a5..b5977870 100644 --- a/builtins/web/abort/abort-signal.cpp +++ b/builtins/web/abort/abort-signal.cpp @@ -5,6 +5,7 @@ #include "../event/event.h" #include "../timers.h" +#include "js/ForOfIterator.h" namespace builtins::web::abort { @@ -102,9 +103,6 @@ bool AbortSignal::timeout(JSContext *cx, unsigned argc, JS::Value *vp) { // https://dom.spec.whatwg.org/#dom-abortsignal-abort bool AbortSignal::abort(JSContext *cx, unsigned argc, JS::Value *vp) { CallArgs args = JS::CallArgsFromVp(argc, vp); - if (!args.requireAtLeast(cx, "abort", 1)) { - return false; - } // The abort() method steps are inlined in the AbortSignal::create_with_reason method. RootedObject self(cx, create_with_reason(cx, args.get(0))); @@ -123,8 +121,35 @@ bool AbortSignal::any(JSContext *cx, unsigned argc, JS::Value *vp) { return false; } - // The any() method steps are inlined in the AbortSignal::create_with_signals method. - RootedObject self(cx, create_with_signals(cx, args)); + RootedValue iterable(cx, args.get(0)); + JS::ForOfIterator it(cx); + if (!it.init(iterable)) { + return false; + } + + JS::RootedValueVector signals(cx); + RootedValue signal(cx); + while (true) { + bool done = false; + if (!it.next(&signal, &done)) { + return false; + } + + if (done) { + break; + } + + if (!is_instance(signal)) { + return api::throw_error(cx, api::Errors::TypeError, "AbortSignal.any", "signals", + "contain only AbortSignal objects"); + } + + if (!signals.append(signal)) { + return false; + } + } + + RootedObject self(cx, create_with_signals(cx, signals)); if (!self) { return false; } @@ -142,6 +167,7 @@ bool AbortSignal::throwIfAborted(JSContext *cx, unsigned argc, JS::Value *vp) { if (is_aborted(self)) { RootedValue reason(cx, JS::GetReservedSlot(self, std::to_underlying(Slots::Reason))); JS_SetPendingException(cx, reason); + return false; } return true; @@ -222,7 +248,10 @@ bool AbortSignal::abort(JSContext *cx, HandleObject self, HandleValue reason) { // 2. Set signal's abort reason to reason if it is given; // otherwise to a new "AbortError" DOMException. - set_reason(cx, self, reason); + if (!set_reason(cx, self, reason)) { + return false; + } + RootedValue abort_reason(cx, AbortSignal::reason(self)); // 3. Let dependentSignalsToAbort be a new list. JS::RootedObjectVector dep_signals_to_abort(cx); @@ -234,7 +263,9 @@ bool AbortSignal::abort(JSContext *cx, HandleObject self, HandleValue reason) { // 1. If dependentSignal is not aborted: if (!is_aborted(signal)) { // 1. Set dependentSignal's abort reason to signal's abort reason. - set_reason(cx, signal, reason); + if (!set_reason(cx, signal, abort_reason)) { + return false; + } // 2. Append dependentSignal to dependentSignalsToAbort. if (!dep_signals_to_abort.append(signal)) { return false; @@ -276,7 +307,7 @@ bool AbortSignal::run_abort_steps(JSContext *cx, HandleObject self) { RootedObject event(cx, Event::create(cx, type_val, JS::NullHandleValue)); RootedValue event_val(cx, JS::ObjectValue(*event)); - return EventTarget::dispatch_event(cx, self, event_val, &res_val); + return EventTarget::dispatch_event(cx, self, event_val, &res_val, true); } // Set signal's abort reason to reason if it is given; otherwise to a new "AbortError" DOMException. @@ -408,7 +439,9 @@ JSObject *AbortSignal::create_with_signals(JSContext *cx, HandleValueArray signa // 2. For each signal of signals: if signal is aborted, then set resultSignal's abort reason to // signal's abort reason and return resultSignal. for (size_t i = 0; i < signals.length(); ++i) { - RootedObject signal(cx, &signals[i].toObject()); + RootedValue signal_val(cx, signals[i]); + MOZ_ASSERT(is_instance(signal_val)); + RootedObject signal(cx, &signal_val.toObject()); if (is_aborted(signal)) { SetReservedSlot(self, std::to_underlying(Slots::Reason), reason(signal)); @@ -422,7 +455,8 @@ JSObject *AbortSignal::create_with_signals(JSContext *cx, HandleValueArray signa // 4. For each signal of signals: for (size_t i = 0; i < signals.length(); ++i) { - RootedObject signal(cx, &signals[i].toObject()); + RootedValue signal_val(cx, signals[i]); + RootedObject signal(cx, &signal_val.toObject()); // 1. If signal's dependent is false: if (!is_dependent(signal)) { @@ -514,5 +548,3 @@ bool install(api::Engine *engine) { JSString *AbortSignal::abort_type_atom = nullptr; } // namespace builtins::web::abort - - diff --git a/builtins/web/event/event-target.cpp b/builtins/web/event/event-target.cpp index b77b1530..55d1ca5a 100644 --- a/builtins/web/event/event-target.cpp +++ b/builtins/web/event/event-target.cpp @@ -361,7 +361,7 @@ bool EventTarget::remove_listener(JSContext *cx, HandleObject self, HandleValue // https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent bool EventTarget::dispatch_event(JSContext *cx, HandleObject self, HandleValue event_val, - MutableHandleValue rval) { + MutableHandleValue rval, bool trusted) { MOZ_ASSERT(is_instance(self)); if (!Event::is_instance(event_val)) { @@ -379,8 +379,8 @@ bool EventTarget::dispatch_event(JSContext *cx, HandleObject self, HandleValue e "InvalidStateError"); } - // 2. Initialize event's isTrusted attribute to false. - Event::set_flag(event, EventFlag::Trusted, false); + // 2. Initialize event's isTrusted attribute. + Event::set_flag(event, EventFlag::Trusted, trusted); // 3. Return the result of dispatching event to this. return dispatch(cx, self, event, nullptr, rval); @@ -651,4 +651,3 @@ bool EventTarget::init_class(JSContext *cx, JS::HandleObject global) { } // namespace builtins::web::event - diff --git a/builtins/web/event/event-target.h b/builtins/web/event/event-target.h index f780a047..aeeac58c 100644 --- a/builtins/web/event/event-target.h +++ b/builtins/web/event/event-target.h @@ -67,7 +67,7 @@ class EventTarget : public BuiltinImpl { static bool remove_listener(JSContext *cx, HandleObject self, HandleValue type, HandleValue callback, HandleValue opts); static bool dispatch_event(JSContext *cx, HandleObject self, HandleValue event, - MutableHandleValue rval); + MutableHandleValue rval, bool trusted = false); static JSObject *create(JSContext *cx); diff --git a/tests/wpt-harness/expectations/dom/abort/AbortSignal.any.js.json b/tests/wpt-harness/expectations/dom/abort/AbortSignal.any.js.json new file mode 100644 index 00000000..eb9125de --- /dev/null +++ b/tests/wpt-harness/expectations/dom/abort/AbortSignal.any.js.json @@ -0,0 +1,17 @@ +{ + "the AbortSignal.abort() static returns an already aborted signal": { + "status": "PASS" + }, + "signal returned by AbortSignal.abort() should not fire abort event": { + "status": "PASS" + }, + "AbortSignal.timeout() returns a non-aborted signal": { + "status": "PASS" + }, + "Signal returned by AbortSignal.timeout() times out": { + "status": "PASS" + }, + "AbortSignal timeouts fire in order": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/dom/abort/abort-signal-any.any.js.json b/tests/wpt-harness/expectations/dom/abort/abort-signal-any.any.js.json new file mode 100644 index 00000000..ce8e6ba7 --- /dev/null +++ b/tests/wpt-harness/expectations/dom/abort/abort-signal-any.any.js.json @@ -0,0 +1,44 @@ +{ + "AbortSignal.any() works with an empty array of signals": { + "status": "PASS" + }, + "AbortSignal.any() follows a single signal (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() follows multiple signals (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() returns an aborted signal if passed an aborted signal (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() can be passed the same signal more than once (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() uses the first instance of a duplicate signal (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() signals are composable (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() works with signals returned by AbortSignal.timeout() (using AbortController)": { + "status": "PASS" + }, + "AbortSignal.any() works with intermediate signals (using AbortController)": { + "status": "PASS" + }, + "Abort events for AbortSignal.any() signals fire in the right order (using AbortController)": { + "status": "PASS" + }, + "Dependent signals for AbortSignal.any() are marked aborted before abort events fire (using AbortController)": { + "status": "PASS" + }, + "Dependent signals for AbortSignal.any() are aborted correctly for reentrant aborts (using AbortController)": { + "status": "PASS" + }, + "Dependent signals for AbortSignal.any() should use the same DOMException instance from the already aborted source signal (using AbortController)": { + "status": "PASS" + }, + "Dependent signals for AbortSignal.any() should use the same DOMException instance from the source signal being aborted later (using AbortController)": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/expectations/dom/abort/event.any.js.json b/tests/wpt-harness/expectations/dom/abort/event.any.js.json new file mode 100644 index 00000000..cd31fbb2 --- /dev/null +++ b/tests/wpt-harness/expectations/dom/abort/event.any.js.json @@ -0,0 +1,50 @@ +{ + "AbortController abort() should fire event synchronously": { + "status": "PASS" + }, + "controller.signal should always return the same object": { + "status": "PASS" + }, + "controller.abort() should do nothing the second time it is called": { + "status": "PASS" + }, + "event handler should not be called if added after controller.abort()": { + "status": "PASS" + }, + "the abort event should have the right properties": { + "status": "PASS" + }, + "AbortController abort(reason) should set signal.reason": { + "status": "PASS" + }, + "aborting AbortController without reason creates an \"AbortError\" DOMException": { + "status": "PASS" + }, + "AbortController abort(undefined) creates an \"AbortError\" DOMException": { + "status": "PASS" + }, + "AbortController abort(null) should set signal.reason": { + "status": "PASS" + }, + "static aborting signal should have right properties": { + "status": "PASS" + }, + "static aborting signal with reason should set signal.reason": { + "status": "PASS" + }, + "throwIfAborted() should throw abort.reason if signal aborted": { + "status": "PASS" + }, + "throwIfAborted() should throw primitive abort.reason if signal aborted": { + "status": "PASS" + }, + "throwIfAborted() should not throw if signal not aborted": { + "status": "PASS" + }, + "AbortSignal.reason returns the same DOMException": { + "status": "PASS" + }, + "AbortController.signal.reason returns the same DOMException": { + "status": "PASS" + } +} \ No newline at end of file diff --git a/tests/wpt-harness/tests.json b/tests/wpt-harness/tests.json index 6f19dc13..df1e73ce 100644 --- a/tests/wpt-harness/tests.json +++ b/tests/wpt-harness/tests.json @@ -25,6 +25,9 @@ "console/console-namespace-object-class-string.any.js", "console/console-tests-historical.any.js", "console/idlharness.any.js", + "dom/abort/AbortSignal.any.js", + "dom/abort/abort-signal-any.any.js", + "dom/abort/event.any.js", "dom/events/AddEventListenerOptions-once.any.js", "dom/events/AddEventListenerOptions-passive.any.js", "dom/events/Event-constructors.any.js",