Make CDP less generic.

It's still generic over the client - we need to assert messages written to and
be able to send specific commands, but it's no longer generic over Browser/
Session/Page/etc..
This commit is contained in:
Karl Seguin
2025-04-24 18:06:55 +08:00
parent 332508f563
commit 1fca035cfe
4 changed files with 18 additions and 208 deletions

View File

@@ -97,7 +97,7 @@ pub const Browser = struct {
return session; return session;
} }
fn closeSession(self: *Browser) void { pub fn closeSession(self: *Browser) void {
if (self.session) |session| { if (self.session) |session| {
session.deinit(); session.deinit();
self.session_pool.destroy(session); self.session_pool.destroy(session);

View File

@@ -22,6 +22,8 @@ const json = std.json;
const App = @import("../app.zig").App; const App = @import("../app.zig").App;
const asUint = @import("../str/parser.zig").asUint; const asUint = @import("../str/parser.zig").asUint;
const Browser = @import("../browser/browser.zig").Browser;
const Session = @import("../browser/browser.zig").Session;
const Incrementing = @import("../id.zig").Incrementing; const Incrementing = @import("../id.zig").Incrementing;
const Notification = @import("../notification.zig").Notification; const Notification = @import("../notification.zig").Notification;
@@ -32,8 +34,6 @@ pub const LOADER_ID = "LOADERID24DD2FD56CF1EF33C965C79C";
pub const CDP = CDPT(struct { pub const CDP = CDPT(struct {
const Client = *@import("../server.zig").Client; 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 SessionIdGen = Incrementing(u32, "SID");
@@ -69,8 +69,6 @@ pub fn CDPT(comptime TypeProvider: type) type {
message_arena: std.heap.ArenaAllocator, message_arena: std.heap.ArenaAllocator,
const Self = @This(); const Self = @This();
pub const Browser = TypeProvider.Browser;
pub const Session = TypeProvider.Session;
pub fn init(app: *App, client: TypeProvider.Client) !Self { pub fn init(app: *App, client: TypeProvider.Client) !Self {
const allocator = app.allocator; const allocator = app.allocator;
@@ -247,6 +245,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
return false; return false;
} }
bc.deinit(); bc.deinit();
self.browser.closeSession();
self.browser_context_pool.destroy(bc); self.browser_context_pool.destroy(bc);
self.browser_context = null; self.browser_context = null;
return true; return true;
@@ -282,7 +281,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// all intents and purpose, from CDP's point of view our Browser and // 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 // our Session more or less maps to a BrowserContext. THIS HAS ZERO
// RELATION TO SESSION_ID // RELATION TO SESSION_ID
session: *CDP_T.Session, session: *Session,
// Points to the session arena // Points to the session arena
arena: Allocator, arena: Allocator,
@@ -309,7 +308,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
node_registry: Node.Registry, node_registry: Node.Registry,
node_search_list: Node.Search.List, node_search_list: Node.Search.List,
isolated_world: ?IsolatedWorld(CDP_T.Browser.EnvType), isolated_world: ?IsolatedWorld(Browser.EnvType),
const Self = @This(); const Self = @This();

View File

@@ -522,7 +522,7 @@ test "cdp.target: createTarget" {
try testing.expectEqual(true, bc.target_id != null); try testing.expectEqual(true, bc.target_id != null);
try testing.expectEqual( try testing.expectEqual(
\\{"isDefault":true,"type":"default","frameId":"TID-1"} \\{"isDefault":true,"type":"default","frameId":"TID-1"}
, bc.session.page.?.aux_data); , bc.session.aux_data);
try ctx.expectSentResult(.{ .targetId = bc.target_id.? }, .{ .id = 10 }); 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.targetCreated", .{ .targetInfo = .{ .url = "about:blank", .title = "about:blank", .attached = false, .type = "page", .canAccessOpener = false, .browserContextId = "BID-9", .targetId = bc.target_id.? } }, .{});

View File

@@ -19,6 +19,7 @@
const std = @import("std"); const std = @import("std");
const json = std.json; const json = std.json;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Testing = @This(); const Testing = @This();
@@ -36,196 +37,6 @@ pub const expectEqualSlices = base.expectEqualSlices;
pub const Document = @import("../testing.zig").Document; pub const Document = @import("../testing.zig").Document;
const Browser = struct {
session: ?*Session = null,
arena: std.heap.ArenaAllocator,
env: Env,
pub const EnvType = Env;
pub fn init(app: *App) !Browser {
return .{
.arena = std.heap.ArenaAllocator.init(app.allocator),
.env = Env{},
};
}
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;
}
const arena = self.arena.allocator();
const executor = arena.create(Env.Executor) catch unreachable;
self.session = try arena.create(Session);
self.session.?.* = .{
.page = null,
.arena = self.arena,
.executor = executor,
.inspector = .{},
.state = 0,
};
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);
}
pub fn runMicrotasks(_: *const Browser) void {}
};
const Session = struct {
page: ?Page = null,
arena: std.heap.ArenaAllocator,
executor: *Env.Executor,
inspector: Inspector,
state: i32,
pub fn currentPage(self: *Session) ?*Page {
return &(self.page orelse return null);
}
pub fn createPage(self: *Session, aux_data: ?[]const u8) !*Page {
if (self.page != null) {
return error.MockBrowserPageAlreadyExists;
}
self.page = .{
.session = self,
.url = URL.parse("https://lightpanda.io/", null) catch unreachable,
.aux_data = try self.arena.allocator().dupe(u8, aux_data orelse ""),
};
return &self.page.?;
}
pub fn removePage(self: *Session) void {
self.page = null;
}
pub fn callInspector(self: *Session, msg: []const u8) void {
_ = self;
_ = msg;
}
};
const Env = struct {
pub const Executor = MockExecutor;
pub fn startExecutor(self: *Env, comptime Global: type, state: anytype, module_loader: anytype, kind: anytype) !*Executor {
_ = self;
_ = Global;
_ = state;
_ = module_loader;
_ = kind;
return error.MockExecutor;
}
pub fn stopExecutor(self: *Env, executor: *Executor) void {
_ = self;
_ = executor;
}
};
const MockExecutor = struct {
context: Context,
pub fn startScope(self: *MockExecutor, global: anytype) !void {
_ = self;
_ = global;
}
pub fn endScope(self: *MockExecutor) void {
_ = self;
}
};
const Context = struct {
pub fn debugContextId(self: Context) i32 {
_ = self;
return 0;
}
};
const Inspector = struct {
pub fn getRemoteObject(
self: *const Inspector,
executor: *Env.Executor,
group: []const u8,
value: anytype,
) !RemoteObject {
_ = self;
_ = executor;
_ = group;
_ = value;
return RemoteObject{};
}
pub fn getNodePtr(self: Inspector, alloc: std.mem.Allocator, object_id: []const u8) !?*anyopaque {
_ = self;
_ = object_id;
return try alloc.create(i32);
}
pub fn contextCreated(
self: *const Inspector,
executor: *const Env.Executor,
name: []const u8,
origin: []const u8,
aux_data: ?[]const u8,
is_default_context: bool,
) void {
_ = self;
_ = executor;
_ = name;
_ = origin;
_ = aux_data;
_ = is_default_context;
}
};
const RemoteObject = struct {
pub fn deinit(self: RemoteObject) void {
_ = self;
}
pub fn getType(self: RemoteObject, alloc: std.mem.Allocator) ![:0]const u8 {
_ = self;
_ = alloc;
return "TheType";
}
pub fn getSubtype(self: RemoteObject, alloc: std.mem.Allocator) ![:0]const u8 {
_ = self;
_ = alloc;
return "TheSubtype";
}
pub fn getClassName(self: RemoteObject, alloc: std.mem.Allocator) ![:0]const u8 {
_ = self;
_ = alloc;
return "TheClassName";
}
pub fn getDescription(self: RemoteObject, alloc: std.mem.Allocator) ![:0]const u8 {
_ = self;
_ = alloc;
return "TheDescription";
}
pub fn getObjectId(self: RemoteObject, alloc: std.mem.Allocator) ![:0]const u8 {
_ = self;
_ = alloc;
return "TheObjectId";
}
};
const Page = struct {
session: *Session,
url: ?URL = null,
aux_data: []const u8 = "",
doc: ?*parser.Document = null,
pub fn navigate(_: *Page, url: URL, opts: anytype) !void {
_ = url;
_ = opts;
}
const MouseEvent = @import("../browser/browser.zig").Page.MouseEvent;
pub fn mouseEvent(_: *Page, _: MouseEvent) !void {}
};
const Client = struct { const Client = struct {
allocator: Allocator, allocator: Allocator,
sent: std.ArrayListUnmanaged(json.Value) = .{}, sent: std.ArrayListUnmanaged(json.Value) = .{},
@@ -246,11 +57,14 @@ const Client = struct {
const value = try json.parseFromSliceLeaky(json.Value, self.allocator, serialized, .{}); const value = try json.parseFromSliceLeaky(json.Value, self.allocator, serialized, .{});
try self.sent.append(self.allocator, value); try self.sent.append(self.allocator, value);
} }
pub fn sendJSONRaw(self: *Client, _: ArenaAllocator, buf: std.ArrayListUnmanaged(u8)) !void {
const value = try json.parseFromSliceLeaky(json.Value, self.allocator, buf.items, .{});
try self.sent.append(self.allocator, value);
}
}; };
const TestCDP = main.CDPT(struct { const TestCDP = main.CDPT(struct {
pub const Browser = Testing.Browser;
pub const Session = Testing.Session;
pub const Client = *Testing.Client; pub const Client = *Testing.Client;
}); });
@@ -258,7 +72,7 @@ const TestContext = struct {
app: *App, app: *App,
client: ?Client = null, client: ?Client = null,
cdp_: ?TestCDP = null, cdp_: ?TestCDP = null,
arena: std.heap.ArenaAllocator, arena: ArenaAllocator,
pub fn deinit(self: *TestContext) void { pub fn deinit(self: *TestContext) void {
if (self.cdp_) |*c| { if (self.cdp_) |*c| {
@@ -273,7 +87,7 @@ const TestContext = struct {
self.client = Client.init(self.arena.allocator()); self.client = Client.init(self.arena.allocator());
// Don't use the arena here. We want to detect leaks in CDP. // Don't use the arena here. We want to detect leaks in CDP.
// The arena is only for test-specific stuff // The arena is only for test-specific stuff
self.cdp_ = try TestCDP.init(self.app, &self.client.?); self.cdp_ = TestCDP.init(self.app, &self.client.?) catch unreachable;
} }
return &self.cdp_.?; return &self.cdp_.?;
} }
@@ -286,11 +100,8 @@ const TestContext = struct {
}; };
pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) { pub fn loadBrowserContext(self: *TestContext, opts: BrowserContextOpts) !*main.BrowserContext(TestCDP) {
var c = self.cdp(); var c = self.cdp();
c.browser.session = null;
if (c.browser_context) |bc| { if (c.browser_context) |bc| {
bc.deinit(); _ = c.disposeBrowserContext(bc.id);
c.browser_context = null;
} }
_ = try c.createBrowserContext(); _ = try c.createBrowserContext();
@@ -410,7 +221,7 @@ const TestContext = struct {
pub fn context() TestContext { pub fn context() TestContext {
return .{ return .{
.app = App.init(std.testing.allocator, .{ .run_mode = .serve }) catch unreachable, .app = App.init(std.testing.allocator, .{ .run_mode = .serve }) catch unreachable,
.arena = std.heap.ArenaAllocator.init(std.testing.allocator), .arena = ArenaAllocator.init(std.testing.allocator),
}; };
} }
@@ -420,7 +231,7 @@ pub fn context() TestContext {
// json and check if the two are equal. // json and check if the two are equal.
// Except serializing to JSON isn't deterministic. // Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can // 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 // compare our anytype expectation with the json.Value that we captured
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool { fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{}); const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});