From b59400c836495375fcaab03f73e335ce455ddfb8 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Fri, 10 Jul 2026 18:50:46 +0200 Subject: [PATCH] webapi: invoke event target separately for capture and bubble passes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the failing test in WPT /dom/events/Event-dispatch-handlers- changed.html ("Dispatch additional events inside an event listener"). At the target, listeners were run in a single pass mixing capture and bubble listeners, with one snapshot of the listener list. Per the DOM dispatch algorithm, the target participates in both the capturing and the bubbling iterations of the event path, and each invoke clones the listener list separately: a bubble listener added by one of the target's capture listeners must run during the bubbling pass. The single pass used one snapshot, so such a listener never ran. The at-target step now dispatches twice — capture listeners first, then non-capture listeners — re-fetching the listener list (and taking a new sentinel snapshot) for the second pass, and honoring stopPropagation / stopImmediatePropagation between the two. Coverage: /dom/events/Event-dispatch-handlers-changed.html 0/1 -> 1/1, /dom/events/EventTarget-dispatchEvent.html 4/25 -> 5/25. No regressions across /dom/events. Co-Authored-By: Claude Fable 5 --- src/browser/EventManager.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index bd7994229..4802d9fe7 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -300,8 +300,18 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts } } + // Per spec, the target is invoked once during the capturing iteration + // and once during the bubbling iteration, each with its own snapshot + // of the listener list: a bubble listener added while running the + // target's capture listeners must run. if (self.base.getListeners(target_et, event._type_string)) |list| { - try self.dispatchPhase(list, target_et, event, &was_handled, &ls.local, comptime .init(null, opts)); + try self.dispatchPhase(list, target_et, event, &was_handled, &ls.local, comptime .init(true, opts)); + if (event._stop_propagation) { + return; + } + } + if (self.base.getListeners(target_et, event._type_string)) |list| { + try self.dispatchPhase(list, target_et, event, &was_handled, &ls.local, comptime .init(false, opts)); if (event._stop_propagation) { return; }