diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 435685c7..9075c2a5 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -723,23 +723,13 @@ pub fn documentIsComplete(self: *Page) void { fn _documentIsComplete(self: *Page) !void { self.document._ready_state = .complete; + // Run load events before window.load. + try self.dispatchLoad(); + var ls: JS.Local.Scope = undefined; self.js.localScope(&ls); defer ls.deinit(); - { - // Dispatch `_to_load` events before window.load. - const has_dom_load_listener = self._event_manager.has_dom_load_listener; - for (self._to_load.items) |html_element| { - if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) { - const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); - try self._event_manager.dispatch(html_element.asEventTarget(), event); - } - } - } - // `_to_load` can be cleaned here. - self._to_load.clearAndFree(self.arena); - // Dispatch window.load event. const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); // This event is weird, it's dispatched directly on the window, but @@ -1217,6 +1207,18 @@ pub fn checkIntersections(self: *Page) !void { } } +pub fn dispatchLoad(self: *Page) !void { + const has_dom_load_listener = self._event_manager.has_dom_load_listener; + for (self._to_load.items) |html_element| { + if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) { + const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); + try self._event_manager.dispatch(html_element.asEventTarget(), event); + } + } + // We drained everything. + self._to_load.clearRetainingCapacity(); +} + pub fn scheduleMutationDelivery(self: *Page) !void { if (self._mutation_delivery_scheduled) { return; @@ -2842,6 +2844,16 @@ fn nodeIsReady(self: *Page, comptime from_parser: bool, node: *Node) !void { log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type, .url = self.url }); return err; }; + } else if (node.is(Element.Html.Link)) |link| { + link.linkAddedCallback(self) catch |err| { + log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "link", .type = self._type }); + return error.LinkLoadError; + }; + } else if (node.is(Element.Html.Style)) |style| { + style.styleAddedCallback(self) catch |err| { + log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "style", .type = self._type }); + return error.StyleLoadError; + }; } } diff --git a/src/browser/Session.zig b/src/browser/Session.zig index 18468933..540ba520 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -241,6 +241,9 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult { // it AFTER. const ms_to_next_task = try browser.runMacrotasks(); + // Each call to this runs scheduled load events. + try page.dispatchLoad(); + const http_active = http_client.active; const total_network_activity = http_active + http_client.intercepted; if (page._notified_network_almost_idle.check(total_network_activity <= 2)) { diff --git a/src/browser/tests/element/html/image.html b/src/browser/tests/element/html/image.html index e7868229..92cd947d 100644 --- a/src/browser/tests/element/html/image.html +++ b/src/browser/tests/element/html/image.html @@ -114,48 +114,15 @@ } - - - - - + + + + diff --git a/src/browser/tests/element/html/link.html b/src/browser/tests/element/html/link.html index 2031e5fe..bed5e6ab 100644 --- a/src/browser/tests/element/html/link.html +++ b/src/browser/tests/element/html/link.html @@ -19,3 +19,68 @@ l2.crossOrigin = ''; testing.expectEqual('anonymous', l2.crossOrigin); + + + + + + + + diff --git a/src/browser/tests/element/html/style.html b/src/browser/tests/element/html/style.html index 713d8e2e..8abbb229 100644 --- a/src/browser/tests/element/html/style.html +++ b/src/browser/tests/element/html/style.html @@ -106,3 +106,28 @@ testing.expectEqual(true, style.disabled); } + + diff --git a/src/browser/webapi/Node.zig b/src/browser/webapi/Node.zig index 9a413170..7849622a 100644 --- a/src/browser/webapi/Node.zig +++ b/src/browser/webapi/Node.zig @@ -722,6 +722,8 @@ const CloneError = error{ CloneError, IFrameLoadError, TooManyContexts, + LinkLoadError, + StyleLoadError, }; pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node { const deep = deep_ orelse false; diff --git a/src/browser/webapi/element/html/Image.zig b/src/browser/webapi/element/html/Image.zig index 1e33b3c0..c8fba91e 100644 --- a/src/browser/webapi/element/html/Image.zig +++ b/src/browser/webapi/element/html/Image.zig @@ -50,7 +50,10 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 { } pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { - try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page); + const element = self.asElement(); + try element.setAttributeSafe(comptime .wrap("src"), .wrap(value), page); + // No need to check if `Image` is connected to DOM; this is a special case. + return self.imageAddedCallback(page); } pub fn getAlt(self: *const Image) []const u8 { @@ -120,6 +123,21 @@ pub fn getComplete(_: *const Image) bool { return true; } +/// Used in `Page.nodeIsReady`. +pub fn imageAddedCallback(self: *Image, page: *Page) !void { + // if we're planning on navigating to another page, don't trigger load event. + if (page.isGoingAway()) { + return; + } + + const element = self.asElement(); + // Exit if src not set. + const src = element.getAttributeSafe(comptime .wrap("src")) orelse return; + if (src.len == 0) return; + + try page._to_load.append(page.arena, self._proto); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Image); @@ -145,13 +163,7 @@ pub const JsApi = struct { pub const Build = struct { pub fn created(node: *Node, page: *Page) !void { const self = node.as(Image); - const image = self.asElement(); - // Exit if src not set. - // TODO: We might want to check if src point to valid image. - _ = image.getAttributeSafe(comptime .wrap("src")) orelse return; - - // Push to `_to_load` to dispatch load event just before window load event. - return page._to_load.append(page.arena, self._proto); + return self.imageAddedCallback(page); } }; diff --git a/src/browser/webapi/element/html/Link.zig b/src/browser/webapi/element/html/Link.zig index 7f6b48fc..fe56bd89 100644 --- a/src/browser/webapi/element/html/Link.zig +++ b/src/browser/webapi/element/html/Link.zig @@ -50,7 +50,12 @@ pub fn getHref(self: *Link, page: *Page) ![]const u8 { } pub fn setHref(self: *Link, value: []const u8, page: *Page) !void { - try self.asElement().setAttributeSafe(comptime .wrap("href"), .wrap(value), page); + const element = self.asElement(); + try element.setAttributeSafe(comptime .wrap("href"), .wrap(value), page); + + if (element.asNode().isConnected()) { + try self.linkAddedCallback(page); + } } pub fn getRel(self: *Link) []const u8 { @@ -81,6 +86,24 @@ pub fn setCrossOrigin(self: *Link, value: []const u8, page: *Page) !void { return self.asElement().setAttributeSafe(comptime .wrap("crossOrigin"), .wrap(normalized), page); } +pub fn linkAddedCallback(self: *Link, page: *Page) !void { + // if we're planning on navigating to another page, don't trigger load event. + if (page.isGoingAway()) { + return; + } + + const element = self.asElement(); + // Exit if rel not set. + const rel = element.getAttributeSafe(comptime .wrap("rel")) orelse return; + // Exit if rel is not stylesheet. + if (!std.mem.eql(u8, rel, "stylesheet")) return; + // Exit if href not set. + const href = element.getAttributeSafe(comptime .wrap("href")) orelse return; + if (href.len == 0) return; + + try page._to_load.append(page.arena, self._proto); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Link); diff --git a/src/browser/webapi/element/html/Style.zig b/src/browser/webapi/element/html/Style.zig index 3dbb288b..131b7634 100644 --- a/src/browser/webapi/element/html/Style.zig +++ b/src/browser/webapi/element/html/Style.zig @@ -97,6 +97,15 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet { return sheet; } +pub fn styleAddedCallback(self: *Style, page: *Page) !void { + // if we're planning on navigating to another page, don't trigger load event. + if (page.isGoingAway()) { + return; + } + + try page._to_load.append(page.arena, self._proto); +} + pub const JsApi = struct { pub const bridge = js.Bridge(Style); @@ -113,13 +122,6 @@ pub const JsApi = struct { pub const sheet = bridge.accessor(Style.getSheet, null, .{}); }; -pub const Build = struct { - pub fn created(node: *Node, page: *Page) !void { - // Push to `_to_load` to dispatch load event just before window load event. - return page._to_load.append(page.arena, node.as(Element.Html)); - } -}; - const testing = @import("../../../../testing.zig"); test "WebApi: Style" { try testing.htmlRunner("element/html/style.html", .{});