diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index e9c0e3eb..48614603 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -365,6 +365,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { inspector_session: *js.Inspector.Session, isolated_worlds: std.ArrayList(*IsolatedWorld), + // Scripts registered via Page.addScriptToEvaluateOnNewDocument. + // Evaluated in each new document after navigation completes. + scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty, + next_script_id: u32 = 1, + http_proxy_changed: bool = false, // Extra headers to add to all requests. @@ -764,6 +769,11 @@ pub fn BrowserContext(comptime CDP_T: type) type { /// Clients create this to be able to create variables and run code without interfering with the /// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after /// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed +const ScriptOnNewDocument = struct { + identifier: u32, + source: []const u8, +}; + /// in the isolated world by using its Context ID or the worldName. /// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects. /// An isolated world has it's own instance of globals like Window. diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 0e781fa4..a76fb716 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -37,6 +37,7 @@ pub fn processMessage(cmd: anytype) !void { getFrameTree, setLifecycleEventsEnabled, addScriptToEvaluateOnNewDocument, + removeScriptToEvaluateOnNewDocument, createIsolatedWorld, navigate, reload, @@ -51,6 +52,7 @@ pub fn processMessage(cmd: anytype) !void { .getFrameTree => return getFrameTree(cmd), .setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd), .addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd), + .removeScriptToEvaluateOnNewDocument => return removeScriptToEvaluateOnNewDocument(cmd), .createIsolatedWorld => return createIsolatedWorld(cmd), .navigate => return navigate(cmd), .reload => return doReload(cmd), @@ -147,22 +149,52 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void { return cmd.sendResult(null, .{}); } -// TODO: hard coded method -// With the command we receive a script we need to store and run for each new document. -// Note that the worldName refers to the name given to the isolated world. fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { - // const params = (try cmd.params(struct { - // source: []const u8, - // worldName: ?[]const u8 = null, - // includeCommandLineAPI: bool = false, - // runImmediately: bool = false, - // })) orelse return error.InvalidParams; + const params = (try cmd.params(struct { + source: []const u8, + worldName: ?[]const u8 = null, + includeCommandLineAPI: bool = false, + runImmediately: bool = false, + })) orelse return error.InvalidParams; + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + const script_id = bc.next_script_id; + bc.next_script_id += 1; + + const source_dupe = try bc.arena.dupe(u8, params.source); + try bc.scripts_on_new_document.append(bc.arena, .{ + .identifier = script_id, + .source = source_dupe, + }); + + var id_buf: [16]u8 = undefined; + const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{script_id}) catch "1"; return cmd.sendResult(.{ - .identifier = "1", + .identifier = id_str, }, .{}); } +fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void { + const params = (try cmd.params(struct { + identifier: []const u8, + })) orelse return error.InvalidParams; + + const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; + + const target_id = std.fmt.parseInt(u32, params.identifier, 10) catch + return cmd.sendResult(null, .{}); + + for (bc.scripts_on_new_document.items, 0..) |script, i| { + if (script.identifier == target_id) { + _ = bc.scripts_on_new_document.orderedRemove(i); + break; + } + } + + return cmd.sendResult(null, .{}); +} + fn close(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; @@ -482,6 +514,22 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P ); } + // Evaluate scripts registered via Page.addScriptToEvaluateOnNewDocument. + // Must run after the execution context is created but before the client + // receives frameNavigated/loadEventFired so polyfills are available for + // subsequent CDP commands. + if (bc.scripts_on_new_document.items.len > 0) { + var ls: js.Local.Scope = undefined; + page.js.localScope(&ls); + defer ls.deinit(); + + for (bc.scripts_on_new_document.items) |script| { + ls.local.eval(script.source, null) catch |err| { + log.warn(.cdp, "script on new doc failed", .{ .err = err }); + }; + } + } + // frameNavigated event try cdp.sendEvent("Page.frameNavigated", .{ .type = "Navigation", @@ -840,3 +888,38 @@ test "cdp.page: reload" { try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } }); } } + +test "cdp.page: addScriptToEvaluateOnNewDocument" { + var ctx = try testing.context(); + defer ctx.deinit(); + + _ = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* }); + + { + // Register a script — should return unique identifier "1" + try ctx.processMessage(.{ .id = 20, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test = 1" } }); + try ctx.expectSentResult(.{ + .identifier = "1", + }, .{ .id = 20 }); + } + + { + // Register another script — should return identifier "2" + try ctx.processMessage(.{ .id = 21, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test2 = 2" } }); + try ctx.expectSentResult(.{ + .identifier = "2", + }, .{ .id = 21 }); + } + + { + // Remove the first script — should succeed + try ctx.processMessage(.{ .id = 22, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "1" } }); + try ctx.expectSentResult(null, .{ .id = 22 }); + } + + { + // Remove a non-existent identifier — should succeed silently + try ctx.processMessage(.{ .id = 23, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "999" } }); + try ctx.expectSentResult(null, .{ .id = 23 }); + } +}