From ad9e1d8dc6767bb224d26e5ebb2a90f7fdcd2dbb Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Fri, 10 Jul 2026 20:24:48 +0200 Subject: [PATCH] webapi: uninitialized-event dispatch throws; more createEvent aliases Fixes the 20 failing tests in WPT /dom/events/EventTarget- dispatchEvent.html (5/25 -> 25/25), all of the form "If the event's initialized flag is not set, an InvalidStateError must be thrown". Per the DOM spec, an event created via document.createEvent has its initialized flag unset until one of the legacy init*Event methods runs, and dispatchEvent must throw an InvalidStateError for it. Lightpanda dispatched such events happily. Event now carries an _initialized flag: true by default (constructor-created and internal events), cleared by document.createEvent, and set by initEvent, initCustomEvent, initUIEvent, initMouseEvent, initKeyboardEvent, initCompositionEvent and initTextEvent. EventTarget.dispatchEvent rejects uninitialized events. document.createEvent also gains the spec-required aliases it was missing: DragEvent, HashChangeEvent and SVGEvents, plus BeforeUnloadEvent, DeviceMotionEvent, DeviceOrientationEvent and StorageEvent mapped to a plain Event (logged as not_implemented) until those interfaces exist. Coverage: /dom/events/EventTarget-dispatchEvent.html 5/25 -> 25/25. No regressions across /dom/events. Co-Authored-By: Claude Fable 5 --- src/browser/webapi/Document.zig | 109 +++++++++++------- src/browser/webapi/Event.zig | 5 + src/browser/webapi/EventTarget.zig | 5 + src/browser/webapi/event/CompositionEvent.zig | 1 + src/browser/webapi/event/CustomEvent.zig | 1 + src/browser/webapi/event/KeyboardEvent.zig | 1 + src/browser/webapi/event/MouseEvent.zig | 1 + src/browser/webapi/event/TextEvent.zig | 1 + src/browser/webapi/event/UIEvent.zig | 1 + 9 files changed, 85 insertions(+), 40 deletions(-) diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 281328f25..e3b711b7f 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -484,56 +484,85 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !* } const normalized = std.ascii.lowerString(&frame.buf, event_type); - if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents")) { - return Event.init("", null, frame._page); - } + const event: *Event = blk: { + if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents") or std.mem.eql(u8, normalized, "svgevents")) { + break :blk try Event.init("", null, frame._page); + } - if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) { - const CustomEvent = @import("event/CustomEvent.zig"); - return (try CustomEvent.init("", null, frame._page)).asEvent(); - } + if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) { + const CustomEvent = @import("event/CustomEvent.zig"); + break :blk (try CustomEvent.init("", null, frame._page)).asEvent(); + } - if (std.mem.eql(u8, normalized, "keyboardevent")) { - const KeyboardEvent = @import("event/KeyboardEvent.zig"); - return (try KeyboardEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "keyboardevent")) { + const KeyboardEvent = @import("event/KeyboardEvent.zig"); + break :blk (try KeyboardEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "inputevent")) { - const InputEvent = @import("event/InputEvent.zig"); - return (try InputEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "inputevent")) { + const InputEvent = @import("event/InputEvent.zig"); + break :blk (try InputEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) { - const MouseEvent = @import("event/MouseEvent.zig"); - return (try MouseEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) { + const MouseEvent = @import("event/MouseEvent.zig"); + break :blk (try MouseEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "messageevent")) { - const MessageEvent = @import("event/MessageEvent.zig"); - return (try MessageEvent.init("", null, frame._page)).asEvent(); - } + if (std.mem.eql(u8, normalized, "dragevent")) { + const DragEvent = @import("event/DragEvent.zig"); + break :blk (try DragEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) { - const UIEvent = @import("event/UIEvent.zig"); - return (try UIEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "messageevent")) { + const MessageEvent = @import("event/MessageEvent.zig"); + break :blk (try MessageEvent.init("", null, frame._page)).asEvent(); + } - if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) { - const FocusEvent = @import("event/FocusEvent.zig"); - return (try FocusEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "hashchangeevent")) { + const HashChangeEvent = @import("event/HashChangeEvent.zig"); + break :blk (try HashChangeEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) { - const TextEvent = @import("event/TextEvent.zig"); - return (try TextEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) { + const UIEvent = @import("event/UIEvent.zig"); + break :blk (try UIEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "compositionevent")) { - const CompositionEvent = @import("event/CompositionEvent.zig"); - return (try CompositionEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) { + const FocusEvent = @import("event/FocusEvent.zig"); + break :blk (try FocusEvent.init("", null, frame)).asEvent(); + } - return error.NotSupported; + if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) { + const TextEvent = @import("event/TextEvent.zig"); + break :blk (try TextEvent.init("", null, frame)).asEvent(); + } + + if (std.mem.eql(u8, normalized, "compositionevent")) { + const CompositionEvent = @import("event/CompositionEvent.zig"); + break :blk (try CompositionEvent.init("", null, frame)).asEvent(); + } + + // Aliases the spec requires createEvent to support but whose + // interfaces aren't implemented yet: return a plain Event so the + // caller can at least initialize and dispatch it. + if (std.mem.eql(u8, normalized, "beforeunloadevent") or + std.mem.eql(u8, normalized, "devicemotionevent") or + std.mem.eql(u8, normalized, "deviceorientationevent") or + std.mem.eql(u8, normalized, "storageevent")) + { + log.info(.not_implemented, "createEvent interface", .{ .type = event_type }); + break :blk try Event.init("", null, frame._page); + } + + return error.NotSupported; + }; + + // createEvent returns an uninitialized event: dispatching it before one + // of the init*Event calls throws an InvalidStateError. + event._initialized = false; + return event; } pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, frame: *Frame) !*DOMTreeWalker { diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 53179e125..c592e2793 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -50,6 +50,10 @@ _needs_retargeting: bool = false, _is_trusted: bool = false, _in_passive_listener: bool = false, _listeners_did_throw: bool = false, // IndexedDB needs to abort on callback throw +// Per spec, events created via document.createEvent are not initialized +// until one of the init*Event methods runs; dispatching one throws an +// InvalidStateError. Events created any other way start initialized. +_initialized: bool = true, // There's a period of time between creating an event and handing it off to v8 // where things can fail. If it does fail, we need to deinit the event. The timing @@ -137,6 +141,7 @@ pub fn initEvent( return; } + self._initialized = true; self._type_string = try String.init(self._arena, event_string, .{}); self._bubbles = bubbles orelse false; self._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 7fe689396..e8422f99c 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -69,6 +69,11 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo if (event._event_phase != .none) { return error.InvalidStateError; } + // An event created by document.createEvent stays uninitialized until an + // init*Event call; dispatching it is an error. + if (!event._initialized) { + return error.InvalidStateError; + } event._is_trusted = false; switch (exec.js.global) { diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index fb3b4fd4d..93493d418 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -79,6 +79,7 @@ pub fn initCompositionEvent( } const arena = event._arena; + event._initialized = true; event._type_string = try String.init(arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index e1412a5d7..61f96e941 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -73,6 +73,7 @@ pub fn initCustomEvent( // This function can only be called after the constructor has called. // So we assume proto is initialized already by constructor. + self._proto._initialized = true; self._proto._type_string = try String.init(self._proto._arena, event_string, .{}); self._proto._bubbles = bubbles orelse false; self._proto._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig index 73ff74706..5c66cde3d 100644 --- a/src/browser/webapi/event/KeyboardEvent.zig +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -428,6 +428,7 @@ pub fn initKeyboardEvent( } const arena = event._arena; + event._initialized = true; event._type_string = try String.init(arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index eac879547..819d6fe9c 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -239,6 +239,7 @@ pub fn initMouseEvent( return; } + event._initialized = true; event._type_string = try String.init(event._arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/TextEvent.zig b/src/browser/webapi/event/TextEvent.zig index 8a5e238f8..f03cdd0dc 100644 --- a/src/browser/webapi/event/TextEvent.zig +++ b/src/browser/webapi/event/TextEvent.zig @@ -83,6 +83,7 @@ pub fn initTextEvent( } const arena = event._arena; + event._initialized = true; event._type_string = try String.init(arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index c5145e7f7..ded62c17a 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -146,6 +146,7 @@ pub fn initUIEvent( return; } + event._initialized = true; event._type_string = try String.init(event._arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false;