Compare commits

..

2 Commits

Author SHA1 Message Date
Karl Seguin
078e13d8a4 Remove DOMContentLoaded and Loaded events from page_navigated
These were moved to their own distinct events, and should have been removed from
here.
2026-03-30 11:39:38 +08:00
Karl Seguin
cabb029bed Improve/Fix CDP navigation event order
These changes all better align with chrome's event ordering/timing.

There are two big changes. The first is that our internal page_navigated event,
which is kind of our heavy hitter, is sent once the header is received as
opposed to (much later) on document load. The main goal of this internal event
is to trigger the "Page.frameNavigated" CDP event which is meant to happen
once the URL is committed, which _is_ on header response.

To accommodate this earlier trigger, new explicit events for DOMContentLoaded
and load have be added.

This drastically changes the flow of events as things go from:
Start Page Navigation
Response Received
  Start Frame Navigation
  Response Received
  End Frame Navigation
End Page Navigation
context clear + reset
DOMContentLoaded
Loaded

TO:
Start Page Navigation
Response Received
End Page Navigation
context clear + reset
Start Frame Navigation
Response Received
End Frame Navigation
DOMContentLoaded
Loaded

So not only does it remove the nesting, but it ensures that the context are
cleared and reset once the main page's navigation is locked in, and before any
frame is created.
2026-03-30 11:02:53 +08:00
24 changed files with 741 additions and 827 deletions

View File

@@ -74,6 +74,8 @@ const EventListeners = struct {
page_network_idle: List = .{},
page_network_almost_idle: List = .{},
page_frame_created: List = .{},
page_dom_content_loaded: List = .{},
page_loaded: List = .{},
http_request_fail: List = .{},
http_request_start: List = .{},
http_request_intercept: List = .{},
@@ -91,6 +93,8 @@ const Events = union(enum) {
page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle,
page_frame_created: *const PageFrameCreated,
page_dom_content_loaded: *const PageDOMContentLoaded,
page_loaded: *const PageLoaded,
http_request_fail: *const RequestFail,
http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept,
@@ -137,6 +141,18 @@ pub const PageFrameCreated = struct {
timestamp: u64,
};
pub const PageDOMContentLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const PageLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const RequestStart = struct {
transfer: *Transfer,
};

View File

@@ -1325,15 +1325,15 @@ pub const Transfer = struct {
}
}
transfer.req.notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
const proceed = transfer.req.header_callback(transfer) catch |err| {
log.err(.http, "header_callback", .{ .err = err, .req = transfer });
return err;
};
transfer.req.notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
return proceed and transfer.aborted == false;
}

View File

@@ -487,7 +487,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
return error.InjectBlankFailed;
};
}
self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{
.frame_id = self._frame_id,
@@ -519,6 +518,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// force next request id manually b/c we won't create a real req.
_ = session.browser.http_client.incrReqId();
self.documentIsComplete();
return;
}
@@ -738,6 +739,12 @@ pub fn _documentIsLoaded(self: *Page) !void {
self.document.asEventTarget(),
event,
);
self._session.notification.dispatch(.page_dom_content_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
}
pub fn scriptsCompletedLoading(self: *Page) void {
@@ -796,19 +803,6 @@ pub fn documentIsComplete(self: *Page) void {
self._documentIsComplete() catch |err| {
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
};
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
}
fn _documentIsComplete(self: *Page) !void {
@@ -827,6 +821,12 @@ fn _documentIsComplete(self: *Page) !void {
try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" });
}
self._session.notification.dispatch(.page_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) {
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
@@ -879,6 +879,19 @@ fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
});
}
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
return true;
}

View File

@@ -15,9 +15,8 @@
testing.expectEqual(true, validPlatforms.includes(navigator.platform));
testing.expectEqual('en-US', navigator.language);
testing.expectEqual(true, Array.isArray(navigator.languages));
testing.expectEqual(2, navigator.languages.length);
testing.expectEqual(1, navigator.languages.length);
testing.expectEqual('en-US', navigator.languages[0]);
testing.expectEqual('en', navigator.languages[1]);
testing.expectEqual(true, navigator.onLine);
testing.expectEqual(true, navigator.cookieEnabled);
testing.expectEqual(true, navigator.hardwareConcurrency > 0);

View File

