mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Merge pull request #1640 from lightpanda-io/nikneym/load-events-after-doc-complete
Dispatch `load` events that're attached after `documentIsComplete`
This commit is contained in:
@@ -723,23 +723,13 @@ pub fn documentIsComplete(self: *Page) void {
|
|||||||
fn _documentIsComplete(self: *Page) !void {
|
fn _documentIsComplete(self: *Page) !void {
|
||||||
self.document._ready_state = .complete;
|
self.document._ready_state = .complete;
|
||||||
|
|
||||||
|
// Run load events before window.load.
|
||||||
|
try self.dispatchLoad();
|
||||||
|
|
||||||
var ls: JS.Local.Scope = undefined;
|
var ls: JS.Local.Scope = undefined;
|
||||||
self.js.localScope(&ls);
|
self.js.localScope(&ls);
|
||||||
defer ls.deinit();
|
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.
|
// Dispatch window.load event.
|
||||||
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
|
||||||
// This event is weird, it's dispatched directly on the window, but
|
// 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 {
|
pub fn scheduleMutationDelivery(self: *Page) !void {
|
||||||
if (self._mutation_delivery_scheduled) {
|
if (self._mutation_delivery_scheduled) {
|
||||||
return;
|
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 });
|
log.err(.page, "page.nodeIsReady", .{ .err = err, .element = "iframe", .type = self._type, .url = self.url });
|
||||||
return err;
|
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;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -241,6 +241,9 @@ fn _wait(self: *Session, page: *Page, wait_ms: u32) !WaitResult {
|
|||||||
// it AFTER.
|
// it AFTER.
|
||||||
const ms_to_next_task = try browser.runMacrotasks();
|
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 http_active = http_client.active;
|
||||||
const total_network_activity = http_active + http_client.intercepted;
|
const total_network_activity = http_active + http_client.intercepted;
|
||||||
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
if (page._notified_network_almost_idle.check(total_network_activity <= 2)) {
|
||||||
|
|||||||
@@ -114,48 +114,15 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="load-trigger-event">
|
<body></body>
|
||||||
|
|
||||||
|
<script id="img-load-event">
|
||||||
{
|
{
|
||||||
|
// An img fires a load event when src is set.
|
||||||
const img = document.createElement("img");
|
const img = document.createElement("img");
|
||||||
let count = 0;
|
let result = false;
|
||||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
|
||||||
testing.expectEqual(true, count < 3);
|
|
||||||
count++;
|
|
||||||
|
|
||||||
testing.expectEqual(false, bubbles);
|
|
||||||
testing.expectEqual(false, cancelBubble);
|
|
||||||
testing.expectEqual(false, cancelable);
|
|
||||||
testing.expectEqual(false, composed);
|
|
||||||
testing.expectEqual(true, isTrusted);
|
|
||||||
testing.expectEqual(img, target);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
|
||||||
testing.expectEqual("https://cdn.lightpanda.io/website/assets/images/docs/hn.png", img.src);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure count is incremented asynchronously.
|
|
||||||
testing.expectEqual(0, count);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<img
|
|
||||||
id="inline-img"
|
|
||||||
src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png"
|
|
||||||
onload="(() => testing.expectEqual(true, true))()"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<script id="inline-on-load">
|
|
||||||
{
|
|
||||||
const img = document.getElementById("inline-img");
|
|
||||||
testing.expectEqual(true, img.onload instanceof Function);
|
|
||||||
// Also call inline to double check.
|
|
||||||
img.onload();
|
|
||||||
|
|
||||||
// Make sure ones attached with `addEventListener` also executed.
|
|
||||||
testing.async(async () => {
|
testing.async(async () => {
|
||||||
const result = await new Promise(resolve => {
|
await new Promise(resolve => {
|
||||||
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
testing.expectEqual(false, bubbles);
|
testing.expectEqual(false, bubbles);
|
||||||
testing.expectEqual(false, cancelBubble);
|
testing.expectEqual(false, cancelBubble);
|
||||||
@@ -163,13 +130,38 @@
|
|||||||
testing.expectEqual(false, composed);
|
testing.expectEqual(false, composed);
|
||||||
testing.expectEqual(true, isTrusted);
|
testing.expectEqual(true, isTrusted);
|
||||||
testing.expectEqual(img, target);
|
testing.expectEqual(img, target);
|
||||||
|
result = true;
|
||||||
return resolve(true);
|
return resolve();
|
||||||
});
|
});
|
||||||
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
});
|
});
|
||||||
|
|
||||||
testing.expectEqual(true, result);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="img-no-load-without-src">
|
||||||
|
{
|
||||||
|
// An img without src should not fire a load event.
|
||||||
|
let fired = false;
|
||||||
|
const img = document.createElement("img");
|
||||||
|
img.addEventListener("load", () => { fired = true; });
|
||||||
|
document.body.appendChild(img);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="lazy-src-set">
|
||||||
|
{
|
||||||
|
// Append to DOM first, then set src — load should still fire.
|
||||||
|
const img = document.createElement("img");
|
||||||
|
let result = false;
|
||||||
|
img.onload = () => result = true;
|
||||||
|
document.body.appendChild(img);
|
||||||
|
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -19,3 +19,68 @@
|
|||||||
l2.crossOrigin = '';
|
l2.crossOrigin = '';
|
||||||
testing.expectEqual('anonymous', l2.crossOrigin);
|
testing.expectEqual('anonymous', l2.crossOrigin);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="link-load-event">
|
||||||
|
{
|
||||||
|
// A link with rel=stylesheet and a non-empty href fires a load event when appended to the DOM
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
|
testing.async(async () => {
|
||||||
|
const result = await new Promise(resolve => {
|
||||||
|
link.addEventListener('load', ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
|
testing.expectEqual(false, bubbles);
|
||||||
|
testing.expectEqual(false, cancelBubble);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
testing.expectEqual(false, composed);
|
||||||
|
testing.expectEqual(true, isTrusted);
|
||||||
|
testing.expectEqual(link, target);
|
||||||
|
resolve(true);
|
||||||
|
});
|
||||||
|
document.head.appendChild(link);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, result);
|
||||||
|
});
|
||||||
|
testing.expectEqual(true, true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-no-load-without-href">
|
||||||
|
{
|
||||||
|
// A link with rel=stylesheet but no href should not fire a load event
|
||||||
|
let fired = false;
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.rel = 'stylesheet';
|
||||||
|
link.addEventListener('load', () => { fired = true; });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="link-no-load-wrong-rel">
|
||||||
|
{
|
||||||
|
// A link without rel=stylesheet should not fire a load event
|
||||||
|
let fired = false;
|
||||||
|
const link = document.createElement('link');
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
link.addEventListener('load', () => { fired = true; });
|
||||||
|
document.head.appendChild(link);
|
||||||
|
testing.eventually(() => testing.expectEqual(false, fired));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script id="lazy-href-set">
|
||||||
|
{
|
||||||
|
let result = false;
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.onload = () => result = true;
|
||||||
|
// Append to DOM,
|
||||||
|
document.head.appendChild(link);
|
||||||
|
// then set href.
|
||||||
|
link.href = 'https://lightpanda.io/opensource-browser/15';
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -106,3 +106,28 @@
|
|||||||
testing.expectEqual(true, style.disabled);
|
testing.expectEqual(true, style.disabled);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script id="style-load-event">
|
||||||
|
{
|
||||||
|
// A style element fires a load event when appended to the DOM.
|
||||||
|
const style = document.createElement("style");
|
||||||
|
let result = false;
|
||||||
|
testing.async(async () => {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
style.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
|
||||||
|
testing.expectEqual(false, bubbles);
|
||||||
|
testing.expectEqual(false, cancelBubble);
|
||||||
|
testing.expectEqual(false, cancelable);
|
||||||
|
testing.expectEqual(false, composed);
|
||||||
|
testing.expectEqual(true, isTrusted);
|
||||||
|
testing.expectEqual(style, target);
|
||||||
|
result = true;
|
||||||
|
return resolve();
|
||||||
|
});
|
||||||
|
document.head.appendChild(style);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testing.eventually(() => testing.expectEqual(true, result));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -722,6 +722,8 @@ const CloneError = error{
|
|||||||
CloneError,
|
CloneError,
|
||||||
IFrameLoadError,
|
IFrameLoadError,
|
||||||
TooManyContexts,
|
TooManyContexts,
|
||||||
|
LinkLoadError,
|
||||||
|
StyleLoadError,
|
||||||
};
|
};
|
||||||
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
|
pub fn cloneNode(self: *Node, deep_: ?bool, page: *Page) CloneError!*Node {
|
||||||
const deep = deep_ orelse false;
|
const deep = deep_ orelse false;
|
||||||
|
|||||||
@@ -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 {
|
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 {
|
pub fn getAlt(self: *const Image) []const u8 {
|
||||||
@@ -120,6 +123,21 @@ pub fn getComplete(_: *const Image) bool {
|
|||||||
return true;
|
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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Image);
|
pub const bridge = js.Bridge(Image);
|
||||||
|
|
||||||
@@ -145,13 +163,7 @@ pub const JsApi = struct {
|
|||||||
pub const Build = struct {
|
pub const Build = struct {
|
||||||
pub fn created(node: *Node, page: *Page) !void {
|
pub fn created(node: *Node, page: *Page) !void {
|
||||||
const self = node.as(Image);
|
const self = node.as(Image);
|
||||||
const image = self.asElement();
|
return self.imageAddedCallback(page);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,12 @@ pub fn getHref(self: *Link, page: *Page) ![]const u8 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn setHref(self: *Link, value: []const u8, page: *Page) !void {
|
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 {
|
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);
|
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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Link);
|
pub const bridge = js.Bridge(Link);
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,15 @@ pub fn getSheet(self: *Style, page: *Page) !?*CSSStyleSheet {
|
|||||||
return sheet;
|
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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Style);
|
pub const bridge = js.Bridge(Style);
|
||||||
|
|
||||||
@@ -113,13 +122,6 @@ pub const JsApi = struct {
|
|||||||
pub const sheet = bridge.accessor(Style.getSheet, null, .{});
|
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");
|
const testing = @import("../../../../testing.zig");
|
||||||
test "WebApi: Style" {
|
test "WebApi: Style" {
|
||||||
try testing.htmlRunner("element/html/style.html", .{});
|
try testing.htmlRunner("element/html/style.html", .{});
|
||||||
|
|||||||
Reference in New Issue
Block a user