mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 15:13:28 +00:00
Make CDP server more authoritative with respect to IDs
The TL;DR is that this commit enforces the use of correct IDs, introduces a BrowserContext, and adds some CDP tests. These are the ids we need to be aware of when talking about CDP: - id - browserContextId - targetId - sessionId - loaderId - frameId The `id` is the only one that _should_ originate from the driver. It's attached to most messages and it's how we maintain a request -> response flow: when the server responds to a specific message, it echo's back the id from the requested message. (As opposed to out-of-band events sent from the server which won't have an `id`). When I say "id" from this point forward, I mean every id except for this req->res id. Every other id is created by the browser. Prior to this commit, we didn't really check incoming ids from the driver. If the driver said "attachToTarget" and included a targetId, we just assumed that this was the current targetId. This was aided by the fact that we only used hard-coded IDS. If _we_ only "create" a frameId of "FRAME-1", then it's tempting to think the driver will only ever send a frameId of "FRAME-1". The issue with this approach is that _if_ the browser and driver fall out of sync and there's only ever 1 browserContextId, 1 sessionId and 1 frameId, it's not impossible to imagine cases where we behave on the thing. Imagine this flow: - Driver asks for a new BrowserContext - Browser says OK, your browserContextId is 1 - Driver, for whatever reason, says close browserContextId 2 - Browser says, OK, but it doesn't check the id and just closes the only BrowserContext it knows about (which is 1) By both re-using the same hard-coded ids, and not verifying that the ids sent from the client correspond to the correct ids, any issues are going to be hard to debug. Currently LOADER_ID and FRAEM_ID are still hard-coded. Baby steps.
This commit is contained in:
committed by
Pierre Tachoire
parent
ccacac0597
commit
a3e2b5246e
@@ -98,12 +98,6 @@ pub const Browser = struct {
|
||||
self.session = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn currentPage(self: *Browser) ?*Page {
|
||||
if (self.session.page == null) return null;
|
||||
|
||||
return &self.session.page.?;
|
||||
}
|
||||
};
|
||||
|
||||
// Session is like a browser's tab.
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
setDownloadBehavior,
|
||||
getWindowForTarget,
|
||||
setWindowBounds,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.getVersion => return getVersion(cmd),
|
||||
@@ -88,7 +88,6 @@ test "cdp.browser: getVersion" {
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 32,
|
||||
.sessionID = "leto",
|
||||
.method = "Browser.getVersion",
|
||||
});
|
||||
|
||||
@@ -99,7 +98,7 @@ test "cdp.browser: getVersion" {
|
||||
.revision = REVISION,
|
||||
.userAgent = USER_AGENT,
|
||||
.jsVersion = JS_VERSION,
|
||||
}, .{ .id = 32, .index = 0 });
|
||||
}, .{ .id = 32, .index = 0, .session_id = null });
|
||||
}
|
||||
|
||||
test "cdp.browser: getWindowForTarget" {
|
||||
@@ -108,7 +107,6 @@ test "cdp.browser: getWindowForTarget" {
|
||||
|
||||
try ctx.processMessage(.{
|
||||
.id = 33,
|
||||
.sessionId = "leto",
|
||||
.method = "Browser.getWindowForTarget",
|
||||
});
|
||||
|
||||
@@ -116,5 +114,5 @@ test "cdp.browser: getWindowForTarget" {
|
||||
try ctx.expectSentResult(.{
|
||||
.windowId = DEV_TOOLS_WINDOW_ID,
|
||||
.bounds = .{ .windowState = "normal" },
|
||||
}, .{ .id = 33, .index = 0, .session_id = "leto" });
|
||||
}, .{ .id = 33, .index = 0, .session_id = null });
|
||||
}
|
||||
|
||||
458
src/cdp/cdp.zig
458
src/cdp/cdp.zig
@@ -20,115 +20,78 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const json = std.json;
|
||||
|
||||
const dom = @import("dom.zig");
|
||||
const Loop = @import("jsruntime").Loop;
|
||||
// const Client = @import("../server.zig").Client;
|
||||
const asUint = @import("../str/parser.zig").asUint;
|
||||
const Incrementing = @import("../id.zig").Incrementing;
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
pub const URL_BASE = "chrome://newtab/";
|
||||
pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
|
||||
pub const FRAME_ID = "FRAMEIDD8AED408A0467AC93100BCDBE";
|
||||
pub const BROWSER_SESSION_ID = @tagName(SessionID.BROWSERSESSIONID597D9875C664CAC0);
|
||||
pub const CONTEXT_SESSION_ID = @tagName(SessionID.CONTEXTSESSIONID0497A05C95417CF4);
|
||||
|
||||
pub const TimestampEvent = struct {
|
||||
timestamp: f64,
|
||||
};
|
||||
|
||||
pub const CDP = CDPT(struct {
|
||||
const Client = @import("../server.zig").Client;
|
||||
const Loop = *@import("jsruntime").Loop;
|
||||
const Client = *@import("../server.zig").Client;
|
||||
const Browser = @import("../browser/browser.zig").Browser;
|
||||
const Session = @import("../browser/browser.zig").Session;
|
||||
});
|
||||
|
||||
const SessionIdGen = Incrementing(u32, "SID");
|
||||
const TargetIdGen = Incrementing(u32, "TID");
|
||||
const BrowserContextIdGen = Incrementing(u32, "BID");
|
||||
|
||||
// Generic so that we can inject mocks into it.
|
||||
pub fn CDPT(comptime TypeProvider: type) type {
|
||||
return struct {
|
||||
loop: TypeProvider.Loop,
|
||||
|
||||
// Used for sending message to the client and closing on error
|
||||
client: *TypeProvider.Client,
|
||||
|
||||
// The active browser
|
||||
browser: Browser,
|
||||
|
||||
// The active browser session
|
||||
session: ?*Session,
|
||||
client: TypeProvider.Client,
|
||||
|
||||
allocator: Allocator,
|
||||
|
||||
// The active browser
|
||||
browser: ?Browser = null,
|
||||
|
||||
target_id_gen: TargetIdGen = .{},
|
||||
session_id_gen: SessionIdGen = .{},
|
||||
browser_context_id_gen: BrowserContextIdGen = .{},
|
||||
|
||||
browser_context: ?BrowserContext(Self),
|
||||
|
||||
// Re-used arena for processing a message. We're assuming that we're getting
|
||||
// 1 message at a time.
|
||||
message_arena: std.heap.ArenaAllocator,
|
||||
|
||||
// State
|
||||
url: []const u8,
|
||||
frame_id: []const u8,
|
||||
loader_id: []const u8,
|
||||
session_id: SessionID,
|
||||
context_id: ?[]const u8,
|
||||
execution_context_id: u32,
|
||||
security_origin: []const u8,
|
||||
page_life_cycle_events: bool,
|
||||
secure_context_type: []const u8,
|
||||
node_list: dom.NodeList,
|
||||
node_search_list: dom.NodeSearchList,
|
||||
|
||||
const Self = @This();
|
||||
pub const Browser = TypeProvider.Browser;
|
||||
pub const Session = TypeProvider.Session;
|
||||
|
||||
pub fn init(allocator: Allocator, client: *TypeProvider.Client, loop: anytype) Self {
|
||||
pub fn init(allocator: Allocator, client: TypeProvider.Client, loop: TypeProvider.Loop) Self {
|
||||
return .{
|
||||
.loop = loop,
|
||||
.client = client,
|
||||
.browser = Browser.init(allocator, loop),
|
||||
.session = null,
|
||||
.allocator = allocator,
|
||||
.url = URL_BASE,
|
||||
.execution_context_id = 0,
|
||||
.context_id = null,
|
||||
.frame_id = FRAME_ID,
|
||||
.session_id = .CONTEXTSESSIONID0497A05C95417CF4,
|
||||
.security_origin = URL_BASE,
|
||||
.secure_context_type = "Secure", // TODO = enum
|
||||
.loader_id = LOADER_ID,
|
||||
.browser_context = null,
|
||||
.message_arena = std.heap.ArenaAllocator.init(allocator),
|
||||
.page_life_cycle_events = false, // TODO; Target based value
|
||||
.node_list = dom.NodeList.init(allocator),
|
||||
.node_search_list = dom.NodeSearchList.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.node_list.deinit();
|
||||
for (self.node_search_list.items) |*s| {
|
||||
s.deinit();
|
||||
if (self.browser_context) |*bc| {
|
||||
bc.deinit();
|
||||
}
|
||||
self.node_search_list.deinit();
|
||||
|
||||
self.browser.deinit();
|
||||
self.message_arena.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.node_list.reset();
|
||||
|
||||
// deinit all node searches.
|
||||
for (self.node_search_list.items) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
self.node_search_list.clearAndFree();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Self) !void {
|
||||
self.session = try self.browser.newSession(self);
|
||||
}
|
||||
|
||||
pub fn handleMessage(self: *Self, msg: []const u8) bool {
|
||||
self.processMessage(msg) catch |err| {
|
||||
log.err("failed to process message: {}\n{s}", .{ err, msg });
|
||||
return false;
|
||||
};
|
||||
// if there's an error, it's already been logged
|
||||
self.processMessage(msg) catch return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,83 +103,236 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
|
||||
// Called from above, in processMessage which handles client messages
|
||||
// but can also be called internally. For example, Target.sendMessageToTarget
|
||||
// calls back into dispatch to capture the response
|
||||
// calls back into dispatch to capture the response.
|
||||
pub fn dispatch(self: *Self, arena: Allocator, sender: anytype, str: []const u8) !void {
|
||||
const input = json.parseFromSliceLeaky(InputMessage, arena, str, .{
|
||||
.ignore_unknown_fields = true,
|
||||
}) catch return error.InvalidJSON;
|
||||
|
||||
const domain, const action = blk: {
|
||||
const method = input.method;
|
||||
var command = Command(Self, @TypeOf(sender)){
|
||||
.input = .{
|
||||
.json = str,
|
||||
.id = input.id,
|
||||
.action = "",
|
||||
.params = input.params,
|
||||
.session_id = input.sessionId,
|
||||
},
|
||||
.cdp = self,
|
||||
.arena = arena,
|
||||
.sender = sender,
|
||||
.browser_context = if (self.browser_context) |*bc| bc else null,
|
||||
};
|
||||
|
||||
// See dispatchStartupCommand for more info on this.
|
||||
var is_startup = false;
|
||||
if (input.sessionId) |input_session_id| {
|
||||
if (std.mem.eql(u8, input_session_id, "STARTUP")) {
|
||||
is_startup = true;
|
||||
} else if (self.isValidSessionId(input_session_id) == false) {
|
||||
return command.sendError(-32001, "Unknown sessionId");
|
||||
}
|
||||
}
|
||||
|
||||
if (is_startup) {
|
||||
dispatchStartupCommand(&command) catch |err| {
|
||||
command.sendError(-31999, @errorName(err)) catch {};
|
||||
return err;
|
||||
};
|
||||
} else {
|
||||
dispatchCommand(&command, input.method) catch |err| {
|
||||
command.sendError(-31998, @errorName(err)) catch {};
|
||||
return err;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// A CDP session isn't 100% fully driven by the driver. There's are
|
||||
// independent actions that the browser is expected to take. For example
|
||||
// Puppeteer expects the browser to startup a tab and thus have existing
|
||||
// targets.
|
||||
// To this end, we create a [very] dummy BrowserContext, Target and
|
||||
// Session. There isn't actually a BrowserContext, just a special id.
|
||||
// When messages are received with the "STARTUP" sessionId, we do
|
||||
// "special" handling - the bare minimum we need to do until the driver
|
||||
// switches to a real BrowserContext.
|
||||
// (I can imagine this logic will become driver-specific)
|
||||
fn dispatchStartupCommand(command: anytype) !void {
|
||||
return command.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn dispatchCommand(command: anytype, method: []const u8) !void {
|
||||
const domain = blk: {
|
||||
const i = std.mem.indexOfScalarPos(u8, method, 0, '.') orelse {
|
||||
return error.InvalidMethod;
|
||||
};
|
||||
break :blk .{ method[0..i], method[i + 1 ..] };
|
||||
};
|
||||
|
||||
var command = Command(Self, @TypeOf(sender)){
|
||||
.json = str,
|
||||
.cdp = self,
|
||||
.id = input.id,
|
||||
.arena = arena,
|
||||
.action = action,
|
||||
._params = input.params,
|
||||
.session_id = input.sessionId,
|
||||
.sender = sender,
|
||||
.session = self.session orelse blk: {
|
||||
try self.newSession();
|
||||
break :blk self.session.?;
|
||||
},
|
||||
command.input.action = method[i + 1 ..];
|
||||
break :blk method[0..i];
|
||||
};
|
||||
|
||||
switch (domain.len) {
|
||||
3 => switch (@as(u24, @bitCast(domain[0..3].*))) {
|
||||
asUint("DOM") => return @import("dom.zig").processMessage(&command),
|
||||
asUint("Log") => return @import("log.zig").processMessage(&command),
|
||||
asUint("CSS") => return @import("css.zig").processMessage(&command),
|
||||
asUint("DOM") => return @import("dom.zig").processMessage(command),
|
||||
asUint("Log") => return @import("log.zig").processMessage(command),
|
||||
asUint("CSS") => return @import("css.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
4 => switch (@as(u32, @bitCast(domain[0..4].*))) {
|
||||
asUint("Page") => return @import("page.zig").processMessage(&command),
|
||||
asUint("Page") => return @import("page.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
5 => switch (@as(u40, @bitCast(domain[0..5].*))) {
|
||||
asUint("Fetch") => return @import("fetch.zig").processMessage(&command),
|
||||
asUint("Fetch") => return @import("fetch.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
6 => switch (@as(u48, @bitCast(domain[0..6].*))) {
|
||||
asUint("Target") => return @import("target.zig").processMessage(&command),
|
||||
asUint("Target") => return @import("target.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
7 => switch (@as(u56, @bitCast(domain[0..7].*))) {
|
||||
asUint("Browser") => return @import("browser.zig").processMessage(&command),
|
||||
asUint("Runtime") => return @import("runtime.zig").processMessage(&command),
|
||||
asUint("Network") => return @import("network.zig").processMessage(&command),
|
||||
asUint("Browser") => return @import("browser.zig").processMessage(command),
|
||||
asUint("Runtime") => return @import("runtime.zig").processMessage(command),
|
||||
asUint("Network") => return @import("network.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
8 => switch (@as(u64, @bitCast(domain[0..8].*))) {
|
||||
asUint("Security") => return @import("security.zig").processMessage(&command),
|
||||
asUint("Security") => return @import("security.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
9 => switch (@as(u72, @bitCast(domain[0..9].*))) {
|
||||
asUint("Emulation") => return @import("emulation.zig").processMessage(&command),
|
||||
asUint("Inspector") => return @import("inspector.zig").processMessage(&command),
|
||||
asUint("Emulation") => return @import("emulation.zig").processMessage(command),
|
||||
asUint("Inspector") => return @import("inspector.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
11 => switch (@as(u88, @bitCast(domain[0..11].*))) {
|
||||
asUint("Performance") => return @import("performance.zig").processMessage(&command),
|
||||
asUint("Performance") => return @import("performance.zig").processMessage(command),
|
||||
else => {},
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
|
||||
return error.UnknownDomain;
|
||||
}
|
||||
|
||||
fn isValidSessionId(self: *const Self, input_session_id: []const u8) bool {
|
||||
const browser_context = &(self.browser_context orelse return false);
|
||||
const session_id = browser_context.session_id orelse return false;
|
||||
return std.mem.eql(u8, session_id, input_session_id);
|
||||
}
|
||||
|
||||
pub fn createBrowserContext(self: *Self) ![]const u8 {
|
||||
if (self.browser_context != null) {
|
||||
return error.AlreadyExists;
|
||||
}
|
||||
const browser_context_id = self.browser_context_id_gen.next();
|
||||
|
||||
// is this safe?
|
||||
self.browser_context = undefined;
|
||||
errdefer self.browser_context = null;
|
||||
try BrowserContext(Self).init(&self.browser_context.?, browser_context_id, self);
|
||||
|
||||
return browser_context_id;
|
||||
}
|
||||
|
||||
pub fn disposeBrowserContext(self: *Self, browser_context_id: []const u8) bool {
|
||||
const bc = &(self.browser_context orelse return false);
|
||||
if (std.mem.eql(u8, bc.id, browser_context_id) == false) {
|
||||
return false;
|
||||
}
|
||||
bc.deinit();
|
||||
self.browser_context = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
fn sendJSON(self: *Self, message: anytype) !void {
|
||||
return self.client.sendJSON(message, .{
|
||||
.emit_null_optional_fields = false,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn BrowserContext(comptime CDP_T: type) type {
|
||||
const dom = @import("dom.zig");
|
||||
|
||||
return struct {
|
||||
id: []const u8,
|
||||
cdp: *CDP_T,
|
||||
|
||||
browser: CDP_T.Browser,
|
||||
// Represents the browser session. There is no equivalent in CDP. For
|
||||
// all intents and purpose, from CDP's point of view our Browser and
|
||||
// our Session more or less maps to a BrowserContext. THIS HAS ZERO
|
||||
// RELATION TO SESSION_ID
|
||||
session: *CDP_T.Session,
|
||||
|
||||
// Maps to our Page. (There are other types of targets, but we only
|
||||
// deal with "pages" for now). Since we only allow 1 open page at a
|
||||
// time, we only have 1 target_id.
|
||||
target_id: ?[]const u8,
|
||||
|
||||
// The CDP session_id. After the target/page is created, the client
|
||||
// "attaches" to it (either explicitly or automatically). We return a
|
||||
// "sessionId" which identifies this link. `sessionId` is the how
|
||||
// the CDP client informs us what it's trying to manipulate. Because we
|
||||
// only support 1 BrowserContext at a time, and 1 page at a time, this
|
||||
// is all pretty straightforward, but it still needs to be enforced, i.e.
|
||||
// if we get a request with a sessionId that doesn't match the current one
|
||||
// we should reject it.
|
||||
session_id: ?[]const u8,
|
||||
|
||||
// State
|
||||
url: []const u8,
|
||||
frame_id: []const u8,
|
||||
loader_id: []const u8,
|
||||
security_origin: []const u8,
|
||||
page_life_cycle_events: bool,
|
||||
secure_context_type: []const u8,
|
||||
node_list: dom.NodeList,
|
||||
node_search_list: dom.NodeSearchList,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
fn init(self: *Self, id: []const u8, cdp: *CDP_T) !void {
|
||||
self.* = .{
|
||||
.id = id,
|
||||
.cdp = cdp,
|
||||
.browser = undefined,
|
||||
.session = undefined,
|
||||
.target_id = null,
|
||||
.session_id = null,
|
||||
.url = URL_BASE,
|
||||
.frame_id = FRAME_ID,
|
||||
.security_origin = URL_BASE,
|
||||
.secure_context_type = "Secure", // TODO = enum
|
||||
.loader_id = LOADER_ID,
|
||||
.page_life_cycle_events = false, // TODO; Target based value
|
||||
.node_list = dom.NodeList.init(cdp.allocator),
|
||||
.node_search_list = dom.NodeSearchList.init(cdp.allocator),
|
||||
};
|
||||
|
||||
self.browser = CDP_T.Browser.init(cdp.allocator, cdp.loop);
|
||||
errdefer self.browser.deinit();
|
||||
self.session = try self.browser.newSession(self);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
self.node_list.deinit();
|
||||
for (self.node_search_list.items) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
self.node_search_list.deinit();
|
||||
self.browser.deinit();
|
||||
}
|
||||
|
||||
pub fn reset(self: *Self) void {
|
||||
self.node_list.reset();
|
||||
|
||||
// deinit all node searches.
|
||||
for (self.node_search_list.items) |*s| {
|
||||
s.deinit();
|
||||
}
|
||||
self.node_search_list.clearAndFree();
|
||||
}
|
||||
|
||||
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
|
||||
if (std.log.defaultLogEnabled(.debug)) {
|
||||
@@ -252,19 +368,24 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
};
|
||||
}
|
||||
|
||||
// This is hacky * 2. First, we have the JSON payload by gluing our
|
||||
// This is hacky x 2. First, we create the JSON payload by gluing our
|
||||
// session_id onto it. Second, we're much more client/websocket aware than
|
||||
// we should be.
|
||||
fn sendInspectorMessage(self: *Self, msg: []const u8) !void {
|
||||
var arena = std.heap.ArenaAllocator.init(self.allocator);
|
||||
const session_id = self.session_id orelse {
|
||||
// We no longer have an active session. What should we do
|
||||
// in this case?
|
||||
return;
|
||||
};
|
||||
|
||||
const cdp = self.cdp;
|
||||
var arena = std.heap.ArenaAllocator.init(cdp.allocator);
|
||||
errdefer arena.deinit();
|
||||
|
||||
const field = ",\"sessionId\":\"";
|
||||
const session_id = @tagName(self.session_id);
|
||||
|
||||
// + 1 for the closing quote after the session id
|
||||
// + 10 for the max websocket header
|
||||
|
||||
const message_len = msg.len + session_id.len + 1 + field.len + 10;
|
||||
|
||||
var buf: std.ArrayListUnmanaged(u8) = .{};
|
||||
@@ -283,7 +404,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
buf.appendSliceAssumeCapacity("\"}");
|
||||
std.debug.assert(buf.items.len == message_len);
|
||||
|
||||
try self.client.sendJSONRaw(arena, buf);
|
||||
try cdp.client.sendJSONRaw(arena, buf);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -294,38 +415,29 @@ pub fn CDPT(comptime TypeProvider: type) type {
|
||||
// generic.
|
||||
pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
|
||||
return struct {
|
||||
// reference to our CDP instance
|
||||
cdp: *CDP_T,
|
||||
|
||||
// Comes directly from the input.id field
|
||||
id: ?i64,
|
||||
|
||||
// A misc arena that can be used for any allocation for processing
|
||||
// the message
|
||||
arena: Allocator,
|
||||
|
||||
// the browser session
|
||||
session: *CDP_T.Session,
|
||||
// reference to our CDP instance
|
||||
cdp: *CDP_T,
|
||||
|
||||
// The "action" of the message.Given a method of "LOG.enable", the
|
||||
// action is "enable"
|
||||
action: []const u8,
|
||||
// The browser context this command targets
|
||||
browser_context: ?*BrowserContext(CDP_T),
|
||||
|
||||
// Comes directly from the input.sessionId field
|
||||
session_id: ?[]const u8,
|
||||
|
||||
// Unparsed / untyped input.params.
|
||||
_params: ?InputParams,
|
||||
|
||||
// The full raw json input
|
||||
json: []const u8,
|
||||
// The command input (the id, optional session_id, params, ...)
|
||||
input: Input,
|
||||
|
||||
// In most cases, Sender is going to be cdp itself. We'll call
|
||||
// sender.sendJSON() and CDP will send it to the client. But some
|
||||
// comamnds are dispatched internally, in which cases the Sender will
|
||||
// be code to capture the data that we were "sending".
|
||||
sender: Sender,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn params(self: *const Self, comptime T: type) !?T {
|
||||
if (self._params) |p| {
|
||||
if (self.input.params) |p| {
|
||||
return try json.parseFromSliceLeaky(
|
||||
T,
|
||||
self.arena,
|
||||
@@ -336,20 +448,26 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn createBrowserContext(self: *Self) !*BrowserContext(CDP_T) {
|
||||
_ = try self.cdp.createBrowserContext();
|
||||
self.browser_context = &self.cdp.browser_context.?;
|
||||
return self.browser_context.?;
|
||||
}
|
||||
|
||||
const SendResultOpts = struct {
|
||||
include_session_id: bool = true,
|
||||
};
|
||||
pub fn sendResult(self: *Self, result: anytype, opts: SendResultOpts) !void {
|
||||
return self.sender.sendJSON(.{
|
||||
.id = self.id,
|
||||
.id = self.input.id,
|
||||
.result = if (comptime @typeInfo(@TypeOf(result)) == .Null) struct {}{} else result,
|
||||
.sessionId = if (opts.include_session_id) self.session_id else null,
|
||||
.sessionId = if (opts.include_session_id) self.input.session_id else null,
|
||||
});
|
||||
}
|
||||
|
||||
const SendEventOpts = struct {
|
||||
session_id: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void {
|
||||
// Events ALWAYS go to the client. self.sender should not be used
|
||||
return self.cdp.sendJSON(.{
|
||||
@@ -358,6 +476,32 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
|
||||
.sessionId = opts.session_id,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn sendError(self: *Self, code: i32, message: []const u8) !void {
|
||||
return self.sender.sendJSON(.{
|
||||
.id = self.input.id,
|
||||
.code = code,
|
||||
.message = message,
|
||||
});
|
||||
}
|
||||
|
||||
const Input = struct {
|
||||
// When we reply to a message, we echo back the message id
|
||||
id: ?i64,
|
||||
|
||||
// The "action" of the message.Given a method of "LOG.enable", the
|
||||
// action is "enable"
|
||||
action: []const u8,
|
||||
|
||||
// See notes in BrowserContext about session_id
|
||||
session_id: ?[]const u8,
|
||||
|
||||
// Unparsed / untyped input.params.
|
||||
params: ?InputParams,
|
||||
|
||||
// The full raw json input
|
||||
json: []const u8,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -395,24 +539,7 @@ const InputParams = struct {
|
||||
}
|
||||
};
|
||||
|
||||
// Common
|
||||
// ------
|
||||
|
||||
// TODO: hard coded IDs
|
||||
pub const SessionID = enum {
|
||||
BROWSERSESSIONID597D9875C664CAC0,
|
||||
CONTEXTSESSIONID0497A05C95417CF4,
|
||||
|
||||
pub fn parse(str: []const u8) !SessionID {
|
||||
return std.meta.stringToEnum(SessionID, str) orelse {
|
||||
log.err("parse sessionID: {s}", .{str});
|
||||
return error.InvalidSessionID;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
|
||||
test "cdp: invalid json" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
@@ -425,6 +552,7 @@ test "cdp: invalid json" {
|
||||
try testing.expectError(error.InvalidMethod, ctx.processMessage(.{
|
||||
.method = "Target",
|
||||
}));
|
||||
try ctx.expectSentError(-31998, "InvalidMethod", .{});
|
||||
|
||||
try testing.expectError(error.UnknownDomain, ctx.processMessage(.{
|
||||
.method = "Unknown.domain",
|
||||
@@ -434,3 +562,53 @@ test "cdp: invalid json" {
|
||||
.method = "Target.over9000",
|
||||
}));
|
||||
}
|
||||
|
||||
test "cdp: invalid sessionId" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
// we have no browser context
|
||||
try ctx.processMessage(.{ .method = "Hi", .sessionId = "nope" });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{});
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context but no session_id
|
||||
_ = try ctx.loadBrowserContext(.{});
|
||||
try ctx.processMessage(.{ .method = "Hi", .sessionId = "BC-Has-No-SessionId" });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{});
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context with a different session_id
|
||||
_ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" });
|
||||
try ctx.processMessage(.{ .method = "Hi", .sessionId = "SESS-1" });
|
||||
try ctx.expectSentError(-32001, "Unknown sessionId", .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp: STARTUP sessionId" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
// we have no browser context
|
||||
try ctx.processMessage(.{ .id = 2, .method = "Hi", .sessionId = "STARTUP" });
|
||||
try ctx.expectSentResult(null, .{ .id = 2, .index = 0, .session_id = "STARTUP" });
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context but no session_id
|
||||
_ = try ctx.loadBrowserContext(.{});
|
||||
try ctx.processMessage(.{ .id = 3, .method = "Hi", .sessionId = "STARTUP" });
|
||||
try ctx.expectSentResult(null, .{ .id = 3, .index = 0, .session_id = "STARTUP" });
|
||||
}
|
||||
|
||||
{
|
||||
// we have a brower context with a different session_id
|
||||
_ = try ctx.loadBrowserContext(.{ .session_id = "SESS-2" });
|
||||
try ctx.processMessage(.{ .id = 4, .method = "Hi", .sessionId = "STARTUP" });
|
||||
try ctx.expectSentResult(null, .{ .id = 4, .index = 0, .session_id = "STARTUP" });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ const cdp = @import("cdp.zig");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
|
||||
@@ -29,7 +29,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
performSearch,
|
||||
getSearchResults,
|
||||
discardSearchResults,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
@@ -133,14 +133,13 @@ fn getDocument(cmd: anytype) !void {
|
||||
// pierce: ?bool = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
// retrieve the root node
|
||||
const page = cmd.session.page orelse return error.NoPage;
|
||||
const doc = page.doc orelse return error.NoDocument;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const state = cmd.cdp;
|
||||
const node = parser.documentToNode(doc);
|
||||
var n = try Node.init(node, &state.node_list);
|
||||
_ = try n.initChildren(cmd.arena, node, &state.node_list);
|
||||
var n = try Node.init(node, &bc.node_list);
|
||||
_ = try n.initChildren(cmd.arena, node, &bc.node_list);
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.root = n,
|
||||
@@ -184,21 +183,20 @@ fn performSearch(cmd: anytype) !void {
|
||||
includeUserAgentShadowDOM: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
// retrieve the root node
|
||||
const page = cmd.session.page orelse return error.NoPage;
|
||||
const doc = page.doc orelse return error.NoDocument;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||
const doc = page.doc orelse return error.DocumentNotLoaded;
|
||||
|
||||
const list = try css.querySelectorAll(cmd.cdp.allocator, parser.documentToNode(doc), params.query);
|
||||
const ln = list.nodes.items.len;
|
||||
var ns = try NodeSearch.initCapacity(cmd.cdp.allocator, ln);
|
||||
|
||||
var state = cmd.cdp;
|
||||
for (list.nodes.items) |n| {
|
||||
const id = try state.node_list.set(n);
|
||||
const id = try bc.node_list.set(n);
|
||||
try ns.append(id);
|
||||
}
|
||||
|
||||
try state.node_search_list.append(ns);
|
||||
try bc.node_search_list.append(ns);
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.searchId = ns.name,
|
||||
@@ -212,13 +210,14 @@ fn discardSearchResults(cmd: anytype) !void {
|
||||
searchId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
var state = cmd.cdp;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// retrieve the search from context
|
||||
for (state.node_search_list.items, 0..) |*s, i| {
|
||||
for (bc.node_search_list.items, 0..) |*s, i| {
|
||||
if (!std.mem.eql(u8, s.name, params.searchId)) continue;
|
||||
|
||||
s.deinit();
|
||||
_ = state.node_search_list.swapRemove(i);
|
||||
_ = bc.node_search_list.swapRemove(i);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -237,10 +236,11 @@ fn getSearchResults(cmd: anytype) !void {
|
||||
return error.BadIndices;
|
||||
}
|
||||
|
||||
const state = cmd.cdp;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// retrieve the search from context
|
||||
var ns: ?*const NodeSearch = undefined;
|
||||
for (state.node_search_list.items) |s| {
|
||||
for (bc.node_search_list.items) |s| {
|
||||
if (!std.mem.eql(u8, s.name, params.searchId)) continue;
|
||||
ns = &s;
|
||||
break;
|
||||
|
||||
@@ -26,7 +26,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
setFocusEmulationEnabled,
|
||||
setDeviceMetricsOverride,
|
||||
setTouchEmulationEnabled,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.setEmulatedMedia => return setEmulatedMedia(cmd),
|
||||
|
||||
@@ -22,7 +22,7 @@ const cdp = @import("cdp.zig");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
disable,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.disable => return cmd.sendResult(null, .{}),
|
||||
|
||||
@@ -22,7 +22,7 @@ const cdp = @import("cdp.zig");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
|
||||
@@ -22,7 +22,7 @@ const cdp = @import("cdp.zig");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
|
||||
@@ -23,7 +23,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
setCacheDisabled,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
|
||||
154
src/cdp/page.zig
154
src/cdp/page.zig
@@ -28,7 +28,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
addScriptToEvaluateOnNewDocument,
|
||||
createIsolatedWorld,
|
||||
navigate,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
@@ -56,43 +56,16 @@ const Frame = struct {
|
||||
};
|
||||
|
||||
fn getFrameTree(cmd: anytype) !void {
|
||||
// output
|
||||
const FrameTree = struct {
|
||||
frameTree: struct {
|
||||
frame: Frame,
|
||||
},
|
||||
childFrames: ?[]@This() = null,
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.getFrameTree { ");
|
||||
try writer.writeAll(".frameTree = { ");
|
||||
try writer.writeAll(".frame = { ");
|
||||
|
||||
const frame = self.frameTree.frame;
|
||||
try writer.writeAll(".id = ");
|
||||
try std.fmt.formatText(frame.id, "s", options, writer);
|
||||
try writer.writeAll(", .loaderId = ");
|
||||
try std.fmt.formatText(frame.loaderId, "s", options, writer);
|
||||
try writer.writeAll(", .url = ");
|
||||
try std.fmt.formatText(frame.url, "s", options, writer);
|
||||
try writer.writeAll(" } } }");
|
||||
}
|
||||
};
|
||||
|
||||
const state = cmd.cdp;
|
||||
return cmd.sendResult(FrameTree{
|
||||
return cmd.sendResult(.{
|
||||
.frameTree = .{
|
||||
.frame = .{
|
||||
.id = state.frame_id,
|
||||
.url = state.url,
|
||||
.securityOrigin = state.security_origin,
|
||||
.secureContextType = state.secure_context_type,
|
||||
.loaderId = state.loader_id,
|
||||
.frame = Frame{
|
||||
.url = bc.url,
|
||||
.id = bc.frame_id,
|
||||
.loaderId = bc.loader_id,
|
||||
.securityOrigin = bc.security_origin,
|
||||
.secureContextType = bc.secure_context_type,
|
||||
},
|
||||
},
|
||||
}, .{});
|
||||
@@ -103,7 +76,8 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
|
||||
// enabled: bool,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
cmd.cdp.page_life_cycle_events = true;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
bc.page_life_cycle_events = true;
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
@@ -116,27 +90,16 @@ fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
|
||||
// runImmediately: bool = false,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
const Response = struct {
|
||||
identifier: []const u8 = "1",
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.addScriptToEvaluateOnNewDocument { ");
|
||||
try writer.writeAll(".identifier = ");
|
||||
try std.fmt.formatText(self.identifier, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return cmd.sendResult(Response{}, .{});
|
||||
return cmd.sendResult(.{
|
||||
.identifier = "1",
|
||||
}, .{});
|
||||
}
|
||||
|
||||
// TODO: hard coded method
|
||||
fn createIsolatedWorld(cmd: anytype) !void {
|
||||
const session_id = cmd.session_id orelse return error.SessionIdRequired;
|
||||
_ = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
const session_id = cmd.input.session_id orelse return error.SessionIdRequired;
|
||||
|
||||
const params = (try cmd.params(struct {
|
||||
frameId: []const u8,
|
||||
@@ -166,7 +129,16 @@ fn createIsolatedWorld(cmd: anytype) !void {
|
||||
}
|
||||
|
||||
fn navigate(cmd: anytype) !void {
|
||||
const session_id = cmd.session_id orelse return error.SessionIdRequired;
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// didn't create?
|
||||
_ = bc.target_id orelse return error.TargetIdNotLoaded;
|
||||
|
||||
// didn't attach?
|
||||
const session_id = bc.session_id orelse return error.SessionIdNotLoaded;
|
||||
|
||||
// if we have a target_id we have to have a page;
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
const params = (try cmd.params(struct {
|
||||
url: []const u8,
|
||||
@@ -177,12 +149,11 @@ fn navigate(cmd: anytype) !void {
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
// change state
|
||||
var state = cmd.cdp;
|
||||
state.reset();
|
||||
state.url = params.url;
|
||||
bc.reset();
|
||||
bc.url = params.url;
|
||||
|
||||
// TODO: hard coded ID
|
||||
state.loader_id = "AF8667A203C5392DBE9AC290044AA4C2";
|
||||
bc.loader_id = "AF8667A203C5392DBE9AC290044AA4C2";
|
||||
|
||||
const LifecycleEvent = struct {
|
||||
frameId: []const u8,
|
||||
@@ -192,8 +163,8 @@ fn navigate(cmd: anytype) !void {
|
||||
};
|
||||
|
||||
var life_event = LifecycleEvent{
|
||||
.frameId = state.frame_id,
|
||||
.loaderId = state.loader_id,
|
||||
.frameId = bc.frame_id,
|
||||
.loaderId = bc.loader_id,
|
||||
.name = "init",
|
||||
.timestamp = 343721.796037,
|
||||
};
|
||||
@@ -201,39 +172,17 @@ fn navigate(cmd: anytype) !void {
|
||||
// frameStartedLoading event
|
||||
// TODO: event partially hard coded
|
||||
try cmd.sendEvent("Page.frameStartedLoading", .{
|
||||
.frameId = state.frame_id,
|
||||
.frameId = bc.frame_id,
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
if (state.page_life_cycle_events) {
|
||||
if (bc.page_life_cycle_events) {
|
||||
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// output
|
||||
const Response = struct {
|
||||
frameId: []const u8,
|
||||
loaderId: ?[]const u8,
|
||||
errorText: ?[]const u8 = null,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.page.navigate.Resp { ");
|
||||
try writer.writeAll(".frameId = ");
|
||||
try std.fmt.formatText(self.frameId, "s", options, writer);
|
||||
if (self.loaderId) |loaderId| {
|
||||
try writer.writeAll(", .loaderId = '");
|
||||
try std.fmt.formatText(loaderId, "s", options, writer);
|
||||
}
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
|
||||
try cmd.sendResult(Response{
|
||||
.frameId = state.frame_id,
|
||||
.loaderId = state.loader_id,
|
||||
try cmd.sendResult(.{
|
||||
.frameId = bc.frame_id,
|
||||
.loaderId = bc.loader_id,
|
||||
}, .{});
|
||||
|
||||
// TODO: at this point do we need async the following actions to be async?
|
||||
@@ -242,24 +191,21 @@ fn navigate(cmd: anytype) !void {
|
||||
// TODO: noop event, we have no env context at this point, is it necesarry?
|
||||
try cmd.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||
|
||||
// Launch navigate, the page must have been created by a
|
||||
// target.createTarget.
|
||||
var p = cmd.session.currentPage() orelse return error.NoPage;
|
||||
state.execution_context_id += 1;
|
||||
|
||||
const aux_data = try std.fmt.allocPrint(
|
||||
cmd.arena,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{state.frame_id},
|
||||
.{bc.frame_id},
|
||||
);
|
||||
try p.navigate(params.url, aux_data);
|
||||
|
||||
var page = bc.session.currentPage().?;
|
||||
try page.navigate(params.url, aux_data);
|
||||
|
||||
// Events
|
||||
|
||||
// lifecycle init event
|
||||
// TODO: partially hard coded
|
||||
if (state.page_life_cycle_events) {
|
||||
if (bc.page_life_cycle_events) {
|
||||
life_event.name = "init";
|
||||
life_event.timestamp = 343721.796037;
|
||||
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
|
||||
@@ -271,11 +217,11 @@ fn navigate(cmd: anytype) !void {
|
||||
try cmd.sendEvent("Page.frameNavigated", .{
|
||||
.type = "Navigation",
|
||||
.frame = Frame{
|
||||
.id = state.frame_id,
|
||||
.url = state.url,
|
||||
.securityOrigin = state.security_origin,
|
||||
.secureContextType = state.secure_context_type,
|
||||
.loaderId = state.loader_id,
|
||||
.id = bc.frame_id,
|
||||
.url = bc.url,
|
||||
.securityOrigin = bc.security_origin,
|
||||
.secureContextType = bc.secure_context_type,
|
||||
.loaderId = bc.loader_id,
|
||||
},
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
@@ -289,7 +235,7 @@ fn navigate(cmd: anytype) !void {
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (state.page_life_cycle_events) {
|
||||
if (bc.page_life_cycle_events) {
|
||||
life_event.name = "DOMContentLoaded";
|
||||
life_event.timestamp = 343721.803338;
|
||||
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
|
||||
@@ -305,7 +251,7 @@ fn navigate(cmd: anytype) !void {
|
||||
|
||||
// lifecycle DOMContentLoaded event
|
||||
// TODO: partially hard coded
|
||||
if (state.page_life_cycle_events) {
|
||||
if (bc.page_life_cycle_events) {
|
||||
life_event.name = "load";
|
||||
life_event.timestamp = 343721.824655;
|
||||
try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id });
|
||||
@@ -313,6 +259,6 @@ fn navigate(cmd: anytype) !void {
|
||||
|
||||
// frameStoppedLoading
|
||||
return cmd.sendEvent("Page.frameStoppedLoading", .{
|
||||
.frameId = state.frame_id,
|
||||
.frameId = bc.frame_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const asUint = @import("../str/parser.zig").asUint;
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn processMessage(cmd: anytype) !void {
|
||||
addBinding,
|
||||
callFunctionOn,
|
||||
releaseObject,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.runIfWaitingForDebugger => return cmd.sendResult(null, .{}),
|
||||
@@ -41,26 +41,24 @@ fn sendInspector(cmd: anytype, action: anytype) !void {
|
||||
try logInspector(cmd, action);
|
||||
}
|
||||
|
||||
if (cmd.session_id) |s| {
|
||||
cmd.cdp.session_id = try cdp.SessionID.parse(s);
|
||||
}
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
|
||||
// remove awaitPromise true params
|
||||
// TODO: delete when Promise are correctly handled by zig-js-runtime
|
||||
if (action == .callFunctionOn or action == .evaluate) {
|
||||
const json = cmd.json;
|
||||
const json = cmd.input.json;
|
||||
if (std.mem.indexOf(u8, json, "\"awaitPromise\":true")) |_| {
|
||||
// +1 because we'll be turning a true -> false
|
||||
const buf = try cmd.arena.alloc(u8, json.len + 1);
|
||||
_ = std.mem.replace(u8, json, "\"awaitPromise\":true", "\"awaitPromise\":false", buf);
|
||||
cmd.session.callInspector(buf);
|
||||
bc.session.callInspector(buf);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cmd.session.callInspector(cmd.json);
|
||||
bc.session.callInspector(cmd.input.json);
|
||||
|
||||
if (cmd.id != null) {
|
||||
if (cmd.input.id != null) {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
}
|
||||
@@ -110,7 +108,7 @@ fn logInspector(cmd: anytype, action: anytype) !void {
|
||||
},
|
||||
else => return,
|
||||
};
|
||||
const id = cmd.id orelse return error.RequiredId;
|
||||
const id = cmd.input.id orelse return error.RequiredId;
|
||||
const name = try std.fmt.allocPrint(cmd.arena, "id_{d}.js", .{id});
|
||||
|
||||
var dir = try std.fs.cwd().makeOpenPath("zig-cache/tmp", .{});
|
||||
|
||||
@@ -22,7 +22,7 @@ const cdp = @import("cdp.zig");
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
enable,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.enable => return cmd.sendResult(null, .{}),
|
||||
|
||||
@@ -17,136 +17,229 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const std = @import("std");
|
||||
const cdp = @import("cdp.zig");
|
||||
|
||||
const log = std.log.scoped(.cdp);
|
||||
|
||||
// TODO: hard coded IDs
|
||||
const CONTEXT_ID = "CONTEXTIDDCCDD11109E2D4FEFBE4F89";
|
||||
const PAGE_TARGET_ID = "PAGETARGETIDB638E9DC0F52DDC";
|
||||
const BROWSER_TARGET_ID = "browser9-targ-et6f-id0e-83f3ab73a30c";
|
||||
const BROWER_CONTEXT_ID = "BROWSERCONTEXTIDA95049E9DFE95EA9";
|
||||
const TARGET_ID = "TARGETID460A8F29706A2ADF14316298";
|
||||
const LOADER_ID = "LOADERID42AA389647D702B4D805F49A";
|
||||
|
||||
pub fn processMessage(cmd: anytype) !void {
|
||||
const action = std.meta.stringToEnum(enum {
|
||||
setDiscoverTargets,
|
||||
setAutoAttach,
|
||||
attachToTarget,
|
||||
getTargetInfo,
|
||||
getBrowserContexts,
|
||||
createBrowserContext,
|
||||
disposeBrowserContext,
|
||||
createTarget,
|
||||
closeTarget,
|
||||
sendMessageToTarget,
|
||||
createBrowserContext,
|
||||
createTarget,
|
||||
detachFromTarget,
|
||||
}, cmd.action) orelse return error.UnknownMethod;
|
||||
disposeBrowserContext,
|
||||
getBrowserContexts,
|
||||
getTargetInfo,
|
||||
sendMessageToTarget,
|
||||
setAutoAttach,
|
||||
setDiscoverTargets,
|
||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||
|
||||
switch (action) {
|
||||
.setDiscoverTargets => return setDiscoverTargets(cmd),
|
||||
.setAutoAttach => return setAutoAttach(cmd),
|
||||
.attachToTarget => return attachToTarget(cmd),
|
||||
.getTargetInfo => return getTargetInfo(cmd),
|
||||
.getBrowserContexts => return getBrowserContexts(cmd),
|
||||
.createBrowserContext => return createBrowserContext(cmd),
|
||||
.disposeBrowserContext => return disposeBrowserContext(cmd),
|
||||
.createTarget => return createTarget(cmd),
|
||||
.closeTarget => return closeTarget(cmd),
|
||||
.sendMessageToTarget => return sendMessageToTarget(cmd),
|
||||
.createBrowserContext => return createBrowserContext(cmd),
|
||||
.createTarget => return createTarget(cmd),
|
||||
.detachFromTarget => return detachFromTarget(cmd),
|
||||
.disposeBrowserContext => return disposeBrowserContext(cmd),
|
||||
.getBrowserContexts => return getBrowserContexts(cmd),
|
||||
.getTargetInfo => return getTargetInfo(cmd),
|
||||
.sendMessageToTarget => return sendMessageToTarget(cmd),
|
||||
.setAutoAttach => return setAutoAttach(cmd),
|
||||
.setDiscoverTargets => return setDiscoverTargets(cmd),
|
||||
}
|
||||
}
|
||||
// TODO: noop method
|
||||
fn setDiscoverTargets(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
const AttachToTarget = struct {
|
||||
sessionId: []const u8,
|
||||
targetInfo: TargetInfo,
|
||||
waitingForDebugger: bool = false,
|
||||
};
|
||||
|
||||
const TargetCreated = struct {
|
||||
sessionId: []const u8,
|
||||
targetInfo: TargetInfo,
|
||||
};
|
||||
|
||||
const TargetInfo = struct {
|
||||
targetId: []const u8,
|
||||
type: []const u8 = "page",
|
||||
title: []const u8,
|
||||
url: []const u8,
|
||||
attached: bool = true,
|
||||
canAccessOpener: bool = false,
|
||||
browserContextId: []const u8,
|
||||
};
|
||||
|
||||
// TODO: noop method
|
||||
fn setAutoAttach(cmd: anytype) !void {
|
||||
// const TargetFilter = struct {
|
||||
// type: ?[]const u8 = null,
|
||||
// exclude: ?bool = null,
|
||||
// };
|
||||
|
||||
// const params = (try cmd.params(struct {
|
||||
// autoAttach: bool,
|
||||
// waitForDebuggerOnStart: bool,
|
||||
// flatten: bool = true,
|
||||
// filter: ?[]TargetFilter = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
// attachedToTarget event
|
||||
if (cmd.session_id == null) {
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = cdp.BROWSER_SESSION_ID,
|
||||
.targetInfo = .{
|
||||
.targetId = PAGE_TARGET_ID,
|
||||
.title = "about:blank",
|
||||
.url = cdp.URL_BASE,
|
||||
.browserContextId = BROWER_CONTEXT_ID,
|
||||
},
|
||||
}, .{});
|
||||
fn getBrowserContexts(cmd: anytype) !void {
|
||||
var browser_context_ids: []const []const u8 = undefined;
|
||||
if (cmd.browser_context) |bc| {
|
||||
browser_context_ids = &.{bc.id};
|
||||
} else {
|
||||
browser_context_ids = &.{};
|
||||
}
|
||||
|
||||
return cmd.sendResult(null, .{});
|
||||
return cmd.sendResult(.{
|
||||
.browserContextIds = browser_context_ids,
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
fn createBrowserContext(cmd: anytype) !void {
|
||||
const bc = cmd.createBrowserContext() catch |err| switch (err) {
|
||||
error.AlreadyExists => return cmd.sendError(-32000, "Cannot have more than one browser context at a time"),
|
||||
else => return err,
|
||||
};
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.browserContextId = bc.id,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn disposeBrowserContext(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
browserContextId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (cmd.cdp.disposeBrowserContext(params.browserContextId) == false) {
|
||||
return cmd.sendError(-32602, "No browser context with the given id found");
|
||||
}
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn createTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
// url: []const u8,
|
||||
// width: ?u64 = null,
|
||||
// height: ?u64 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
// enableBeginFrameControl: bool = false,
|
||||
// newWindow: bool = false,
|
||||
// background: bool = false,
|
||||
// forTab: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
if (bc.target_id != null) {
|
||||
return error.TargetAlreadyLoaded;
|
||||
}
|
||||
if (params.browserContextId) |param_browser_context_id| {
|
||||
if (std.mem.eql(u8, param_browser_context_id, bc.id) == false) {
|
||||
return error.UnknownBrowserContextId;
|
||||
}
|
||||
}
|
||||
|
||||
// if target_id is null, we should never have a page
|
||||
std.debug.assert(bc.session.page == null);
|
||||
|
||||
// if target_id is null, we should never have a session_id
|
||||
std.debug.assert(bc.session_id == null);
|
||||
|
||||
const page = try bc.session.createPage();
|
||||
const target_id = cmd.cdp.target_id_gen.next();
|
||||
|
||||
// change CDP state
|
||||
bc.url = "about:blank";
|
||||
bc.security_origin = "://";
|
||||
bc.secure_context_type = "InsecureScheme";
|
||||
bc.loader_id = LOADER_ID;
|
||||
|
||||
// start the js env
|
||||
const aux_data = try std.fmt.allocPrint(
|
||||
cmd.arena,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{target_id},
|
||||
);
|
||||
try page.start(aux_data);
|
||||
|
||||
try cmd.sendResult(.{
|
||||
.targetId = target_id,
|
||||
}, .{});
|
||||
|
||||
// send targetCreated event
|
||||
// TODO: should this only be sent when Target.setDiscoverTargets
|
||||
// has been enabled?
|
||||
try cmd.sendEvent("Target.targetCreated", .{
|
||||
.targetInfo = TargetInfo{
|
||||
.url = bc.url,
|
||||
.targetId = target_id,
|
||||
.title = "about:blank",
|
||||
.browserContextId = bc.id,
|
||||
.attached = false,
|
||||
},
|
||||
}, .{});
|
||||
|
||||
// only if setAutoAttach is true?
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
bc.target_id = target_id;
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn attachToTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
targetId: []const u8,
|
||||
flatten: bool = true,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
// attachedToTarget event
|
||||
if (cmd.session_id == null) {
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = cdp.BROWSER_SESSION_ID,
|
||||
.targetInfo = .{
|
||||
.targetId = params.targetId,
|
||||
.title = "about:blank",
|
||||
.url = cdp.URL_BASE,
|
||||
.browserContextId = BROWER_CONTEXT_ID,
|
||||
},
|
||||
}, .{});
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
if (std.mem.eql(u8, target_id, params.targetId) == false) {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
if (bc.session_id != null) {
|
||||
return error.SessionAlreadyLoaded;
|
||||
}
|
||||
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
|
||||
return cmd.sendResult(
|
||||
.{ .sessionId = cmd.session_id orelse cdp.BROWSER_SESSION_ID },
|
||||
.{ .sessionId = bc.session_id },
|
||||
.{ .include_session_id = false },
|
||||
);
|
||||
}
|
||||
|
||||
fn closeTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
targetId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
if (std.mem.eql(u8, target_id, params.targetId) == false) {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
// can't be null if we have a target_id
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false });
|
||||
|
||||
// could be null, created but never attached
|
||||
if (bc.session_id) |session_id| {
|
||||
// Inspector.detached event
|
||||
try cmd.sendEvent("Inspector.detached", .{
|
||||
.reason = "Render process gone.",
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
// detachedFromTarget event
|
||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||
.targetId = target_id,
|
||||
.sessionId = session_id,
|
||||
.reason = "Render process gone.",
|
||||
}, .{});
|
||||
|
||||
bc.session_id = null;
|
||||
}
|
||||
|
||||
bc.session.currentPage().?.end();
|
||||
bc.target_id = null;
|
||||
}
|
||||
|
||||
fn getTargetInfo(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// targetId: ?[]const u8 = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
const params = (try cmd.params(struct {
|
||||
targetId: ?[]const u8 = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
if (params.targetId) |param_target_id| {
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
const target_id = bc.target_id orelse return error.TargetNotLoaded;
|
||||
if (std.mem.eql(u8, target_id, param_target_id) == false) {
|
||||
return error.UnknownTargetId;
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.targetId = target_id,
|
||||
.type = "page",
|
||||
.title = "",
|
||||
.url = "",
|
||||
.attached = true,
|
||||
.canAccessOpener = false,
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.targetId = BROWSER_TARGET_ID,
|
||||
.type = "browser",
|
||||
.title = "",
|
||||
.url = "",
|
||||
@@ -155,188 +248,24 @@ fn getTargetInfo(cmd: anytype) !void {
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
// Browser context are not handled and not in the roadmap for now
|
||||
// The following methods are "fake"
|
||||
|
||||
// TODO: noop method
|
||||
fn getBrowserContexts(cmd: anytype) !void {
|
||||
var context_ids: []const []const u8 = undefined;
|
||||
if (cmd.cdp.context_id) |context_id| {
|
||||
context_ids = &.{context_id};
|
||||
} else {
|
||||
context_ids = &.{};
|
||||
}
|
||||
|
||||
return cmd.sendResult(.{
|
||||
.browserContextIds = context_ids,
|
||||
}, .{ .include_session_id = false });
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn createBrowserContext(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// disposeOnDetach: bool = false,
|
||||
// proxyServer: ?[]const u8 = null,
|
||||
// proxyBypassList: ?[]const u8 = null,
|
||||
// originsWithUniversalNetworkAccess: ?[][]const u8 = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
cmd.cdp.context_id = CONTEXT_ID;
|
||||
|
||||
const Response = struct {
|
||||
browserContextId: []const u8,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.target.createBrowserContext { ");
|
||||
try writer.writeAll(".browserContextId = ");
|
||||
try std.fmt.formatText(self.browserContextId, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
|
||||
return cmd.sendResult(Response{
|
||||
.browserContextId = CONTEXT_ID,
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn disposeBrowserContext(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// browserContextId: []const u8,
|
||||
// proxyServer: ?[]const u8 = null,
|
||||
// proxyBypassList: ?[]const u8 = null,
|
||||
// originsWithUniversalNetworkAccess: ?[][]const u8 = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
try cmd.cdp.newSession();
|
||||
try cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn createTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
url: []const u8,
|
||||
width: ?u64 = null,
|
||||
height: ?u64 = null,
|
||||
browserContextId: ?[]const u8 = null,
|
||||
enableBeginFrameControl: bool = false,
|
||||
newWindow: bool = false,
|
||||
background: bool = false,
|
||||
forTab: ?bool = null,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
// change CDP state
|
||||
var state = cmd.cdp;
|
||||
state.frame_id = TARGET_ID;
|
||||
state.url = "about:blank";
|
||||
state.security_origin = "://";
|
||||
state.secure_context_type = "InsecureScheme";
|
||||
state.loader_id = LOADER_ID;
|
||||
|
||||
if (cmd.session_id) |s| {
|
||||
state.session_id = try cdp.SessionID.parse(s);
|
||||
}
|
||||
|
||||
// TODO stop the previous page instead?
|
||||
if (cmd.session.page != null) {
|
||||
return error.pageAlreadyExists;
|
||||
}
|
||||
|
||||
// create the page
|
||||
const p = try cmd.session.createPage();
|
||||
state.execution_context_id += 1;
|
||||
|
||||
// start the js env
|
||||
const aux_data = try std.fmt.allocPrint(
|
||||
cmd.arena,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{state.frame_id},
|
||||
);
|
||||
try p.start(aux_data);
|
||||
|
||||
const browser_context_id = params.browserContextId orelse CONTEXT_ID;
|
||||
|
||||
// send targetCreated event
|
||||
try cmd.sendEvent("Target.targetCreated", TargetCreated{
|
||||
.sessionId = cdp.CONTEXT_SESSION_ID,
|
||||
.targetInfo = .{
|
||||
.targetId = state.frame_id,
|
||||
.title = "about:blank",
|
||||
.url = state.url,
|
||||
.browserContextId = browser_context_id,
|
||||
.attached = true,
|
||||
},
|
||||
}, .{ .session_id = cmd.session_id });
|
||||
|
||||
// send attachToTarget event
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = cdp.CONTEXT_SESSION_ID,
|
||||
.waitingForDebugger = true,
|
||||
.targetInfo = .{
|
||||
.targetId = state.frame_id,
|
||||
.title = "about:blank",
|
||||
.url = state.url,
|
||||
.browserContextId = browser_context_id,
|
||||
.attached = true,
|
||||
},
|
||||
}, .{ .session_id = cmd.session_id });
|
||||
|
||||
const Response = struct {
|
||||
targetId: []const u8 = TARGET_ID,
|
||||
|
||||
pub fn format(
|
||||
self: @This(),
|
||||
comptime _: []const u8,
|
||||
options: std.fmt.FormatOptions,
|
||||
writer: anytype,
|
||||
) !void {
|
||||
try writer.writeAll("cdp.target.createTarget { ");
|
||||
try writer.writeAll(".targetId = ");
|
||||
try std.fmt.formatText(self.targetId, "s", options, writer);
|
||||
try writer.writeAll(" }");
|
||||
}
|
||||
};
|
||||
return cmd.sendResult(Response{}, .{});
|
||||
}
|
||||
|
||||
fn closeTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
targetId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
try cmd.sendResult(.{
|
||||
.success = true,
|
||||
}, .{ .include_session_id = false });
|
||||
|
||||
const session_id = cmd.session_id orelse cdp.CONTEXT_SESSION_ID;
|
||||
|
||||
// Inspector.detached event
|
||||
try cmd.sendEvent("Inspector.detached", .{
|
||||
.reason = "Render process gone.",
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
// detachedFromTarget event
|
||||
try cmd.sendEvent("Target.detachedFromTarget", .{
|
||||
.sessionId = session_id,
|
||||
.targetId = params.targetId,
|
||||
.reason = "Render process gone.",
|
||||
}, .{});
|
||||
|
||||
if (cmd.session.page) |*page| {
|
||||
page.end();
|
||||
}
|
||||
}
|
||||
|
||||
fn sendMessageToTarget(cmd: anytype) !void {
|
||||
const params = (try cmd.params(struct {
|
||||
message: []const u8,
|
||||
sessionId: []const u8,
|
||||
})) orelse return error.InvalidParams;
|
||||
|
||||
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
|
||||
if (bc.target_id == null) {
|
||||
return error.TargetNotLoaded;
|
||||
}
|
||||
|
||||
std.debug.assert(bc.session_id != null);
|
||||
if (std.mem.eql(u8, bc.session_id.?, params.sessionId) == false) {
|
||||
// Is this right? Is the params.sessionId meant to be the active
|
||||
// sessionId? What else could it be? We have no other session_id.
|
||||
return error.UnknownSessionId;
|
||||
}
|
||||
|
||||
const Capture = struct {
|
||||
allocator: std.mem.Allocator,
|
||||
buf: std.ArrayListUnmanaged(u8),
|
||||
@@ -354,7 +283,7 @@ fn sendMessageToTarget(cmd: anytype) !void {
|
||||
};
|
||||
|
||||
cmd.cdp.dispatch(cmd.arena, &capture, params.message) catch |err| {
|
||||
log.err("send message {d} ({s}): {any}", .{ cmd.id orelse -1, params.message, err });
|
||||
log.err("send message {d} ({s}): {any}", .{ cmd.input.id orelse -1, params.message, err });
|
||||
return err;
|
||||
};
|
||||
|
||||
@@ -368,3 +297,253 @@ fn sendMessageToTarget(cmd: anytype) !void {
|
||||
fn detachFromTarget(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
// TODO: noop method
|
||||
fn setDiscoverTargets(cmd: anytype) !void {
|
||||
return cmd.sendResult(null, .{});
|
||||
}
|
||||
|
||||
fn setAutoAttach(cmd: anytype) !void {
|
||||
// const params = (try cmd.params(struct {
|
||||
// autoAttach: bool,
|
||||
// waitForDebuggerOnStart: bool,
|
||||
// flatten: bool = true,
|
||||
// filter: ?[]TargetFilter = null,
|
||||
// })) orelse return error.InvalidParams;
|
||||
|
||||
// TODO: should set a flag to send Target.attachedToTarget events
|
||||
|
||||
try cmd.sendResult(null, .{});
|
||||
|
||||
if (cmd.browser_context) |bc| {
|
||||
if (bc.target_id == null) {
|
||||
// hasn't attached yet
|
||||
const target_id = cmd.cdp.target_id_gen.next();
|
||||
try doAttachtoTarget(cmd, target_id);
|
||||
bc.target_id = target_id;
|
||||
}
|
||||
// should we send something here?
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a hack. Puppeteer, and probably others, expect the Browser to
|
||||
// automatically started creating targets. Things like an empty tab, or
|
||||
// a blank page. And they block until this happens. So we send an event
|
||||
// telling them that they've been attached to our Broswer. Hopefully, the
|
||||
// first thing they'll do is create a real BrowserContext and progress from
|
||||
// there.
|
||||
// This hack requires the main cdp dispatch handler to special case
|
||||
// messages from this "STARTUP" session.
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = "STARTUP",
|
||||
.targetInfo = TargetInfo{
|
||||
.type = "browser",
|
||||
.targetId = "TID-STARTUP",
|
||||
.title = "about:blank",
|
||||
.url = "chrome://newtab/",
|
||||
.browserContextId = "BID-STARTUP",
|
||||
},
|
||||
}, .{});
|
||||
}
|
||||
|
||||
fn doAttachtoTarget(cmd: anytype, target_id: []const u8) !void {
|
||||
const bc = cmd.browser_context.?;
|
||||
std.debug.assert(bc.session_id == null);
|
||||
const session_id = cmd.cdp.session_id_gen.next();
|
||||
|
||||
try cmd.sendEvent("Target.attachedToTarget", AttachToTarget{
|
||||
.sessionId = session_id,
|
||||
.targetInfo = TargetInfo{
|
||||
.targetId = target_id,
|
||||
.title = "about:blank",
|
||||
.url = "chrome://newtab/",
|
||||
.browserContextId = bc.id,
|
||||
},
|
||||
}, .{});
|
||||
|
||||
bc.session_id = session_id;
|
||||
}
|
||||
|
||||
const AttachToTarget = struct {
|
||||
sessionId: []const u8,
|
||||
targetInfo: TargetInfo,
|
||||
waitingForDebugger: bool = false,
|
||||
};
|
||||
|
||||
const TargetInfo = struct {
|
||||
url: []const u8,
|
||||
title: []const u8,
|
||||
targetId: []const u8,
|
||||
attached: bool = true,
|
||||
type: []const u8 = "page",
|
||||
canAccessOpener: bool = false,
|
||||
browserContextId: []const u8,
|
||||
};
|
||||
|
||||
const testing = @import("testing.zig");
|
||||
test "cdp.target: getBrowserContexts" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
// {
|
||||
// // no browser context
|
||||
// try ctx.processMessage(.{.id = 4, .method = "Target.getBrowserContexts"});
|
||||
|
||||
// try ctx.expectSentResult(.{
|
||||
// .browserContextIds = &.{},
|
||||
// }, .{ .id = 4, .session_id = null });
|
||||
// }
|
||||
|
||||
{
|
||||
// with a browser context
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-X" });
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Target.getBrowserContexts" });
|
||||
|
||||
try ctx.expectSentResult(.{
|
||||
.browserContextIds = &.{"BID-X"},
|
||||
}, .{ .id = 5, .session_id = null });
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: createBrowserContext" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 4, .method = "Target.createBrowserContext" });
|
||||
try ctx.expectSentResult(.{
|
||||
.browserContextId = ctx.cdp().browser_context.?.id,
|
||||
}, .{ .id = 4, .session_id = null });
|
||||
}
|
||||
|
||||
{
|
||||
// we already have one now
|
||||
try ctx.processMessage(.{ .id = 5, .method = "Target.createBrowserContext" });
|
||||
try ctx.expectSentError(-32000, "Cannot have more than one browser context at a time", .{ .id = 5 });
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: disposeBrowserContext" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.InvalidParams, ctx.processMessage(.{ .id = 7, .method = "Target.disposeBrowserContext" }));
|
||||
try ctx.expectSentError(-31998, "InvalidParams", .{ .id = 7 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{
|
||||
.id = 8,
|
||||
.method = "Target.disposeBrowserContext",
|
||||
.params = .{ .browserContextId = "BID-10" },
|
||||
});
|
||||
try ctx.expectSentError(-32602, "No browser context with the given id found", .{ .id = 8 });
|
||||
}
|
||||
|
||||
{
|
||||
_ = try ctx.loadBrowserContext(.{ .id = "BID-20" });
|
||||
try ctx.processMessage(.{
|
||||
.id = 9,
|
||||
.method = "Target.disposeBrowserContext",
|
||||
.params = .{ .browserContextId = "BID-20" },
|
||||
});
|
||||
try ctx.expectSentResult(null, .{ .id = 9 });
|
||||
try testing.expectEqual(null, ctx.cdp().browser_context);
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: createTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{
|
||||
.id = 10,
|
||||
.method = "Target.createTarget",
|
||||
.params = struct {}{},
|
||||
}));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.UnknownBrowserContextId, ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownBrowserContextId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 10, .method = "Target.createTarget", .params = .{ .browserContextId = "BID-9" } });
|
||||
try testing.expectEqual(true, bc.target_id != null);
|
||||
try testing.expectString(
|
||||
\\{"isDefault":true,"type":"default","frameId":"TID-1"}
|
||||
, bc.session.page.?.aux_data);
|
||||
|
||||
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 });
|
||||
try ctx.expectSentEvent("Target.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = bc.session_id.?, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: closeTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "X" } }));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-A";
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-A" } });
|
||||
try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 });
|
||||
try testing.expectEqual(null, bc.session.page);
|
||||
try testing.expectEqual(null, bc.target_id);
|
||||
}
|
||||
}
|
||||
|
||||
test "cdp.target: attachToTarget" {
|
||||
var ctx = testing.context();
|
||||
defer ctx.deinit();
|
||||
|
||||
{
|
||||
try testing.expectError(error.BrowserContextNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "X" } }));
|
||||
try ctx.expectSentError(-31998, "BrowserContextNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
const bc = try ctx.loadBrowserContext(.{ .id = "BID-9" });
|
||||
{
|
||||
try testing.expectError(error.TargetNotLoaded, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "TargetNotLoaded", .{ .id = 10 });
|
||||
}
|
||||
|
||||
// pretend we createdTarget first
|
||||
_ = try bc.session.createPage();
|
||||
bc.target_id = "TID-B";
|
||||
{
|
||||
try testing.expectError(error.UnknownTargetId, ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }));
|
||||
try ctx.expectSentError(-31998, "UnknownTargetId", .{ .id = 10 });
|
||||
}
|
||||
|
||||
{
|
||||
try ctx.processMessage(.{ .id = 11, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-B" } });
|
||||
const session_id = bc.session_id.?;
|
||||
try ctx.expectSentResult(.{ .sessionId = session_id }, .{ .id = 11 });
|
||||
try ctx.expectSentEvent("Target.attachedToTarget", .{ .sessionId = session_id, .targetInfo = .{ .url = "chrome://newtab/", .title = "about:blank", .attached = true, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ const Allocator = std.mem.Allocator;
|
||||
|
||||
const Testing = @This();
|
||||
|
||||
const cdp = @import("cdp.zig");
|
||||
const main = @import("cdp.zig");
|
||||
const parser = @import("netsurf");
|
||||
|
||||
pub const expectEqual = std.testing.expectEqual;
|
||||
@@ -13,32 +13,57 @@ pub const expectError = std.testing.expectError;
|
||||
pub const expectString = std.testing.expectEqualStrings;
|
||||
|
||||
const Browser = struct {
|
||||
session: ?Session = null,
|
||||
session: ?*Session = null,
|
||||
arena: std.heap.ArenaAllocator,
|
||||
|
||||
pub fn init(_: Allocator, loop: anytype) Browser {
|
||||
pub fn init(allocator: Allocator, loop: anytype) Browser {
|
||||
_ = loop;
|
||||
return .{};
|
||||
return .{
|
||||
.arena = std.heap.ArenaAllocator.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(_: *const Browser) void {}
|
||||
pub fn deinit(self: *Browser) void {
|
||||
self.arena.deinit();
|
||||
}
|
||||
|
||||
pub fn newSession(self: *Browser, ctx: anytype) !*Session {
|
||||
_ = ctx;
|
||||
if (self.session != null) {
|
||||
return error.MockBrowserSessionAlreadyExists;
|
||||
}
|
||||
|
||||
self.session = .{};
|
||||
return &self.session.?;
|
||||
const allocator = self.arena.allocator();
|
||||
self.session = try allocator.create(Session);
|
||||
self.session.?.* = .{
|
||||
.page = null,
|
||||
.allocator = allocator,
|
||||
};
|
||||
return self.session.?;
|
||||
}
|
||||
|
||||
pub fn hasSession(self: *const Browser, session_id: []const u8) bool {
|
||||
const session = self.session orelse return false;
|
||||
return std.mem.eql(u8, session.id, session_id);
|
||||
}
|
||||
};
|
||||
|
||||
const Session = struct {
|
||||
page: ?Page = null,
|
||||
allocator: Allocator,
|
||||
|
||||
pub fn currentPage(self: *Session) ?*Page {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
pub fn createPage(self: *Session) !*Page {
|
||||
self.page = .{};
|
||||
if (self.page != null) {
|
||||
return error.MockBrowserPageAlreadyExists;
|
||||
}
|
||||
self.page = .{
|
||||
.session = self,
|
||||
.allocator = self.allocator,
|
||||
};
|
||||
return &self.page.?;
|
||||
}
|
||||
|
||||
@@ -49,6 +74,9 @@ const Session = struct {
|
||||
};
|
||||
|
||||
const Page = struct {
|
||||
session: *Session,
|
||||
allocator: Allocator,
|
||||
aux_data: []const u8 = "",
|
||||
doc: ?*parser.Document = null,
|
||||
|
||||
pub fn navigate(self: *Page, url: []const u8, aux_data: []const u8) !void {
|
||||
@@ -58,18 +86,18 @@ const Page = struct {
|
||||
}
|
||||
|
||||
pub fn start(self: *Page, aux_data: []const u8) !void {
|
||||
_ = self;
|
||||
_ = aux_data;
|
||||
self.aux_data = try self.allocator.dupe(u8, aux_data);
|
||||
}
|
||||
|
||||
pub fn end(self: *Page) void {
|
||||
_ = self;
|
||||
self.session.page = null;
|
||||
}
|
||||
};
|
||||
|
||||
const Client = struct {
|
||||
allocator: Allocator,
|
||||
sent: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
sent: std.ArrayListUnmanaged(json.Value) = .{},
|
||||
serialized: std.ArrayListUnmanaged([]const u8) = .{},
|
||||
|
||||
fn init(allocator: Allocator) Client {
|
||||
return .{
|
||||
@@ -78,15 +106,21 @@ const Client = struct {
|
||||
}
|
||||
|
||||
pub fn sendJSON(self: *Client, message: anytype, opts: json.StringifyOptions) !void {
|
||||
const serialized = try json.stringifyAlloc(self.allocator, message, opts);
|
||||
try self.sent.append(self.allocator, serialized);
|
||||
var opts_copy = opts;
|
||||
opts_copy.whitespace = .indent_2;
|
||||
const serialized = try json.stringifyAlloc(self.allocator, message, opts_copy);
|
||||
try self.serialized.append(self.allocator, serialized);
|
||||
|
||||
const value = try json.parseFromSliceLeaky(json.Value, self.allocator, serialized, .{});
|
||||
try self.sent.append(self.allocator, value);
|
||||
}
|
||||
};
|
||||
|
||||
const TestCDP = cdp.CDPT(struct {
|
||||
const TestCDP = main.CDPT(struct {
|
||||
pub const Loop = void;
|
||||
pub const Browser = Testing.Browser;
|
||||
pub const Session = Testing.Session;
|
||||
pub const Client = Testing.Client;
|
||||
pub const Client = *Testing.Client;
|
||||
});
|
||||
|
||||
const TestContext = struct {
|
||||
@@ -106,15 +140,39 @@ const TestContext = struct {
|
||||
self.client = Client.init(self.arena.allocator());
|
||||
// Don't use the arena here. We want to detect leaks in CDP.
|
||||
// The arena is only for test-specific stuff
|
||||
self.cdp_ = TestCDP.init(std.testing.allocator, &self.client.?, "dummy-loop");
|
||||
self.cdp_ = TestCDP.init(std.testing.allocator, &self.client.?, {});
|
||||
}
|
||||
return &self.cdp_.?;
|
||||
}
|
||||
|
||||
const BrowserContextOpts = struct {
|
||||
id: ?[]const u8 = null,
|
||||
session_id: ?[]const u8 = null,
|
||||
};
|
||||
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) {
|
||||
var c = self.cdp();
|
||||
if (c.browser_context) |*bc| {
|
||||
bc.deinit();
|
||||
c.browser_context = null;
|
||||
}
|
||||
|
||||
_ = try c.createBrowserContext();
|
||||
var bc = &c.browser_context.?;
|
||||
|
||||
if (opts.id) |id| {
|
||||
bc.id = id;
|
||||
}
|
||||
|
||||
if (opts.session_id) |sid| {
|
||||
bc.session_id = sid;
|
||||
}
|
||||
return bc;
|
||||
}
|
||||
|
||||
pub fn processMessage(self: *TestContext, msg: anytype) !void {
|
||||
var json_message: []const u8 = undefined;
|
||||
if (@typeInfo(@TypeOf(msg)) != .Pointer) {
|
||||
json_message = try std.json.stringifyAlloc(self.arena.allocator(), msg, .{});
|
||||
json_message = try json.stringifyAlloc(self.arena.allocator(), msg, .{});
|
||||
} else {
|
||||
// assume this is a string we want to send as-is, if it isn't, we'll
|
||||
// get a compile error, so no big deal.
|
||||
@@ -132,34 +190,71 @@ const TestContext = struct {
|
||||
index: ?usize = null,
|
||||
session_id: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
pub fn expectSentResult(self: *TestContext, expected: anytype, opts: ExpectResultOpts) !void {
|
||||
const expected_result = .{
|
||||
.id = opts.id,
|
||||
.result = expected,
|
||||
.result = if (comptime @typeInfo(@TypeOf(expected)) == .Null) struct {}{} else expected,
|
||||
.sessionId = opts.session_id,
|
||||
};
|
||||
|
||||
const serialized = try json.stringifyAlloc(self.arena.allocator(), expected_result, .{
|
||||
try self.expectSent(expected_result, .{ .index = opts.index });
|
||||
}
|
||||
|
||||
const ExpectEventOpts = struct {
|
||||
index: ?usize = null,
|
||||
session_id: ?[]const u8 = null,
|
||||
};
|
||||
pub fn expectSentEvent(self: *TestContext, method: []const u8, params: anytype, opts: ExpectEventOpts) !void {
|
||||
const expected_event = .{
|
||||
.method = method,
|
||||
.params = if (comptime @typeInfo(@TypeOf(params)) == .Null) struct {}{} else params,
|
||||
.sessionId = opts.session_id,
|
||||
};
|
||||
|
||||
try self.expectSent(expected_event, .{ .index = opts.index });
|
||||
}
|
||||
|
||||
const ExpectErrorOpts = struct {
|
||||
id: ?usize = null,
|
||||
index: ?usize = null,
|
||||
};
|
||||
pub fn expectSentError(self: *TestContext, code: i32, message: []const u8, opts: ExpectErrorOpts) !void {
|
||||
const expected_message = .{
|
||||
.id = opts.id,
|
||||
.code = code,
|
||||
.message = message,
|
||||
};
|
||||
try self.expectSent(expected_message, .{ .index = opts.index });
|
||||
}
|
||||
|
||||
const SentOpts = struct {
|
||||
index: ?usize = null,
|
||||
};
|
||||
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
|
||||
const serialized = try json.stringifyAlloc(self.arena.allocator(), expected, .{
|
||||
.whitespace = .indent_2,
|
||||
.emit_null_optional_fields = false,
|
||||
});
|
||||
|
||||
for (self.client.?.sent.items, 0..) |sent, i| {
|
||||
if (std.mem.eql(u8, sent, serialized) == false) {
|
||||
if (try compareExpectedToSent(serialized, sent) == false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (opts.index) |expected_index| {
|
||||
if (expected_index != i) {
|
||||
return error.MessageAtWrongIndex;
|
||||
return error.ErrorAtWrongIndex;
|
||||
}
|
||||
return;
|
||||
}
|
||||
_ = self.client.?.sent.orderedRemove(i);
|
||||
_ = self.client.?.serialized.orderedRemove(i);
|
||||
return;
|
||||
}
|
||||
std.debug.print("Message not found. Expecting:\n{s}\n\nGot:\n", .{serialized});
|
||||
for (self.client.?.sent.items, 0..) |sent, i| {
|
||||
std.debug.print("Error not found. Expecting:\n{s}\n\nGot:\n", .{serialized});
|
||||
for (self.client.?.serialized.items, 0..) |sent, i| {
|
||||
std.debug.print("#{d}\n{s}\n\n", .{ i, sent });
|
||||
}
|
||||
return error.MessageNotFound;
|
||||
return error.ErrorNotFound;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -168,3 +263,152 @@ pub fn context() TestContext {
|
||||
.arena = std.heap.ArenaAllocator.init(std.testing.allocator),
|
||||
};
|
||||
}
|
||||
|
||||
// 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 expection 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 compareJsonValues(expected_value.value, actual);
|
||||
}
|
||||
|
||||
fn compareJsonValues(a: std.json.Value, b: std.json.Value) bool {
|
||||
if (!std.mem.eql(u8, @tagName(a), @tagName(b))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (a) {
|
||||
.null => return true,
|
||||
.bool => return a.bool == b.bool,
|
||||
.integer => return a.integer == b.integer,
|
||||
.float => return a.float == b.float,
|
||||
.number_string => return std.mem.eql(u8, a.number_string, b.number_string),
|
||||
.string => return std.mem.eql(u8, a.string, b.string),
|
||||
.array => {
|
||||
const a_len = a.array.items.len;
|
||||
const b_len = b.array.items.len;
|
||||
if (a_len != b_len) {
|
||||
return false;
|
||||
}
|
||||
for (a.array.items, b.array.items) |a_item, b_item| {
|
||||
if (compareJsonValues(a_item, b_item) == false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
.object => {
|
||||
var it = a.object.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const key = entry.key_ptr.*;
|
||||
if (b.object.get(key)) |b_item| {
|
||||
if (compareJsonValues(entry.value_ptr.*, b_item) == false) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// fn compareAnyToJsonValue(expected: anytype, actual: json.Value) bool {
|
||||
// switch (@typeInfo(@TypeOf(expected))) {
|
||||
// .Optional => {
|
||||
// if (expected) |e| {
|
||||
// return compareAnyToJsonValue(e, actual);
|
||||
// }
|
||||
// return actual == .null;
|
||||
// },
|
||||
// .Int, .ComptimeInt => {
|
||||
// if (actual != .integer) {
|
||||
// return false;
|
||||
// }
|
||||
// return expected == actual.integer;
|
||||
// },
|
||||
// .Float, .ComptimeFloat => {
|
||||
// if (actual != .float) {
|
||||
// return false;
|
||||
// }
|
||||
// return expected == actual.float;
|
||||
// },
|
||||
// .Bool => {
|
||||
// if (actual != .bool) {
|
||||
// return false;
|
||||
// }
|
||||
// return expected == actual.bool;
|
||||
// },
|
||||
// .Pointer => |ptr| switch (ptr.size) {
|
||||
// .One => switch (@typeInfo(ptr.child)) {
|
||||
// .Struct => return compareAnyToJsonValue(expected.*, actual),
|
||||
// .Array => |arr| if (arr.child == u8) {
|
||||
// if (actual != .string) {
|
||||
// return false;
|
||||
// }
|
||||
// return std.mem.eql(u8, expected, actual.string);
|
||||
// },
|
||||
// else => {},
|
||||
// },
|
||||
// .Slice => switch (ptr.child) {
|
||||
// u8 => {
|
||||
// if (actual != .string) {
|
||||
// return false;
|
||||
// }
|
||||
// return std.mem.eql(u8, expected, actual.string);
|
||||
// },
|
||||
// else => {},
|
||||
// },
|
||||
// else => {},
|
||||
// },
|
||||
// .Struct => |s| {
|
||||
// if (s.is_tuple) {
|
||||
// // how an array might look in an anytype
|
||||
// if (actual != .array) {
|
||||
// return false;
|
||||
// }
|
||||
// if (s.fields.len != actual.array.items.len) {
|
||||
// return false;
|
||||
// }
|
||||
|
||||
// inline for (s.fields, 0..) |f, i| {
|
||||
// const e = @field(expected, f.name);
|
||||
// if (compareAnyToJsonValue(e, actual.array.items[i]) == false) {
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// }
|
||||
|
||||
// if (s.fields.len == 0) {
|
||||
// return (actual == .array and actual.array.items.len == 0);
|
||||
// }
|
||||
|
||||
// if (actual != .object) {
|
||||
// return false;
|
||||
// }
|
||||
// inline for (s.fields) |f| {
|
||||
// const e = @field(expected, f.name);
|
||||
// if (actual.object.get(f.name)) |a| {
|
||||
// if (compareAnyToJsonValue(e, a) == false) {
|
||||
// return false;
|
||||
// }
|
||||
// } else if (@typeInfo(f.type) != .Optional or e != null) {
|
||||
// // We don't JSON serialize nulls. So if we're expecting
|
||||
// // a null, that should show up as a missing field.
|
||||
// return false;
|
||||
// }
|
||||
// }
|
||||
// return true;
|
||||
// },
|
||||
// else => {},
|
||||
// }
|
||||
// @compileError("Can't compare " ++ @typeName(@TypeOf(expected)));
|
||||
// }
|
||||
|
||||
12
src/id.zig
12
src/id.zig
@@ -9,7 +9,7 @@ const std = @import("std");
|
||||
// - while incrementor is valid
|
||||
// - until the next call to next()
|
||||
// On the positive, it's zero allocation
|
||||
fn Incrementing(comptime T: type, comptime prefix: []const u8) type {
|
||||
pub fn Incrementing(comptime T: type, comptime prefix: []const u8) type {
|
||||
// +1 for the '-' separator
|
||||
const NUMERIC_START = prefix.len + 1;
|
||||
const MAX_BYTES = NUMERIC_START + switch (T) {
|
||||
@@ -35,15 +35,15 @@ fn Incrementing(comptime T: type, comptime prefix: []const u8) type {
|
||||
const PREFIX_INT_CODE: PrefixIntType = @bitCast(buffer[0..NUMERIC_START].*);
|
||||
|
||||
return struct {
|
||||
current: T = 0,
|
||||
counter: T = 0,
|
||||
buffer: [MAX_BYTES]u8 = buffer,
|
||||
|
||||
const Self = @This();
|
||||
|
||||
pub fn next(self: *Self) []const u8 {
|
||||
const current = self.current;
|
||||
const n = current +% 1;
|
||||
defer self.current = n;
|
||||
const counter = self.counter;
|
||||
const n = counter +% 1;
|
||||
defer self.counter = n;
|
||||
|
||||
const size = std.fmt.formatIntBuf(self.buffer[NUMERIC_START..], n, 10, .lower, .{});
|
||||
return self.buffer[0 .. NUMERIC_START + size];
|
||||
@@ -106,7 +106,7 @@ test "id: Incrementing.next" {
|
||||
try testing.expectEqualStrings("IDX-3", id.next());
|
||||
|
||||
// force a wrap
|
||||
id.current = 65533;
|
||||
id.counter = 65533;
|
||||
try testing.expectEqualStrings("IDX-65534", id.next());
|
||||
try testing.expectEqualStrings("IDX-65535", id.next());
|
||||
try testing.expectEqualStrings("IDX-0", id.next());
|
||||
|
||||
Reference in New Issue
Block a user