@@ -40,8 +40,8 @@ pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.config.http_headers.user_agent;
}
pub fn getLanguages(_: *const Navigator) [2][]const u8 {
return .{ "en-US", "en" };
pub fn getLanguages(_: *const Navigator) [1][]const u8 {
return .{"en-US"};
}
pub fn getPlatform(_: *const Navigator) []const u8 {

File diff suppressed because it is too large Load Diff

View File

@@ -18,9 +18,8 @@
const std = @import("std");
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,
@@ -33,15 +32,15 @@ pub fn processMessage(cmd: *CDP.Command) !void {
.getFullAXTree => return getFullAXTree(cmd),
}
}
fn enable(cmd: *CDP.Command) !void {
fn enable(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn disable(cmd: *CDP.Command) !void {
fn disable(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn getFullAXTree(cmd: *CDP.Command) !void {
fn getFullAXTree(cmd: anytype) !void {
const params = (try cmd.params(struct {
depth: ?i32 = null,
frameId: ?[]const u8 = null,

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
// TODO: hard coded data
const PROTOCOL_VERSION = "1.3";
@@ -36,7 +35,7 @@ const PRODUCT = "Chrome/124.0.6367.29";
const JS_VERSION = "12.4.254.8";
const DEV_TOOLS_WINDOW_ID = 1923710101;
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getVersion,
setPermission,
@@ -58,7 +57,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
}
fn getVersion(cmd: *CDP.Command) !void {
fn getVersion(cmd: anytype) !void {
// TODO: pre-serialize?
return cmd.sendResult(.{
.protocolVersion = PROTOCOL_VERSION,
@@ -70,7 +69,7 @@ fn getVersion(cmd: *CDP.Command) !void {
}
// TODO: noop method
fn setDownloadBehavior(cmd: *CDP.Command) !void {
fn setDownloadBehavior(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// behavior: []const u8,
// browserContextId: ?[]const u8 = null,
@@ -81,7 +80,7 @@ fn setDownloadBehavior(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{ .include_session_id = false });
}
fn getWindowForTarget(cmd: *CDP.Command) !void {
fn getWindowForTarget(cmd: anytype) !void {
// const params = (try cmd.params(struct {
// targetId: ?[]const u8 = null,
// })) orelse return error.InvalidParams;
@@ -92,22 +91,22 @@ fn getWindowForTarget(cmd: *CDP.Command) !void {
}
// TODO: noop method
fn setWindowBounds(cmd: *CDP.Command) !void {
fn setWindowBounds(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn grantPermissions(cmd: *CDP.Command) !void {
fn grantPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setPermission(cmd: *CDP.Command) !void {
fn setPermission(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn resetPermissions(cmd: *CDP.Command) !void {
fn resetPermissions(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
}, cmd.input.action) orelse return error.UnknownMethod;

View File

@@ -18,18 +18,17 @@
const std = @import("std");
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const Node = @import("../Node.zig");
const log = @import("../../log.zig");
const dump = @import("../../browser/dump.zig");
const js = @import("../../browser/js/js.zig");
const Node = @import("../Node.zig");
const DOMNode = @import("../../browser/webapi/Node.zig");
const Selector = @import("../../browser/webapi/selector/Selector.zig");
const dump = @import("../../browser/dump.zig");
const js = @import("../../browser/js/js.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
getDocument,
@@ -70,7 +69,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getDocument
fn getDocument(cmd: *CDP.Command) !void {
fn getDocument(cmd: anytype) !void {
const Params = struct {
// CDP documentation implies that 0 isn't valid, but it _does_ work in Chrome
depth: i32 = 3,
@@ -90,7 +89,7 @@ fn getDocument(cmd: *CDP.Command) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-performSearch
fn performSearch(cmd: *CDP.Command) !void {
fn performSearch(cmd: anytype) !void {
const params = (try cmd.params(struct {
query: []const u8,
includeUserAgentShadowDOM: ?bool = null,
@@ -117,7 +116,7 @@ fn performSearch(cmd: *CDP.Command) !void {
// hierarchy of each nodes.
// We dispatch event in the reverse order: from the top level to the direct parents.
// We should dispatch a node only if it has never been sent.
fn dispatchSetChildNodes(cmd: *CDP.Command, dom_nodes: []const *DOMNode) !void {
fn dispatchSetChildNodes(cmd: anytype, dom_nodes: []const *DOMNode) !void {
const arena = cmd.arena;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
@@ -173,7 +172,7 @@ fn dispatchSetChildNodes(cmd: *CDP.Command, dom_nodes: []const *DOMNode) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-discardSearchResults
fn discardSearchResults(cmd: *CDP.Command) !void {
fn discardSearchResults(cmd: anytype) !void {
const params = (try cmd.params(struct {
searchId: []const u8,
})) orelse return error.InvalidParams;
@@ -185,7 +184,7 @@ fn discardSearchResults(cmd: *CDP.Command) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getSearchResults
fn getSearchResults(cmd: *CDP.Command) !void {
fn getSearchResults(cmd: anytype) !void {
const params = (try cmd.params(struct {
searchId: []const u8,
fromIndex: u32,
@@ -210,7 +209,7 @@ fn getSearchResults(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .nodeIds = node_ids[params.fromIndex..params.toIndex] }, .{});
}
fn querySelector(cmd: *CDP.Command) !void {
fn querySelector(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
selector: []const u8,
@@ -236,7 +235,7 @@ fn querySelector(cmd: *CDP.Command) !void {
}, .{});
}
fn querySelectorAll(cmd: *CDP.Command) !void {
fn querySelectorAll(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
selector: []const u8,
@@ -267,7 +266,7 @@ fn querySelectorAll(cmd: *CDP.Command) !void {
}, .{});
}
fn resolveNode(cmd: *CDP.Command) !void {
fn resolveNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
@@ -328,7 +327,7 @@ fn resolveNode(cmd: *CDP.Command) !void {
} }, .{});
}
fn describeNode(cmd: *CDP.Command) !void {
fn describeNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -375,7 +374,7 @@ fn rectToQuad(rect: DOMNode.Element.DOMRect) Quad {
};
}
fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void {
fn scrollIntoViewIfNeeded(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
@@ -398,7 +397,7 @@ fn scrollIntoViewIfNeeded(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
fn getNode(arena: Allocator, bc: anytype, node_id: ?Node.Id, backend_node_id: ?Node.Id, object_id: ?[]const u8) !*Node {
const input_node_id = node_id orelse backend_node_id;
if (input_node_id) |input_node_id_| {
return bc.node_registry.lookup_by_id.get(input_node_id_) orelse return error.NodeNotFound;
@@ -418,7 +417,7 @@ fn getNode(arena: Allocator, bc: *CDP.BrowserContext, node_id: ?Node.Id, backend
// https://chromedevtools.github.io/devtools-protocol/tot/DOM/#method-getContentQuads
// Related to: https://drafts.csswg.org/cssom-view/#the-geometryutils-interface
fn getContentQuads(cmd: *CDP.Command) !void {
fn getContentQuads(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -444,7 +443,7 @@ fn getContentQuads(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .quads = &.{quad} }, .{});
}
fn getBoxModel(cmd: *CDP.Command) !void {
fn getBoxModel(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?u32 = null,
@@ -473,7 +472,7 @@ fn getBoxModel(cmd: *CDP.Command) !void {
} }, .{});
}
fn requestChildNodes(cmd: *CDP.Command) !void {
fn requestChildNodes(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: Node.Id,
depth: i32 = 1,
@@ -497,7 +496,7 @@ fn requestChildNodes(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn getFrameOwner(cmd: *CDP.Command) !void {
fn getFrameOwner(cmd: anytype) !void {
const params = (try cmd.params(struct {
frameId: []const u8,
})) orelse return error.InvalidParams;
@@ -513,7 +512,7 @@ fn getFrameOwner(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .nodeId = node.id, .backendNodeId = node.id }, .{});
}
fn getOuterHTML(cmd: *CDP.Command) !void {
fn getOuterHTML(cmd: anytype) !void {
const params = (try cmd.params(struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -535,7 +534,7 @@ fn getOuterHTML(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .outerHTML = aw.written() }, .{});
}
fn requestNode(cmd: *CDP.Command) !void {
fn requestNode(cmd: anytype) !void {
const params = (try cmd.params(struct {
objectId: []const u8,
})) orelse return error.InvalidParams;

View File

@@ -17,10 +17,9 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
setEmulatedMedia,
setFocusEmulationEnabled,
@@ -39,7 +38,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
// TODO: noop method
fn setEmulatedMedia(cmd: *CDP.Command) !void {
fn setEmulatedMedia(cmd: anytype) !void {
// const input = (try const incoming.params(struct {
// media: ?[]const u8 = null,
// features: ?[]struct{
@@ -52,7 +51,7 @@ fn setEmulatedMedia(cmd: *CDP.Command) !void {
}
// TODO: noop method
fn setFocusEmulationEnabled(cmd: *CDP.Command) !void {
fn setFocusEmulationEnabled(cmd: anytype) !void {
// const input = (try const incoming.params(struct {
// enabled: bool,
// })) orelse return error.InvalidParams;
@@ -60,16 +59,16 @@ fn setFocusEmulationEnabled(cmd: *CDP.Command) !void {
}
// TODO: noop method
fn setDeviceMetricsOverride(cmd: *CDP.Command) !void {
fn setDeviceMetricsOverride(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
// TODO: noop method
fn setTouchEmulationEnabled(cmd: *CDP.Command) !void {
fn setTouchEmulationEnabled(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn setUserAgentOverride(cmd: *CDP.Command) !void {
fn setUserAgentOverride(cmd: anytype) !void {
log.info(.app, "setUserAgentOverride ignored", .{});
return cmd.sendResult(null, .{});
}

View File

@@ -17,19 +17,17 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const Allocator = std.mem.Allocator;
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
const network = @import("network.zig");
const HttpClient = @import("../../browser/HttpClient.zig");
const net_http = @import("../../network/http.zig");
const Notification = @import("../../Notification.zig");
const network = @import("network.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
disable,
enable,
@@ -137,13 +135,13 @@ const ErrorReason = enum {
BlockedByResponse,
};
fn disable(cmd: *CDP.Command) !void {
fn disable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.fetchDisable();
return cmd.sendResult(null, .{});
}
fn enable(cmd: *CDP.Command) !void {
fn enable(cmd: anytype) !void {
const params = (try cmd.params(EnableParam)) orelse EnableParam{};
if (!arePatternsSupported(params.patterns)) {
log.warn(.not_implemented, "Fetch.enable", .{ .params = "pattern" });
@@ -182,7 +180,7 @@ fn arePatternsSupported(patterns: []RequestPattern) bool {
return true;
}
pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.RequestIntercept) !void {
pub fn requestIntercept(bc: anytype, intercept: *const Notification.RequestIntercept) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
@@ -217,7 +215,7 @@ pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.
intercept.wait_for_interception.* = true;
}
fn continueRequest(cmd: *CDP.Command) !void {
fn continueRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // INT-{d}"
@@ -277,7 +275,7 @@ const AuthChallengeResponse = enum {
ProvideCredentials,
};
fn continueWithAuth(cmd: *CDP.Command) !void {
fn continueWithAuth(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}"
@@ -320,7 +318,7 @@ fn continueWithAuth(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn fulfillRequest(cmd: *CDP.Command) !void {
fn fulfillRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
@@ -362,7 +360,7 @@ fn fulfillRequest(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn failRequest(cmd: *CDP.Command) !void {
fn failRequest(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
requestId: []const u8, // "INT-{d}"
@@ -384,7 +382,7 @@ fn failRequest(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
pub fn requestAuthRequired(bc: *CDP.BrowserContext, intercept: *const Notification.RequestAuthRequired) !void {
pub fn requestAuthRequired(bc: anytype, intercept: *const Notification.RequestAuthRequired) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
dispatchKeyEvent,
dispatchMouseEvent,
@@ -34,7 +33,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
fn dispatchKeyEvent(cmd: *CDP.Command) !void {
fn dispatchKeyEvent(cmd: anytype) !void {
const params = (try cmd.params(struct {
type: Type,
key: []const u8 = "",
@@ -75,7 +74,7 @@ fn dispatchKeyEvent(cmd: *CDP.Command) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
fn dispatchMouseEvent(cmd: *CDP.Command) !void {
fn dispatchMouseEvent(cmd: anytype) !void {
const params = (try cmd.params(struct {
x: f64,
y: f64,
@@ -105,7 +104,7 @@ fn dispatchMouseEvent(cmd: *CDP.Command) !void {
}
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-insertText
fn insertText(cmd: *CDP.Command) !void {
fn insertText(cmd: anytype) !void {
const params = (try cmd.params(struct {
text: []const u8, // The text to insert
})) orelse return error.InvalidParams;

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,

View File

@@ -18,18 +18,14 @@
const std = @import("std");
const lp = @import("lightpanda");
const CDP = @import("../CDP.zig");
const Node = @import("../Node.zig");
const DOMNode = @import("../../browser/webapi/Node.zig");
const markdown = lp.markdown;
const SemanticTree = lp.SemanticTree;
const interactive = lp.interactive;
const structured_data = lp.structured_data;
const Node = @import("../Node.zig");
const DOMNode = @import("../../browser/webapi/Node.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getMarkdown,
getSemanticTree,
@@ -55,7 +51,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
}
fn getSemanticTree(cmd: *CDP.Command) !void {
fn getSemanticTree(cmd: anytype) !void {
const Params = struct {
format: ?enum { text } = null,
prune: ?bool = null,
@@ -100,7 +96,7 @@ fn getSemanticTree(cmd: *CDP.Command) !void {
}, .{});
}
fn getMarkdown(cmd: *CDP.Command) !void {
fn getMarkdown(cmd: anytype) !void {
const Params = struct {
nodeId: ?Node.Id = null,
};
@@ -123,7 +119,7 @@ fn getMarkdown(cmd: *CDP.Command) !void {
}, .{});
}
fn getInteractiveElements(cmd: *CDP.Command) !void {
fn getInteractiveElements(cmd: anytype) !void {
const Params = struct {
nodeId: ?Node.Id = null,
};
@@ -145,7 +141,7 @@ fn getInteractiveElements(cmd: *CDP.Command) !void {
}, .{});
}
fn getStructuredData(cmd: *CDP.Command) !void {
fn getStructuredData(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.NoBrowserContext;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
@@ -160,7 +156,7 @@ fn getStructuredData(cmd: *CDP.Command) !void {
}, .{});
}
fn detectForms(cmd: *CDP.Command) !void {
fn detectForms(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.NoBrowserContext;
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
@@ -177,7 +173,7 @@ fn detectForms(cmd: *CDP.Command) !void {
}, .{});
}
fn clickNode(cmd: *CDP.Command) !void {
fn clickNode(cmd: anytype) !void {
const Params = struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -198,7 +194,7 @@ fn clickNode(cmd: *CDP.Command) !void {
return cmd.sendResult(.{}, .{});
}
fn fillNode(cmd: *CDP.Command) !void {
fn fillNode(cmd: anytype) !void {
const Params = struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -220,7 +216,7 @@ fn fillNode(cmd: *CDP.Command) !void {
return cmd.sendResult(.{}, .{});
}
fn scrollNode(cmd: *CDP.Command) !void {
fn scrollNode(cmd: anytype) !void {
const Params = struct {
nodeId: ?Node.Id = null,
backendNodeId: ?Node.Id = null,
@@ -248,7 +244,7 @@ fn scrollNode(cmd: *CDP.Command) !void {
return cmd.sendResult(.{}, .{});
}
fn waitForSelector(cmd: *CDP.Command) !void {
fn waitForSelector(cmd: anytype) !void {
const Params = struct {
selector: []const u8,
timeout: ?u32 = null,

View File

@@ -18,21 +18,18 @@
const std = @import("std");
const lp = @import("lightpanda");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const CdpStorage = @import("storage.zig");
const id = @import("../id.zig");
const URL = @import("../../browser/URL.zig");
const Transfer = @import("../../browser/HttpClient.zig").Transfer;
const Notification = @import("../../Notification.zig");
const Mime = @import("../../browser/Mime.zig");
const CdpStorage = @import("storage.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,
@@ -62,19 +59,19 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
}
fn enable(cmd: *CDP.Command) !void {
fn enable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.networkEnable();
return cmd.sendResult(null, .{});
}
fn disable(cmd: *CDP.Command) !void {
fn disable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.networkDisable();
return cmd.sendResult(null, .{});
}
fn setExtraHTTPHeaders(cmd: *CDP.Command) !void {
fn setExtraHTTPHeaders(cmd: anytype) !void {
const params = (try cmd.params(struct {
headers: std.json.ArrayHashMap([]const u8),
})) orelse return error.InvalidParams;
@@ -113,7 +110,7 @@ fn cookieMatches(cookie: *const Cookie, name: []const u8, domain: ?[]const u8, p
return true;
}
fn deleteCookies(cmd: *CDP.Command) !void {
fn deleteCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
name: []const u8,
url: ?[:0]const u8 = null,
@@ -147,14 +144,14 @@ fn deleteCookies(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn clearBrowserCookies(cmd: *CDP.Command) !void {
fn clearBrowserCookies(cmd: anytype) !void {
if (try cmd.params(struct {}) != null) return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.session.cookie_jar.clearRetainingCapacity();
return cmd.sendResult(null, .{});
}
fn setCookie(cmd: *CDP.Command) !void {
fn setCookie(cmd: anytype) !void {
const params = (try cmd.params(
CdpStorage.CdpCookie,
)) orelse return error.InvalidParams;
@@ -165,7 +162,7 @@ fn setCookie(cmd: *CDP.Command) !void {
try cmd.sendResult(.{ .success = true }, .{});
}
fn setCookies(cmd: *CDP.Command) !void {
fn setCookies(cmd: anytype) !void {
const params = (try cmd.params(struct {
cookies: []const CdpStorage.CdpCookie,
})) orelse return error.InvalidParams;
@@ -181,7 +178,7 @@ fn setCookies(cmd: *CDP.Command) !void {
const GetCookiesParam = struct {
urls: ?[]const [:0]const u8 = null,
};
fn getCookies(cmd: *CDP.Command) !void {
fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{};
@@ -204,7 +201,7 @@ fn getCookies(cmd: *CDP.Command) !void {
try cmd.sendResult(.{ .cookies = writer }, .{});
}
fn getResponseBody(cmd: *CDP.Command) !void {
fn getResponseBody(cmd: anytype) !void {
const params = (try cmd.params(struct {
requestId: []const u8, // "REQ-{d}"
})) orelse return error.InvalidParams;
@@ -230,7 +227,7 @@ fn getResponseBody(cmd: *CDP.Command) !void {
}, .{});
}
pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.RequestFail) !void {
pub fn httpRequestFail(bc: anytype, msg: *const Notification.RequestFail) !void {
// It's possible that the request failed because we aborted when the client
// sent Target.closeTarget. In that case, bc.session_id will be cleared
// already, and we can skip sending these messages to the client.
@@ -250,7 +247,7 @@ pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.Request
}, .{ .session_id = session_id });
}
pub fn httpRequestStart(bc: *CDP.BrowserContext, msg: *const Notification.RequestStart) !void {
pub fn httpRequestStart(bc: anytype, msg: *const Notification.RequestStart) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
@@ -279,7 +276,7 @@ pub fn httpRequestStart(bc: *CDP.BrowserContext, msg: *const Notification.Reques
}, .{ .session_id = session_id });
}
pub fn httpResponseHeaderDone(arena: Allocator, bc: *CDP.BrowserContext, msg: *const Notification.ResponseHeaderDone) !void {
pub fn httpResponseHeaderDone(arena: Allocator, bc: anytype, msg: *const Notification.ResponseHeaderDone) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
@@ -296,7 +293,7 @@ pub fn httpResponseHeaderDone(arena: Allocator, bc: *CDP.BrowserContext, msg: *c
}, .{ .session_id = session_id });
}
pub fn httpRequestDone(bc: *CDP.BrowserContext, msg: *const Notification.RequestDone) !void {
pub fn httpRequestDone(bc: anytype, msg: *const Notification.RequestDone) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;

View File

@@ -1,5 +1,4 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
@@ -23,8 +22,6 @@ const lp = @import("lightpanda");
const screenshot_png = @embedFile("screenshot.png");
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
const js = @import("../../browser/js/js.zig");
const URL = @import("../../browser/URL.zig");
@@ -34,13 +31,12 @@ const Notification = @import("../../Notification.zig");
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
getFrameTree,
setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument,
removeScriptToEvaluateOnNewDocument,
createIsolatedWorld,
navigate,
reload,
@@ -55,7 +51,6 @@ pub fn processMessage(cmd: *CDP.Command) !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),
@@ -81,7 +76,7 @@ const Frame = struct {
gatedAPIFeatures: [][]const u8 = &[0][]const u8{},
};
fn getFrameTree(cmd: *CDP.Command) !void {
fn getFrameTree(cmd: anytype) !void {
// Stagehand parses the response and error if we don't return a
// correct one for this call when browser context or target id are missing.
const startup = .{
@@ -111,7 +106,7 @@ fn getFrameTree(cmd: *CDP.Command) !void {
}, .{});
}
fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void {
fn setLifecycleEventsEnabled(cmd: anytype) !void {
const params = (try cmd.params(struct {
enabled: bool,
})) orelse return error.InvalidParams;
@@ -152,56 +147,23 @@ fn setLifecycleEventsEnabled(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn addScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !void {
const params = (try cmd.params(struct {
source: []const u8,
worldName: ?[]const u8 = null,
includeCommandLineAPI: bool = false,
runImmediately: bool = false,
})) orelse return error.InvalidParams;
// 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 bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.runImmediately) {
log.warn(.not_implemented, "addScriptOnNewDocument", .{ .param = "runImmediately" });
}
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 = id_str,
.identifier = "1",
}, .{});
}
fn removeScriptToEvaluateOnNewDocument(cmd: *CDP.Command) !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: *CDP.Command) !void {
fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const target_id = bc.target_id orelse return error.TargetNotLoaded;
@@ -238,7 +200,7 @@ fn close(cmd: *CDP.Command) !void {
bc.target_id = null;
}
fn createIsolatedWorld(cmd: *CDP.Command) !void {
fn createIsolatedWorld(cmd: anytype) !void {
const params = (try cmd.params(struct {
frameId: []const u8,
worldName: []const u8,
@@ -258,7 +220,7 @@ fn createIsolatedWorld(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .executionContextId = js_context.id }, .{});
}
fn navigate(cmd: *CDP.Command) !void {
fn navigate(cmd: anytype) !void {
const params = (try cmd.params(struct {
url: [:0]const u8,
// referrer: ?[]const u8 = null,
@@ -292,7 +254,7 @@ fn navigate(cmd: *CDP.Command) !void {
});
}
fn doReload(cmd: *CDP.Command) !void {
fn doReload(cmd: anytype) !void {
const params = try cmd.params(struct {
ignoreCache: ?bool = null,
scriptToEvaluateOnLoad: ?[]const u8 = null,
@@ -322,7 +284,7 @@ fn doReload(cmd: *CDP.Command) !void {
});
}
pub fn pageNavigate(bc: *CDP.BrowserContext, event: *const Notification.PageNavigate) !void {
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
@@ -374,7 +336,7 @@ pub fn pageNavigate(bc: *CDP.BrowserContext, event: *const Notification.PageNavi
}, .{ .session_id = session_id });
}
pub fn pageRemove(bc: *CDP.BrowserContext) !void {
pub fn pageRemove(bc: anytype) !void {
// Clear all remote object mappings to prevent stale objectIds from being used
// after the context is destroy
bc.inspector_session.inspector.resetContextGroup();
@@ -385,7 +347,7 @@ pub fn pageRemove(bc: *CDP.BrowserContext) !void {
}
}
pub fn pageCreated(bc: *CDP.BrowserContext, page: *Page) !void {
pub fn pageCreated(bc: anytype, page: *Page) !void {
_ = bc.cdp.page_arena.reset(.{ .retain_with_limit = 1024 * 512 });
for (bc.isolated_worlds.items) |isolated_world| {
@@ -397,7 +359,7 @@ pub fn pageCreated(bc: *CDP.BrowserContext, page: *Page) !void {
bc.captured_responses = .empty;
}
pub fn pageFrameCreated(bc: *CDP.BrowserContext, event: *const Notification.PageFrameCreated) !void {
pub fn pageFrameCreated(bc: anytype, event: *const Notification.PageFrameCreated) !void {
const session_id = bc.session_id orelse return;
const cdp = bc.cdp;
@@ -418,12 +380,11 @@ pub fn pageFrameCreated(bc: *CDP.BrowserContext, event: *const Notification.Page
}
}
pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const Notification.PageNavigated) !void {
pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.PageNavigated) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
const frame_id = &id.toFrameId(event.frame_id);
const loader_id = &id.toLoaderId(event.req_id);
@@ -475,9 +436,9 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
// When we actually recreated the context we should have the inspector send
// this event, see: resetContextGroup Sending this event will tell the
// client that the context ids they had are invalid and the context shouls
// be dropped The client will expect us to send new contextCreated events,
// this event, see: resetContextGroup. Sending this event will tell the
// client that the context ids they had are invalid and the context should
// be dropped. The client will expect us to send new contextCreated events,
// such that the client has new id's for the active contexts.
// Only send executionContextsCleared for main frame navigations. For child
// frames (iframes), clearing all contexts would destroy the main frame's
@@ -487,6 +448,18 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
}
// frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
.frame = Frame{
.id = frame_id,
.url = event.url,
.loaderId = loader_id,
.securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type,
},
}, .{ .session_id = session_id });
{
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id });
@@ -520,27 +493,6 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
);
}
// 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| {
var try_catch: lp.js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
ls.local.eval(script.source, null) catch |err| {
const caught = try_catch.caughtOrError(arena, err);
log.warn(.cdp, "script on new doc", .{ .caught = caught });
};
}
}
// frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
@@ -557,18 +509,22 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
// chromedp client expects to receive the events is this order.
// see https://github.com/chromedp/chromedp/issues/1558
try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id });
}
pub fn pageDOMContentLoaded(bc: anytype, event: *const Notification.PageDOMContentLoaded) !void {
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// domContentEventFired event
// TODO: partially hard coded
try cdp.sendEvent(
"Page.domContentEventFired",
.{ .timestamp = timestamp },
.{ .session_id = session_id },
);
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (bc.page_life_cycle_events) {
const frame_id = &id.toFrameId(event.frame_id);
const loader_id = &id.toLoaderId(event.req_id);
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.timestamp = timestamp,
.name = "DOMContentLoaded",
@@ -576,16 +532,23 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
.loaderId = loader_id,
}, .{ .session_id = session_id });
}
}
pub fn pageLoaded(bc: anytype, event: *const Notification.PageLoaded) !void {
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
var cdp = bc.cdp;
const frame_id = &id.toFrameId(event.frame_id);
// loadEventFired event
try cdp.sendEvent(
"Page.loadEventFired",
.{ .timestamp = timestamp },
.{ .session_id = session_id },
);
// lifecycle DOMContentLoaded event
if (bc.page_life_cycle_events) {
const loader_id = &id.toLoaderId(event.req_id);
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.timestamp = timestamp,
.name = "load",
@@ -594,21 +557,20 @@ pub fn pageNavigated(arena: Allocator, bc: *CDP.BrowserContext, event: *const No
}, .{ .session_id = session_id });
}
// frameStoppedLoading
return cdp.sendEvent("Page.frameStoppedLoading", .{
.frameId = frame_id,
}, .{ .session_id = session_id });
}
pub fn pageNetworkIdle(bc: *CDP.BrowserContext, event: *const Notification.PageNetworkIdle) !void {
pub fn pageNetworkIdle(bc: anytype, event: *const Notification.PageNetworkIdle) !void {
return sendPageLifecycle(bc, "networkIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id));
}
pub fn pageNetworkAlmostIdle(bc: *CDP.BrowserContext, event: *const Notification.PageNetworkAlmostIdle) !void {
pub fn pageNetworkAlmostIdle(bc: anytype, event: *const Notification.PageNetworkAlmostIdle) !void {
return sendPageLifecycle(bc, "networkAlmostIdle", event.timestamp, &id.toFrameId(event.frame_id), &id.toLoaderId(event.req_id));
}
fn sendPageLifecycle(bc: *CDP.BrowserContext, name: []const u8, timestamp: u64, frame_id: []const u8, loader_id: []const u8) !void {
fn sendPageLifecycle(bc: anytype, name: []const u8, timestamp: u64, frame_id: []const u8, loader_id: []const u8) !void {
// detachTarget could be called, in which case, we still have a page doing
// things, but no session.
const session_id = bc.session_id orelse return;
@@ -643,7 +605,7 @@ fn base64Encode(comptime input: []const u8) [std.base64.standard.Encoder.calcSiz
return buf;
}
fn captureScreenshot(cmd: *CDP.Command) !void {
fn captureScreenshot(cmd: anytype) !void {
const Params = struct {
format: ?[]const u8 = "png",
quality: ?u8 = null,
@@ -679,7 +641,7 @@ fn captureScreenshot(cmd: *CDP.Command) !void {
}, .{});
}
fn getLayoutMetrics(cmd: *CDP.Command) !void {
fn getLayoutMetrics(cmd: anytype) !void {
const width = 1920;
const height = 1080;
@@ -899,55 +861,3 @@ 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();
var bc = 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 });
}
{
try ctx.processMessage(.{ .id = 34, .method = "Page.reload" });
// wait for this event, which is sent after we've run the registered scripts
try ctx.expectSentEvent("Page.frameNavigated", .{
.frame = .{ .loaderId = "LID-0000000002" },
}, .{});
const page = bc.session.currentPage() orelse unreachable;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const test_val = try ls.local.exec("window.__test2", null);
try testing.expectEqual(2, try test_val.toI32());
}
}

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,

View File

@@ -19,9 +19,7 @@
const std = @import("std");
const builtin = @import("builtin");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
runIfWaitingForDebugger,
@@ -38,7 +36,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
}
fn sendInspector(cmd: *CDP.Command, action: anytype) !void {
fn sendInspector(cmd: anytype, action: anytype) !void {
// save script in file at debug mode
if (builtin.mode == .Debug) {
try logInspector(cmd, action);
@@ -50,7 +48,7 @@ fn sendInspector(cmd: *CDP.Command, action: anytype) !void {
bc.callInspector(cmd.input.json);
}
fn logInspector(cmd: *CDP.Command, action: anytype) !void {
fn logInspector(cmd: anytype, action: anytype) !void {
const script = switch (action) {
.evaluate => blk: {
const params = (try cmd.params(struct {

View File

@@ -17,9 +17,8 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const CDP = @import("../CDP.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
enable,
disable,
@@ -33,7 +32,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
}
fn setIgnoreCertificateErrors(cmd: *CDP.Command) !void {
fn setIgnoreCertificateErrors(cmd: anytype) !void {
const params = (try cmd.params(struct {
ignore: bool,
})) orelse return error.InvalidParams;

View File

@@ -18,16 +18,13 @@
const std = @import("std");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig");
const Cookie = @import("../../browser/webapi/storage/storage.zig").Cookie;
const CookieJar = Cookie.Jar;
pub const PreparedUri = Cookie.PreparedUri;
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
clearCookies,
setCookies,
@@ -43,7 +40,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
const BrowserContextParam = struct { browserContextId: ?[]const u8 = null };
fn clearCookies(cmd: *CDP.Command) !void {
fn clearCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
@@ -58,7 +55,7 @@ fn clearCookies(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
fn getCookies(cmd: *CDP.Command) !void {
fn getCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(BrowserContextParam)) orelse BrowserContextParam{};
@@ -72,7 +69,7 @@ fn getCookies(cmd: *CDP.Command) !void {
try cmd.sendResult(.{ .cookies = writer }, .{});
}
fn setCookies(cmd: *CDP.Command) !void {
fn setCookies(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const params = (try cmd.params(struct {
cookies: []const CdpCookie,

View File

@@ -20,13 +20,11 @@ const std = @import("std");
const lp = @import("lightpanda");
const id = @import("../id.zig");
const CDP = @import("../CDP.zig");
const log = @import("../../log.zig");
const URL = @import("../../browser/URL.zig");
const js = @import("../../browser/js/js.zig");
pub fn processMessage(cmd: *CDP.Command) !void {
pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum {
getTargets,
attachToTarget,
@@ -62,7 +60,7 @@ pub fn processMessage(cmd: *CDP.Command) !void {
}
}
fn getTargets(cmd: *CDP.Command) !void {
fn getTargets(cmd: anytype) !void {
// If no context available, return an empty array.
const bc = cmd.browser_context orelse {
return cmd.sendResult(.{
@@ -88,7 +86,7 @@ fn getTargets(cmd: *CDP.Command) !void {
}, .{ .include_session_id = false });
}
fn getBrowserContexts(cmd: *CDP.Command) !void {
fn getBrowserContexts(cmd: anytype) !void {
var browser_context_ids: []const []const u8 = undefined;
if (cmd.browser_context) |bc| {
browser_context_ids = &.{bc.id};
@@ -101,7 +99,7 @@ fn getBrowserContexts(cmd: *CDP.Command) !void {
}, .{ .include_session_id = false });
}
fn createBrowserContext(cmd: *CDP.Command) !void {
fn createBrowserContext(cmd: anytype) !void {
const params = try cmd.params(struct {
disposeOnDetach: bool = false,
proxyServer: ?[:0]const u8 = null,
@@ -132,7 +130,7 @@ fn createBrowserContext(cmd: *CDP.Command) !void {
}, .{});
}
fn disposeBrowserContext(cmd: *CDP.Command) !void {
fn disposeBrowserContext(cmd: anytype) !void {
const params = (try cmd.params(struct {
browserContextId: []const u8,
})) orelse return error.InvalidParams;
@@ -143,7 +141,7 @@ fn disposeBrowserContext(cmd: *CDP.Command) !void {
try cmd.sendResult(null, .{});
}
fn createTarget(cmd: *CDP.Command) !void {
fn createTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
url: [:0]const u8 = "about:blank",
// width: ?u64 = null,
@@ -232,7 +230,7 @@ fn createTarget(cmd: *CDP.Command) !void {
}, .{});
}
fn attachToTarget(cmd: *CDP.Command) !void {
fn attachToTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
targetId: []const u8,
flatten: bool = true,
@@ -249,7 +247,7 @@ fn attachToTarget(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});
}
fn attachToBrowserTarget(cmd: *CDP.Command) !void {
fn attachToBrowserTarget(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();
@@ -271,7 +269,7 @@ fn attachToBrowserTarget(cmd: *CDP.Command) !void {
return cmd.sendResult(.{ .sessionId = bc.session_id }, .{});
}
fn closeTarget(cmd: *CDP.Command) !void {
fn closeTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
targetId: []const u8,
})) orelse return error.InvalidParams;
@@ -312,7 +310,7 @@ fn closeTarget(cmd: *CDP.Command) !void {
bc.target_id = null;
}
fn getTargetInfo(cmd: *CDP.Command) !void {
fn getTargetInfo(cmd: anytype) !void {
const Params = struct {
targetId: ?[]const u8 = null,
};
@@ -349,7 +347,7 @@ fn getTargetInfo(cmd: *CDP.Command) !void {
}, .{ .include_session_id = false });
}
fn sendMessageToTarget(cmd: *CDP.Command) !void {
fn sendMessageToTarget(cmd: anytype) !void {
const params = (try cmd.params(struct {
message: []const u8,
sessionId: []const u8,
@@ -367,19 +365,32 @@ fn sendMessageToTarget(cmd: *CDP.Command) !void {
return error.UnknownSessionId;
}
var aw = std.Io.Writer.Allocating.init(cmd.arena);
cmd.cdp.dispatch(cmd.arena, .{ .capture = &aw.writer }, params.message) catch |err| {
const Capture = struct {
aw: std.Io.Writer.Allocating,
pub fn sendJSON(self: *@This(), message: anytype) !void {
return std.json.Stringify.value(message, .{
.emit_null_optional_fields = false,
}, &self.aw.writer);
}
};
var capture = Capture{
.aw = .init(cmd.arena),
};
cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| {
log.err(.cdp, "internal dispatch error", .{ .err = err, .id = cmd.input.id, .message = params.message });
return err;
};
try cmd.sendEvent("Target.receivedMessageFromTarget", .{
.message = aw.written(),
.message = capture.aw.written(),
.sessionId = params.sessionId,
}, .{});
}
fn detachFromTarget(cmd: *CDP.Command) !void {
fn detachFromTarget(cmd: anytype) !void {
if (cmd.browser_context) |bc| {
if (bc.session_id) |session_id| {
try cmd.sendEvent("Target.detachedFromTarget", .{
@@ -393,11 +404,11 @@ fn detachFromTarget(cmd: *CDP.Command) !void {
}
// TODO: noop method
fn setDiscoverTargets(cmd: *CDP.Command) !void {
fn setDiscoverTargets(cmd: anytype) !void {
return cmd.sendResult(null, .{});
}
fn setAutoAttach(cmd: *CDP.Command) !void {
fn setAutoAttach(cmd: anytype) !void {
const params = (try cmd.params(struct {
autoAttach: bool,
waitForDebuggerOnStart: bool,
@@ -457,7 +468,7 @@ fn setAutoAttach(cmd: *CDP.Command) !void {
try cmd.sendResult(null, .{});
}
fn doAttachtoTarget(cmd: *CDP.Command, target_id: []const u8) !void {
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
const bc = cmd.browser_context.?;
const session_id = bc.session_id orelse cmd.cdp.session_id_gen.next();

View File

@@ -64,7 +64,7 @@ const TestContext = struct {
session_id: ?[]const u8 = null,
url: ?[:0]const u8 = null,
};
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext {
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*CDP.BrowserContext(CDP) {
var c = self.cdp();
if (c.browser_context) |bc| {
_ = c.disposeBrowserContext(bc.id);
@@ -168,26 +168,13 @@ const TestContext = struct {
index: ?usize = null,
};
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
const expected_json = blk: {
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
// We can't record that in an ArrayList(???), so we serialize it to JSON.
// Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
break :blk try std.json.parseFromSliceLeaky(json.Value, base.arena_allocator, serialized, .{});
};
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
for (0..5) |_| {
for (self.received.items, 0..) |received, i| {
if (try base.isEqualJson(expected_json, received) == false) {
if (try compareExpectedToSent(serialized, received) == false) {
continue;
}
@@ -200,15 +187,6 @@ const TestContext = struct {
}
return;
}
if (self.cdp_) |*cdp__| {
if (cdp__.browser_context) |*bc| {
if (bc.session.page != null) {
var runner = try bc.session.runner(.{});
_ = try runner.tick(.{ .ms = 1000 });
}
}
}
std.Thread.sleep(5 * std.time.ns_per_ms);
try self.read();
}
@@ -321,3 +299,17 @@ pub fn context() !TestContext {
.socket = pair[0],
};
}
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
// We can't record that in an ArrayList(???), so we serialize it to JSON.
// Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
defer expected_value.deinit();
return base.isEqualJson(expected_value.value, actual);
}