diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 89cba801..cce80fe9 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -130,16 +130,6 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E } fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { - if (event._bubbles == false) { - event._event_phase = .at_target; - const target_et = target.asEventTarget(); - if (self.lookup.getPtr(@intFromPtr(target_et))) |list| { - try self.dispatchPhase(list, target_et, event, null); - } - event._event_phase = .none; - return; - } - var path_len: usize = 0; var path_buffer: [128]*EventTarget = undefined; @@ -150,7 +140,8 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { path_len += 1; } - // Even though the window isn't part of the DOM, events bubble to it + // Even though the window isn't part of the DOM, events always propagate + // through it in the capture phase if (path_len < path_buffer.len) { path_buffer[path_len] = self.page.window.asEventTarget(); path_len += 1; @@ -159,6 +150,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { const path = path_buffer[0..path_len]; // Phase 1: Capturing phase (root → target, excluding target) + // This happens for all events, regardless of bubbling event._event_phase = .capturing_phase; var i: usize = path_len; while (i > 1) { @@ -173,6 +165,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { } } + // Phase 2: At target event._event_phase = .at_target; const target_et = target.asEventTarget(); if (self.lookup.getPtr(@intFromPtr(target_et))) |list| { @@ -183,12 +176,16 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { } } - event._event_phase = .bubbling_phase; - for (path[1..]) |current_target| { - if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { - try self.dispatchPhase(list, current_target, event, false); - if (event._stop_propagation) { - break; + // Phase 3: Bubbling phase (target → root, excluding target) + // This only happens if the event bubbles + if (event._bubbles) { + event._event_phase = .bubbling_phase; + for (path[1..]) |current_target| { + if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { + try self.dispatchPhase(list, current_target, event, false); + if (event._stop_propagation) { + break; + } } } } diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 008fb60f..312b85ba 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const builtin = @import("builtin"); const js = @import("js/js.zig"); const log = @import("../log.zig"); @@ -31,11 +32,13 @@ const Element = @import("webapi/Element.zig"); const Allocator = std.mem.Allocator; const ArrayListUnmanaged = std.ArrayListUnmanaged; +const IS_DEBUG = builtin.mode == .Debug; + const ScriptManager = @This(); page: *Page, -// used to prevent recursive evalutaion +// used to prevent recursive evaluation is_evaluating: bool, // Only once this is true can deferred scripts be run @@ -45,9 +48,6 @@ static_scripts_done: bool, // on shutdown/abort, we need to cleanup any pending ones. asyncs: OrderList, -// Normal scripts (non-deferred & non-async). These must be executed in order -scripts: OrderList, - // List of deferred scripts. These must be executed in order, but only once // dom_loaded == true, deferreds: OrderList, @@ -85,7 +85,6 @@ pub fn init(page: *Page) ScriptManager { return .{ .page = page, .asyncs = .{}, - .scripts = .{}, .deferreds = .{}, .importmap = .empty, .sync_modules = .empty, @@ -130,7 +129,6 @@ pub fn reset(self: *ScriptManager) void { self.importmap = .empty; self.clearList(&self.asyncs); - self.clearList(&self.scripts); self.clearList(&self.deferreds); self.static_scripts_done = false; } @@ -212,57 +210,78 @@ pub fn add(self: *ScriptManager, script_element: *Element.Html.Script, comptime .is_async = if (remote_url == null) false else element.getAttributeSafe("async") != null, }; - if (source == .@"inline" and self.scripts.first == null) { - // inline script with no pending scripts, execute it immediately. - // (if there is a pending script, then we cannot execute this immediately - // as it needs to be executed in order) + if (source == .@"inline") { + // inline script gets executed immediately return script.eval(page); } - const pending_script = try self.script_pool.create(); - errdefer self.script_pool.destroy(pending_script); - pending_script.* = .{ - .script = script, - .complete = false, - .manager = self, - .node = .{}, + const pending_script = blk: { + // Done in a block this way so that, if something fails in this block + // it's cleaned up with these errdefers + // BUT, if we need to load/execute the script immediately, cleanup/lifetimes + // become the responsibility of the outer block. + const pending_script = try self.script_pool.create(); + errdefer self.script_pool.destroy(pending_script); + + pending_script.* = .{ + .script = script, + .complete = false, + .manager = self, + .node = .{}, + }; + errdefer pending_script.deinit(); + + if (comptime IS_DEBUG) { + log.debug(.http, "script queue", .{ + .ctx = ctx, + .url = remote_url.?, + .stack = page.js.stackTrace() catch "???", + }); + } + + var headers = try self.client.newHeaders(); + try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers); + + try self.client.request(.{ + .url = remote_url.?, + .ctx = pending_script, + .method = .GET, + .headers = headers, + .resource_type = .script, + .cookie_jar = &page._session.cookie_jar, + .start_callback = if (log.enabled(.http, .debug)) startCallback else null, + .header_callback = headerCallback, + .data_callback = dataCallback, + .done_callback = doneCallback, + .error_callback = errorCallback, + }); + + if (script.is_defer) { + // non-blocking loading, track the list this belongs to, and return + pending_script.list = &self.deferreds; + return; + } + + if (script.is_async) { + // non-blocking loading, track the list this belongs to, and return + pending_script.list = &self.asyncs; + return; + } + + break :blk pending_script; }; - if (source == .@"inline") { - // if we're here, it means that we have pending scripts (i.e. self.scripts - // is not empty). Because the script is inline, it's complete/ready, but - // we need to process them in order - pending_script.complete = true; - self.scripts.append(&pending_script.node); - return; - } else { - log.debug(.http, "script queue", .{ - .ctx = ctx, - .url = remote_url.?, - .stack = page.js.stackTrace() catch "???", - }); + defer pending_script.deinit(); + + // this is , it needs to block the caller + // until it's evaluated + var client = self.client; + while (true) { + if (pending_script.complete) { + return pending_script.script.eval(page); + } + _ = try client.tick(200); } - - pending_script.getList().append(&pending_script.node); - - errdefer pending_script.deinit(); - - var headers = try self.client.newHeaders(); - try page.requestCookie(.{}).headersForRequest(page.arena, remote_url.?, &headers); - - try self.client.request(.{ - .url = remote_url.?, - .ctx = pending_script, - .method = .GET, - .headers = headers, - .resource_type = .script, - .cookie_jar = &page._session.cookie_jar, - .start_callback = if (log.enabled(.http, .debug)) startCallback else null, - .header_callback = headerCallback, - .data_callback = dataCallback, - .done_callback = doneCallback, - .error_callback = errorCallback, - }); } // Resolve a module specifier to an valid URL. @@ -394,6 +413,7 @@ pub fn getAsyncModule(self: *ScriptManager, url: [:0]const u8, cb: AsyncModule.C .error_callback = AsyncModule.errorCallback, }); } + pub fn pageIsLoaded(self: *ScriptManager) void { std.debug.assert(self.static_scripts_done == false); self.static_scripts_done = true; @@ -415,15 +435,6 @@ fn evaluate(self: *ScriptManager) void { self.is_evaluating = true; defer self.is_evaluating = false; - while (self.scripts.first) |n| { - var pending_script: *PendingScript = @fieldParentPtr("node", n); - if (pending_script.complete == false) { - return; - } - defer pending_script.deinit(); - pending_script.script.eval(page); - } - if (self.static_scripts_done == false) { // We can only execute deferred scripts if // 1 - all the normal scripts are done @@ -460,7 +471,6 @@ fn evaluate(self: *ScriptManager) void { pub fn isDone(self: *const ScriptManager) bool { return self.asyncs.first == null and // there are no more async scripts self.static_scripts_done and // and we've finished parsing the HTML to queue all - self.scripts.first == null and // and there are no more --> +
diff --git a/src/browser/tests/net/url_search_params.html b/src/browser/tests/net/url_search_params.html index 689e9e68..12b98f26 100644 --- a/src/browser/tests/net/url_search_params.html +++ b/src/browser/tests/net/url_search_params.html @@ -20,8 +20,8 @@ - + --> diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index d808e70f..45379018 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -56,6 +56,15 @@ pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback: js.Fun return page._event_manager.remove(self, typ, callback, use_capture); } +pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void { + return switch (self._type) { + .node => |n| n.format(writer), + .window => writer.writeAll(""), + .xhr => writer.writeAll(""), + .abort_signal => writer.writeAll(""), + }; +} + pub const JsApi = struct { pub const bridge = js.Bridge(EventTarget); diff --git a/src/browser/webapi/storage/storage.zig b/src/browser/webapi/storage/storage.zig index 8813c092..00e06bf3 100644 --- a/src/browser/webapi/storage/storage.zig +++ b/src/browser/webapi/storage/storage.zig @@ -9,7 +9,7 @@ pub fn registerTypes() []const type { } pub const Jar = @import("cookie.zig").Jar; -pub const Cookie =@import("cookie.zig").Cookie; +pub const Cookie = @import("cookie.zig").Cookie; pub const Shed = struct { _origins: std.StringHashMapUnmanaged(*Bucket) = .empty, diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 7c086f6f..3912b842 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -117,11 +117,12 @@ const TestContext = struct { bc.session_id = sid; } - if (opts.html) |html| { - if (bc.session_id == null) bc.session_id = "SID-X"; - const page = try bc.session.createPage(); - page.window.document = (try Document.init(html)).doc; - } + // @ZIGDOM + // if (opts.html) |html| { + // if (bc.session_id == null) bc.session_id = "SID-X"; + // const page = try bc.session.createPage(); + // page.window._document = (try Document.init(html)).doc; + // } return bc; } diff --git a/src/testing.zig b/src/testing.zig index 7526180e..a4805f98 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -422,9 +422,8 @@ test { const log = @import("log.zig"); const TestHTTPServer = @import("TestHTTPServer.zig"); -// @ZIGDOM-CDP -// const Server = @import("Server.zig"); -// var test_cdp_server: ?Server = null; +const Server = @import("Server.zig"); +var test_cdp_server: ?Server = null; var test_http_server: ?TestHTTPServer = null; test "tests:beforeAll" { @@ -446,12 +445,10 @@ test "tests:beforeAll" { var wg: std.Thread.WaitGroup = .{}; wg.startMany(2); - // @ZIGDOM-CDP - // { - // const thread = try std.Thread.spawn(.{}, serveCDP, .{&wg}); - // thread.detach(); - // } - wg.finish(); // @ZIGDOM-CDP REMOVE + { + const thread = try std.Thread.spawn(.{}, serveCDP, .{&wg}); + thread.detach(); + } test_http_server = TestHTTPServer.init(testHTTPHandler); { @@ -465,10 +462,9 @@ test "tests:beforeAll" { } test "tests:afterAll" { - // @ZIGDOM-CDP - // if (test_cdp_server) |*server| { - // server.deinit(); - // } + if (test_cdp_server) |*server| { + server.deinit(); + } if (test_http_server) |*server| { server.deinit(); } @@ -477,20 +473,19 @@ test "tests:afterAll" { test_app.deinit(); } -// @ZIGDOM-CDP -// fn serveCDP(wg: *std.Thread.WaitGroup) !void { -// const address = try std.net.Address.parseIp("127.0.0.1", 9583); -// test_cdp_server = try Server.init(test_app, address); +fn serveCDP(wg: *std.Thread.WaitGroup) !void { + const address = try std.net.Address.parseIp("127.0.0.1", 9583); + test_cdp_server = try Server.init(test_app, address); -// var server = try Server.init(test_app, address); -// defer server.deinit(); -// wg.finish(); + var server = try Server.init(test_app, address); + defer server.deinit(); + wg.finish(); -// test_cdp_server.?.run(address, 5) catch |err| { -// std.debug.print("CDP server error: {}", .{err}); -// return err; -// }; -// } + test_cdp_server.?.run(address, 5) catch |err| { + std.debug.print("CDP server error: {}", .{err}); + return err; + }; +} fn testHTTPHandler(req: *std.http.Server.Request) !void { const path = req.head.target;