mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
CDP: add waitForSelector to lp.actions
It refactors the implementation from MCP to be reused.
This commit is contained in:
@@ -23,6 +23,7 @@ const Element = @import("webapi/Element.zig");
|
|||||||
const Event = @import("webapi/Event.zig");
|
const Event = @import("webapi/Event.zig");
|
||||||
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||||
const Page = @import("Page.zig");
|
const Page = @import("Page.zig");
|
||||||
|
const Selector = @import("webapi/selector/Selector.zig");
|
||||||
|
|
||||||
pub fn click(node: *DOMNode, page: *Page) !void {
|
pub fn click(node: *DOMNode, page: *Page) !void {
|
||||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||||
@@ -102,3 +103,24 @@ pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn waitForSelector(selector: [:0]const u8, timeout_ms: u32, page: *Page) !*DOMNode {
|
||||||
|
var timer = try std.time.Timer.start();
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const element = Selector.querySelector(page.document.asNode(), selector, page) catch {
|
||||||
|
return error.InvalidSelector;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (element) |el| {
|
||||||
|
return el.asNode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
||||||
|
if (elapsed >= timeout_ms) {
|
||||||
|
return error.Timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = page._session.wait(.{ .timeout_ms = @min(100, timeout_ms - elapsed) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
clickNode,
|
clickNode,
|
||||||
fillNode,
|
fillNode,
|
||||||
scrollNode,
|
scrollNode,
|
||||||
|
waitForSelector,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
@@ -45,6 +46,7 @@ pub fn processMessage(cmd: anytype) !void {
|
|||||||
.clickNode => return clickNode(cmd),
|
.clickNode => return clickNode(cmd),
|
||||||
.fillNode => return fillNode(cmd),
|
.fillNode => return fillNode(cmd),
|
||||||
.scrollNode => return scrollNode(cmd),
|
.scrollNode => return scrollNode(cmd),
|
||||||
|
.waitForSelector => return waitForSelector(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +232,32 @@ fn scrollNode(cmd: anytype) !void {
|
|||||||
|
|
||||||
return cmd.sendResult(.{}, .{});
|
return cmd.sendResult(.{}, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn waitForSelector(cmd: anytype) !void {
|
||||||
|
const Params = struct {
|
||||||
|
selector: []const u8,
|
||||||
|
timeout: ?u32 = null,
|
||||||
|
};
|
||||||
|
const params = (try cmd.params(Params)) orelse return error.InvalidParam;
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return error.NoBrowserContext;
|
||||||
|
const page = bc.session.currentPage() orelse return error.PageNotLoaded;
|
||||||
|
|
||||||
|
const timeout_ms = params.timeout orelse 5000;
|
||||||
|
const selector_z = try cmd.arena.dupeZ(u8, params.selector);
|
||||||
|
|
||||||
|
const node = lp.actions.waitForSelector(selector_z, timeout_ms, page) catch |err| {
|
||||||
|
if (err == error.InvalidSelector) return error.InvalidParam;
|
||||||
|
if (err == error.Timeout) return error.InternalError;
|
||||||
|
return error.InternalError;
|
||||||
|
};
|
||||||
|
|
||||||
|
const registered = try bc.node_registry.register(node);
|
||||||
|
return cmd.sendResult(.{
|
||||||
|
.backendNodeId = registered.id,
|
||||||
|
}, .{});
|
||||||
|
}
|
||||||
|
|
||||||
const testing = @import("../testing.zig");
|
const testing = @import("../testing.zig");
|
||||||
test "cdp.lp: getMarkdown" {
|
test "cdp.lp: getMarkdown" {
|
||||||
var ctx = testing.context();
|
var ctx = testing.context();
|
||||||
@@ -339,3 +367,43 @@ test "cdp.lp: action tools" {
|
|||||||
|
|
||||||
try testing.expect(result.isTrue());
|
try testing.expect(result.isTrue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "cdp.lp: waitForSelector" {
|
||||||
|
var ctx = testing.context();
|
||||||
|
defer ctx.deinit();
|
||||||
|
|
||||||
|
const bc = try ctx.loadBrowserContext(.{});
|
||||||
|
const page = try bc.session.createPage();
|
||||||
|
const url = "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html";
|
||||||
|
try page.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } });
|
||||||
|
_ = bc.session.wait(.{});
|
||||||
|
|
||||||
|
// 1. Existing element
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 1,
|
||||||
|
.method = "LP.waitForSelector",
|
||||||
|
.params = .{ .selector = "#existing", .timeout = 2000 },
|
||||||
|
});
|
||||||
|
var result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
||||||
|
try testing.expect(result.get("backendNodeId") != null);
|
||||||
|
ctx.client.?.sent.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// 2. Delayed element
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 2,
|
||||||
|
.method = "LP.waitForSelector",
|
||||||
|
.params = .{ .selector = "#delayed", .timeout = 5000 },
|
||||||
|
});
|
||||||
|
result = ctx.client.?.sent.items[0].object.get("result").?.object;
|
||||||
|
try testing.expect(result.get("backendNodeId") != null);
|
||||||
|
ctx.client.?.sent.clearRetainingCapacity();
|
||||||
|
|
||||||
|
// 3. Timeout error
|
||||||
|
try ctx.processMessage(.{
|
||||||
|
.id = 3,
|
||||||
|
.method = "LP.waitForSelector",
|
||||||
|
.params = .{ .selector = "#nonexistent", .timeout = 100 },
|
||||||
|
});
|
||||||
|
const err_obj = ctx.client.?.sent.items[0].object.get("error").?.object;
|
||||||
|
try testing.expect(err_obj.get("code") != null);
|
||||||
|
}
|
||||||
|
|||||||
@@ -561,28 +561,21 @@ fn handleWaitForSelector(server: *Server, arena: std.mem.Allocator, id: std.json
|
|||||||
};
|
};
|
||||||
|
|
||||||
const timeout_ms = args.timeout orelse 5000;
|
const timeout_ms = args.timeout orelse 5000;
|
||||||
var timer = try std.time.Timer.start();
|
|
||||||
|
|
||||||
while (true) {
|
const node = lp.actions.waitForSelector(args.selector, timeout_ms, page) catch |err| {
|
||||||
const element = Selector.querySelector(page.document.asNode(), args.selector, page) catch {
|
if (err == error.InvalidSelector) {
|
||||||
return server.sendError(id, .InvalidParams, "Invalid selector");
|
return server.sendError(id, .InvalidParams, "Invalid selector");
|
||||||
|
} else if (err == error.Timeout) {
|
||||||
|
return server.sendError(id, .InternalError, "Timeout waiting for selector");
|
||||||
|
}
|
||||||
|
return server.sendError(id, .InternalError, "Failed waiting for selector");
|
||||||
};
|
};
|
||||||
|
|
||||||
if (element) |el| {
|
const registered = try server.node_registry.register(node);
|
||||||
const registered = try server.node_registry.register(el.asNode());
|
|
||||||
const msg = std.fmt.allocPrint(arena, "Element found. backendNodeId: {d}", .{registered.id}) catch "Element found.";
|
const msg = std.fmt.allocPrint(arena, "Element found. backendNodeId: {d}", .{registered.id}) catch "Element found.";
|
||||||
|
|
||||||
const content = [_]protocol.TextContent([]const u8){.{ .text = msg }};
|
const content = [_]protocol.TextContent([]const u8){.{ .text = msg }};
|
||||||
return server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
return server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||||
}
|
|
||||||
|
|
||||||
const elapsed: u32 = @intCast(timer.read() / std.time.ns_per_ms);
|
|
||||||
if (elapsed >= timeout_ms) {
|
|
||||||
return server.sendError(id, .InternalError, "Timeout waiting for selector");
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = server.session.wait(.{ .timeout_ms = @min(100, timeout_ms - elapsed) });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T {
|
fn parseArguments(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Value, server: *Server, id: std.json.Value, tool_name: []const u8) !T {
|
||||||
|
|||||||
Reference in New Issue
Block a user