From ee50f1238c20f207d14b1d1891df30d7c9aeee1b Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 17 Jul 2025 21:38:43 +0800 Subject: [PATCH] Trigger the DOMContentLoaded on the Window This is hacky, but it's inspired by how NetSurf does it. While the Window isn't the parent of the Document, many events should bubble from the Document to the Window. libdom simply doesn't handle this (it has no concept of a Window, and the Document has no parent). We potentially need to do this for multiple event types (NetSurf only does it for the 'load' event as far as I can tell). It would be nice to find a generic way to do this...maybe intercept any addEventListener on the body and registering special events on the Window? For now, `DOMContentLoaded` is the blocking (for finance.yahoo.com) and we can see if this is really an issue for other event types. --- src/browser/html/document.zig | 8 +++++--- src/browser/html/window.zig | 34 ++++++++++++++++++++++++++++++++++ src/browser/netsurf.zig | 8 ++++++++ src/testing.zig | 6 ++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/browser/html/document.zig b/src/browser/html/document.zig index 694e091f..92b19535 100644 --- a/src/browser/html/document.zig +++ b/src/browser/html/document.zig @@ -278,15 +278,17 @@ pub const HTMLDocument = struct { const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self))); state.ready_state = .interactive; - const evt = try parser.eventCreate(); - defer parser.eventDestroy(evt); - log.debug(.script_event, "dispatch event", .{ .type = "DOMContentLoaded", .source = "document", }); + + const evt = try parser.eventCreate(); + defer parser.eventDestroy(evt); try parser.eventInit(evt, "DOMContentLoaded", .{ .bubbles = true, .cancelable = true }); _ = try parser.eventTargetDispatchEvent(parser.toEventTarget(parser.DocumentHTML, self), evt); + + try page.window.dispatchForDocumentTarget(evt); } pub fn documentIsComplete(self: *parser.DocumentHTML, page: *Page) !void { diff --git a/src/browser/html/window.zig b/src/browser/html/window.zig index 0a8494b6..ef688b52 100644 --- a/src/browser/html/window.zig +++ b/src/browser/html/window.zig @@ -334,6 +334,23 @@ pub const Window = struct { ); } } + + // libdom's document doesn't have a parent, which is correct, but + // breaks the event bubbling that happens for many events from + // document -> window. + // We need to force dispatch this event on the window, with the + // document target. + // In theory, we should do this for a lot of events and might need + // to come up with a good way to solve this more generically. But + // this specific event, and maybe a few others in the near future, + // are blockers. + // Worth noting that NetSurf itself appears to do something similar: + // https://github.com/netsurf-browser/netsurf/blob/a32e1a03e1c91ee9f0aa211937dbae7a96831149/content/handlers/html/html.c#L380 + pub fn dispatchForDocumentTarget(self: *Window, evt: *parser.Event) !void { + // we assume that this evt has already been dispatched on the document + // and thus the target has already been set to the document. + return self.base.redispatchEvent(evt); + } }; const TimerCallback = struct { @@ -491,4 +508,21 @@ test "Browser.HTML.Window" { .{ "var qm = false; window.queueMicrotask(() => {qm = true });", null }, .{ "qm", "true" }, }, .{}); + + { + try runner.testCases(&.{ + .{ + \\ let dcl = false; + \\ window.addEventListener('DOMContentLoaded', (e) => { + \\ dcl = e.target == document; + \\ }); + , + null, + }, + }, .{}); + try runner.dispatchDOMContentLoaded(); + try runner.testCases(&.{ + .{ "dcl", "true" }, + }, .{}); + } } diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index f0af5280..506e0358 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -846,6 +846,14 @@ pub const EventTargetTBase = extern struct { internal_type_.* = @intFromEnum(self.internal_target_type); return c.DOM_NO_ERR; } + + // Called to simulate bubbling from a libdom node (e.g. the Document) to a + // Zig instance (e.g. the Window). + pub fn redispatchEvent(self: *EventTargetTBase, evt: *Event) !void { + var res: bool = undefined; + const err = c._dom_event_target_dispatch(@ptrCast(self), &self.eti, evt, c.DOM_BUBBLING_PHASE, &res); + try DOMErr(err); + } }; // MouseEvent diff --git a/src/testing.zig b/src/testing.zig index f05f9023..65d24f8d 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -474,6 +474,12 @@ pub const JsRunner = struct { return err; }; } + + pub fn dispatchDOMContentLoaded(self: *JsRunner) !void { + const HTMLDocument = @import("browser/html/document.zig").HTMLDocument; + const html_doc = self.page.window.document; + try HTMLDocument.documentIsLoaded(html_doc, self.page); + } }; const RunnerOpts = struct {