diff --git a/src/TestHTTPServer.zig b/src/TestHTTPServer.zig index 85abaf06..21d9fa78 100644 --- a/src/TestHTTPServer.zig +++ b/src/TestHTTPServer.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const URL = @import("browser/URL.zig"); const TestHTTPServer = @This(); @@ -97,7 +98,10 @@ fn handleConnection(self: *TestHTTPServer, conn: std.net.Server.Connection) !voi } pub fn sendFile(req: *std.http.Server.Request, file_path: []const u8) !void { - var file = std.fs.cwd().openFile(file_path, .{}) catch |err| switch (err) { + var url_buf: [1024]u8 = undefined; + var fba = std.heap.FixedBufferAllocator.init(&url_buf); + const unescaped_file_path = try URL.unescape(fba.allocator(), file_path); + var file = std.fs.cwd().openFile(unescaped_file_path, .{}) catch |err| switch (err) { error.FileNotFound => return req.respond("server error", .{ .status = .not_found }), else => return err, }; diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 6e80c2de..101cb522 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -346,7 +346,10 @@ pub fn deinit(self: *Page) void { session.browser.env.destroyContext(self.js); self._script_manager.shutdown = true; - session.browser.http_client.abort(); + if (self.parent == null) { + // only the root frame needs to abort this. It's more efficient this way + session.browser.http_client.abort(); + } self._script_manager.deinit(); if (comptime IS_DEBUG) { @@ -796,7 +799,12 @@ fn pageDataCallback(transfer: *Http.Transfer, data: []const u8) !void { } orelse .unknown; if (comptime IS_DEBUG) { - log.debug(.page, "navigate first chunk", .{ .content_type = mime.content_type, .len = data.len, .type = self._type, .url = self.url }); + log.debug(.page, "navigate first chunk", .{ + .content_type = mime.content_type, + .len = data.len, + .type = self._type, + .url = self.url, + }); } switch (mime.content_type) { @@ -850,7 +858,11 @@ fn pageDoneCallback(ctx: *anyopaque) !void { try self._session.navigation.commitNavigation(self); defer if (comptime IS_DEBUG) { - log.debug(.page, "page.load.complete", .{ .url = self.url, .type = self._type }); + log.debug(.page, "page load complete", .{ + .url = self.url, + .type = self._type, + .state = std.meta.activeTag(self._parse_state), + }); }; const parse_arena = try self.getArena(.{ .debug = "Page.parse" }); @@ -962,21 +974,28 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void { } iframe._executed = true; - const session = self._session; - const frame_id = session.nextFrameId(); + + // A frame can be re-navigated by setting the src. + const existing_window = iframe._content_window; + const page_frame = try self.arena.create(Page); + const frame_id = blk: { + if (existing_window) |w| { + const existing_frame_id = w._page._frame_id; + session.browser.http_client.abortFrame(existing_frame_id); + break :blk existing_frame_id; + } + break :blk session.nextFrameId(); + }; + try Page.init(page_frame, frame_id, session, self); + errdefer page_frame.deinit(); self._pending_loads += 1; page_frame.iframe = iframe; iframe._content_window = page_frame.window; - - self._session.notification.dispatch(.page_frame_created, &.{ - .frame_id = frame_id, - .parent_id = self._frame_id, - .timestamp = timestamp(.monotonic), - }); + errdefer iframe._content_window = null; // navigate will dupe the url const url = try URL.resolve( @@ -986,6 +1005,15 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void { .{ .encode = true }, ); + if (existing_window == null) { + // on first load, dispatch frame_created evnet + self._session.notification.dispatch(.page_frame_created, &.{ + .page_id = page_id, + .parent_id = self.id, + .timestamp = timestamp(.monotonic), + }); + } + page_frame.navigate(url, .{ .reason = .initialFrameNavigation }) catch |err| { log.warn(.page, "iframe navigate failure", .{ .url = url, .err = err }); self._pending_loads -= 1; @@ -994,6 +1022,25 @@ pub fn iframeAddedCallback(self: *Page, iframe: *Element.Html.IFrame) !void { return error.IFrameLoadError; }; + if (existing_window) |w| { + const existing_page = w._page; + if (existing_page._parent_notified == false) { + self._pending_loads -= 1; + } + + for (self.frames.items, 0..) |p, i| { + if (p == existing_page) { + self.frames.items[i] = page_frame; + break; + } + } else { + lp.assert(false, "Existing frame not found", .{ .len = self.frames.items.len }); + } + + existing_page.deinit(); + return; + } + // window[N] is based on document order. For now we'll just append the frame // at the end of our list and set frames_sorted == false. window.getFrame // will check this flag to decide if it needs to sort the frames or not. diff --git a/src/browser/tests/frames/frames.html b/src/browser/tests/frames/frames.html index 1aa81b21..15b59677 100644 --- a/src/browser/tests/frames/frames.html +++ b/src/browser/tests/frames/frames.html @@ -11,6 +11,9 @@ diff --git a/src/browser/tests/frames/support/sub 1.html b/src/browser/tests/frames/support/sub 1.html index f6b8ec4b..c516a6b7 100644 --- a/src/browser/tests/frames/support/sub 1.html +++ b/src/browser/tests/frames/support/sub 1.html @@ -3,4 +3,5 @@ diff --git a/src/browser/webapi/element/html/IFrame.zig b/src/browser/webapi/element/html/IFrame.zig index 1a5498a3..e9cffc6e 100644 --- a/src/browser/webapi/element/html/IFrame.zig +++ b/src/browser/webapi/element/html/IFrame.zig @@ -58,6 +58,9 @@ pub fn setSrc(self: *IFrame, src: []const u8, page: *Page) !void { try element.setAttributeSafe(comptime .wrap("src"), .wrap(src), page); self._src = element.getAttributeSafe(comptime .wrap("src")) orelse unreachable; if (element.asNode().isConnected()) { + // unlike script, an iframe is reloaded every time the src is set + // even if it's set to the same URL. + self._executed = false; try page.iframeAddedCallback(self); } }