mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-04-03 16:10:29 +00:00
Compare commits
1 Commits
mcp-new-ac
...
sqlite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fd5c36ff2 |
22
build.zig
22
build.zig
@@ -89,6 +89,28 @@ pub fn build(b: *Build) !void {
|
||||
break :blk mod;
|
||||
};
|
||||
|
||||
lightpanda_module.addCSourceFile(.{
|
||||
.file = b.path("lib/sqlite3/sqlite3.c"),
|
||||
.flags = &[_][]const u8{
|
||||
"-DSQLITE_DQS=0",
|
||||
"-DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1",
|
||||
"-DSQLITE_USE_ALLOCA=1",
|
||||
"-DSQLITE_THREADSAFE=1",
|
||||
"-DSQLITE_TEMP_STORE=3",
|
||||
"-DSQLITE_ENABLE_API_ARMOR=1",
|
||||
"-DSQLITE_ENABLE_UNLOCK_NOTIFY",
|
||||
"-DSQLITE_DEFAULT_FILE_PERMISSIONS=0600",
|
||||
"-DSQLITE_OMIT_DECLTYPE=1",
|
||||
"-DSQLITE_OMIT_DEPRECATED=1",
|
||||
"-DSQLITE_OMIT_LOAD_EXTENSION=1",
|
||||
"-DSQLITE_OMIT_PROGRESS_CALLBACK=1",
|
||||
"-DSQLITE_OMIT_SHARED_CACHE",
|
||||
"-DSQLITE_OMIT_TRACE=1",
|
||||
"-DSQLITE_OMIT_UTF16=1",
|
||||
"-DHAVE_USLEEP=0",
|
||||
},
|
||||
});
|
||||
|
||||
// Check compilation
|
||||
const check = b.step("check", "Check if lightpanda compiles");
|
||||
|
||||
|
||||
265977
lib/sqlite3/sqlite3.c
Normal file
265977
lib/sqlite3/sqlite3.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,23 +22,10 @@ const DOMNode = @import("webapi/Node.zig");
|
||||
const Element = @import("webapi/Element.zig");
|
||||
const Event = @import("webapi/Event.zig");
|
||||
const MouseEvent = @import("webapi/event/MouseEvent.zig");
|
||||
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
|
||||
const Page = @import("Page.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Selector = @import("webapi/selector/Selector.zig");
|
||||
|
||||
fn dispatchInputAndChangeEvents(el: *Element, page: *Page) !void {
|
||||
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
|
||||
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
|
||||
};
|
||||
|
||||
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
|
||||
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn click(node: *DOMNode, page: *Page) !void {
|
||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||
|
||||
@@ -56,107 +43,9 @@ pub fn click(node: *DOMNode, page: *Page) !void {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn hover(node: *DOMNode, page: *Page) !void {
|
||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||
|
||||
const mouseover_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseover"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
}, page);
|
||||
|
||||
page._event_manager.dispatch(el.asEventTarget(), mouseover_event.asEvent()) catch |err| {
|
||||
lp.log.err(.app, "hover mouseover failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
|
||||
const mouseenter_event: *MouseEvent = try .initTrusted(comptime .wrap("mouseenter"), .{
|
||||
.composed = true,
|
||||
}, page);
|
||||
|
||||
page._event_manager.dispatch(el.asEventTarget(), mouseenter_event.asEvent()) catch |err| {
|
||||
lp.log.err(.app, "hover mouseenter failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn press(node: ?*DOMNode, key: []const u8, page: *Page) !void {
|
||||
const target = if (node) |n|
|
||||
(n.is(Element) orelse return error.InvalidNodeType).asEventTarget()
|
||||
else
|
||||
page.document.asNode().asEventTarget();
|
||||
|
||||
const keydown_event: *KeyboardEvent = try .initTrusted(comptime .wrap("keydown"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.key = key,
|
||||
}, page);
|
||||
|
||||
page._event_manager.dispatch(target, keydown_event.asEvent()) catch |err| {
|
||||
lp.log.err(.app, "press keydown failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
|
||||
const keyup_event: *KeyboardEvent = try .initTrusted(comptime .wrap("keyup"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
.key = key,
|
||||
}, page);
|
||||
|
||||
page._event_manager.dispatch(target, keyup_event.asEvent()) catch |err| {
|
||||
lp.log.err(.app, "press keyup failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn selectOption(node: *DOMNode, value: []const u8, page: *Page) !void {
|
||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||
const select = el.is(Element.Html.Select) orelse return error.InvalidNodeType;
|
||||
|
||||
select.setValue(value, page) catch |err| {
|
||||
lp.log.err(.app, "select setValue failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
|
||||
try dispatchInputAndChangeEvents(el, page);
|
||||
}
|
||||
|
||||
pub fn setChecked(node: *DOMNode, checked: bool, page: *Page) !void {
|
||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||
const input = el.is(Element.Html.Input) orelse return error.InvalidNodeType;
|
||||
|
||||
if (input._input_type != .checkbox and input._input_type != .radio) {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
input.setChecked(checked, page) catch |err| {
|
||||
lp.log.err(.app, "setChecked failed", .{ .err = err });
|
||||
return error.ActionFailed;
|
||||
};
|
||||
|
||||
// Match browser event order: click fires first, then input and change.
|
||||
const click_event: *MouseEvent = try .initTrusted(comptime .wrap("click"), .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.composed = true,
|
||||
}, page);
|
||||
|
||||
page._event_manager.dispatch(el.asEventTarget(), click_event.asEvent()) catch |err| {
|
||||
lp.log.err(.app, "dispatch click event failed", .{ .err = err });
|
||||
};
|
||||
|
||||
try dispatchInputAndChangeEvents(el, page);
|
||||
}
|
||||
|
||||
pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
|
||||
const el = node.is(Element) orelse return error.InvalidNodeType;
|
||||
|
||||
el.focus(page) catch |err| {
|
||||
lp.log.err(.app, "fill focus failed", .{ .err = err });
|
||||
};
|
||||
|
||||
if (el.is(Element.Html.Input)) |input| {
|
||||
input.setValue(text, page) catch |err| {
|
||||
lp.log.err(.app, "fill input failed", .{ .err = err });
|
||||
@@ -176,7 +65,15 @@ pub fn fill(node: *DOMNode, text: []const u8, page: *Page) !void {
|
||||
return error.InvalidNodeType;
|
||||
}
|
||||
|
||||
try dispatchInputAndChangeEvents(el, page);
|
||||
const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, page);
|
||||
page._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch input event failed", .{ .err = err });
|
||||
};
|
||||
|
||||
const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, page);
|
||||
page._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| {
|
||||
lp.log.err(.app, "dispatch change event failed", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, page: *Page) !void {
|
||||
|
||||
@@ -10,20 +10,5 @@
|
||||
<div id="scrollbox" style="width: 100px; height: 100px; overflow: scroll;" onscroll="window.scrolled = true;">
|
||||
<div style="height: 500px;">Long content</div>
|
||||
</div>
|
||||
<div id="hoverTarget" onmouseover="window.hovered = true;">Hover Me</div>
|
||||
<input id="keyTarget" onkeydown="window.keyPressed = event.key;" onkeyup="window.keyReleased = event.key;">
|
||||
<select id="sel2" onchange="window.sel2Changed = this.value">
|
||||
<option value="a">Alpha</option>
|
||||
<option value="b">Beta</option>
|
||||
<option value="c">Gamma</option>
|
||||
</select>
|
||||
<input id="chk" type="checkbox">
|
||||
<input id="rad" type="radio" name="group1">
|
||||
<script>
|
||||
document.getElementById('chk').addEventListener('click', function() { window.chkClicked = true; });
|
||||
document.getElementById('chk').addEventListener('change', function() { window.chkChanged = true; });
|
||||
document.getElementById('rad').addEventListener('click', function() { window.radClicked = true; });
|
||||
document.getElementById('rad').addEventListener('change', function() { window.radChanged = true; });
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
157
src/main.zig
157
src/main.zig
@@ -43,6 +43,8 @@ pub fn main() !void {
|
||||
const main_arena = main_arena_instance.allocator();
|
||||
defer main_arena_instance.deinit();
|
||||
|
||||
try useSqlite3();
|
||||
|
||||
run(gpa, main_arena) catch |err| {
|
||||
log.fatal(.app, "exit", .{ .err = err });
|
||||
std.posix.exit(1);
|
||||
@@ -192,3 +194,158 @@ fn mcpThread(allocator: std.mem.Allocator, app: *App) void {
|
||||
log.fatal(.mcp, "mcp error", .{ .err = err });
|
||||
};
|
||||
}
|
||||
|
||||
fn useSqlite3() !void {
|
||||
const c = @cImport(@cInclude("sqlite3.h"));
|
||||
|
||||
const flags = c.SQLITE_OPEN_READWRITE;
|
||||
|
||||
var conn: ?*c.sqlite3 = null;
|
||||
{
|
||||
const rc = c.sqlite3_open_v2(":memory:", &conn, flags, null);
|
||||
if (rc != c.SQLITE_OK) {
|
||||
return sqlite3Error(rc);
|
||||
}
|
||||
}
|
||||
defer _ = c.sqlite3_close_v2(conn);
|
||||
|
||||
var stmt: ?*c.sqlite3_stmt = null;
|
||||
{
|
||||
const sql = "select sqlite_version()";
|
||||
var tail: [*:0]const u8 = undefined;
|
||||
const rc = c.sqlite3_prepare_v2(conn, sql, @intCast(sql.len), &stmt, @ptrCast(&tail));
|
||||
if (rc != c.SQLITE_OK) {
|
||||
return sqlite3Error(rc);
|
||||
}
|
||||
}
|
||||
defer _ = c.sqlite3_finalize(stmt);
|
||||
|
||||
{
|
||||
const rc = c.sqlite3_step(stmt);
|
||||
if (rc == c.SQLITE_DONE) {
|
||||
return error.NoRow;
|
||||
}
|
||||
if (rc != c.SQLITE_ROW) {
|
||||
return sqlite3Error(rc);
|
||||
}
|
||||
|
||||
const data = c.sqlite3_column_text(stmt, 0);
|
||||
const len = c.sqlite3_column_bytes(stmt, 0);
|
||||
if (len == 0) {
|
||||
return error.EmptyValue;
|
||||
}
|
||||
std.debug.print("sqlite version: {s}\n", .{@as([*c]const u8, @ptrCast(data))[0..@intCast(len)]});
|
||||
}
|
||||
}
|
||||
|
||||
fn sqlite3Error(result: c_int) !void {
|
||||
const c = @cImport(@cInclude("sqlite3.h"));
|
||||
return switch (result) {
|
||||
c.SQLITE_ABORT => error.Abort,
|
||||
c.SQLITE_AUTH => error.Auth,
|
||||
c.SQLITE_BUSY => error.Busy,
|
||||
c.SQLITE_CANTOPEN => error.CantOpen,
|
||||
c.SQLITE_CONSTRAINT => error.Constraint,
|
||||
c.SQLITE_CORRUPT => error.Corrupt,
|
||||
c.SQLITE_EMPTY => error.Empty,
|
||||
c.SQLITE_ERROR => error.Error,
|
||||
c.SQLITE_FORMAT => error.Format,
|
||||
c.SQLITE_FULL => error.Full,
|
||||
c.SQLITE_INTERNAL => error.Internal,
|
||||
c.SQLITE_INTERRUPT => error.Interrupt,
|
||||
c.SQLITE_IOERR => error.IoErr,
|
||||
c.SQLITE_LOCKED => error.Locked,
|
||||
c.SQLITE_MISMATCH => error.Mismatch,
|
||||
c.SQLITE_MISUSE => error.Misuse,
|
||||
c.SQLITE_NOLFS => error.NoLFS,
|
||||
c.SQLITE_NOMEM => error.NoMem,
|
||||
c.SQLITE_NOTADB => error.NotADB,
|
||||
c.SQLITE_NOTFOUND => error.Notfound,
|
||||
c.SQLITE_NOTICE => error.Notice,
|
||||
c.SQLITE_PERM => error.Perm,
|
||||
c.SQLITE_PROTOCOL => error.Protocol,
|
||||
c.SQLITE_RANGE => error.Range,
|
||||
c.SQLITE_READONLY => error.ReadOnly,
|
||||
c.SQLITE_SCHEMA => error.Schema,
|
||||
c.SQLITE_TOOBIG => error.TooBig,
|
||||
c.SQLITE_WARNING => error.Warning,
|
||||
|
||||
// extended codes:
|
||||
c.SQLITE_ERROR_MISSING_COLLSEQ => error.ErrorMissingCollseq,
|
||||
c.SQLITE_ERROR_RETRY => error.ErrorRetry,
|
||||
c.SQLITE_ERROR_SNAPSHOT => error.ErrorSnapshot,
|
||||
c.SQLITE_IOERR_READ => error.IoerrRead,
|
||||
c.SQLITE_IOERR_SHORT_READ => error.IoerrShortRead,
|
||||
c.SQLITE_IOERR_WRITE => error.IoerrWrite,
|
||||
c.SQLITE_IOERR_FSYNC => error.IoerrFsync,
|
||||
c.SQLITE_IOERR_DIR_FSYNC => error.IoerrDir_fsync,
|
||||
c.SQLITE_IOERR_TRUNCATE => error.IoerrTruncate,
|
||||
c.SQLITE_IOERR_FSTAT => error.IoerrFstat,
|
||||
c.SQLITE_IOERR_UNLOCK => error.IoerrUnlock,
|
||||
c.SQLITE_IOERR_RDLOCK => error.IoerrRdlock,
|
||||
c.SQLITE_IOERR_DELETE => error.IoerrDelete,
|
||||
c.SQLITE_IOERR_BLOCKED => error.IoerrBlocked,
|
||||
c.SQLITE_IOERR_NOMEM => error.IoerrNomem,
|
||||
c.SQLITE_IOERR_ACCESS => error.IoerrAccess,
|
||||
c.SQLITE_IOERR_CHECKRESERVEDLOCK => error.IoerrCheckreservedlock,
|
||||
c.SQLITE_IOERR_LOCK => error.IoerrLock,
|
||||
c.SQLITE_IOERR_CLOSE => error.IoerrClose,
|
||||
c.SQLITE_IOERR_DIR_CLOSE => error.IoerrDirClose,
|
||||
c.SQLITE_IOERR_SHMOPEN => error.IoerrShmopen,
|
||||
c.SQLITE_IOERR_SHMSIZE => error.IoerrShmsize,
|
||||
c.SQLITE_IOERR_SHMLOCK => error.IoerrShmlock,
|
||||
c.SQLITE_IOERR_SHMMAP => error.ioerrshmmap,
|
||||
c.SQLITE_IOERR_SEEK => error.IoerrSeek,
|
||||
c.SQLITE_IOERR_DELETE_NOENT => error.IoerrDeleteNoent,
|
||||
c.SQLITE_IOERR_MMAP => error.IoerrMmap,
|
||||
c.SQLITE_IOERR_GETTEMPPATH => error.IoerrGetTempPath,
|
||||
c.SQLITE_IOERR_CONVPATH => error.IoerrConvPath,
|
||||
c.SQLITE_IOERR_VNODE => error.IoerrVnode,
|
||||
c.SQLITE_IOERR_AUTH => error.IoerrAuth,
|
||||
c.SQLITE_IOERR_BEGIN_ATOMIC => error.IoerrBeginAtomic,
|
||||
c.SQLITE_IOERR_COMMIT_ATOMIC => error.IoerrCommitAtomic,
|
||||
c.SQLITE_IOERR_ROLLBACK_ATOMIC => error.IoerrRollbackAtomic,
|
||||
c.SQLITE_IOERR_DATA => error.IoerrData,
|
||||
c.SQLITE_IOERR_CORRUPTFS => error.IoerrCorruptFS,
|
||||
c.SQLITE_LOCKED_SHAREDCACHE => error.LockedSharedCache,
|
||||
c.SQLITE_LOCKED_VTAB => error.LockedVTab,
|
||||
c.SQLITE_BUSY_RECOVERY => error.BusyRecovery,
|
||||
c.SQLITE_BUSY_SNAPSHOT => error.BusySnapshot,
|
||||
c.SQLITE_BUSY_TIMEOUT => error.BusyTimeout,
|
||||
c.SQLITE_CANTOPEN_NOTEMPDIR => error.CantOpenNoTempDir,
|
||||
c.SQLITE_CANTOPEN_ISDIR => error.CantOpenIsDir,
|
||||
c.SQLITE_CANTOPEN_FULLPATH => error.CantOpenFullPath,
|
||||
c.SQLITE_CANTOPEN_CONVPATH => error.CantOpenConvPath,
|
||||
c.SQLITE_CANTOPEN_DIRTYWAL => error.CantOpenDirtyWal,
|
||||
c.SQLITE_CANTOPEN_SYMLINK => error.CantOpenSymlink,
|
||||
c.SQLITE_CORRUPT_VTAB => error.CorruptVTab,
|
||||
c.SQLITE_CORRUPT_SEQUENCE => error.CorruptSequence,
|
||||
c.SQLITE_CORRUPT_INDEX => error.CorruptIndex,
|
||||
c.SQLITE_READONLY_RECOVERY => error.ReadonlyRecovery,
|
||||
c.SQLITE_READONLY_CANTLOCK => error.ReadonlyCantlock,
|
||||
c.SQLITE_READONLY_ROLLBACK => error.ReadonlyRollback,
|
||||
c.SQLITE_READONLY_DBMOVED => error.ReadonlyDbMoved,
|
||||
c.SQLITE_READONLY_CANTINIT => error.ReadonlyCantInit,
|
||||
c.SQLITE_READONLY_DIRECTORY => error.ReadonlyDirectory,
|
||||
c.SQLITE_ABORT_ROLLBACK => error.AbortRollback,
|
||||
c.SQLITE_CONSTRAINT_CHECK => error.ConstraintCheck,
|
||||
c.SQLITE_CONSTRAINT_COMMITHOOK => error.ConstraintCommithook,
|
||||
c.SQLITE_CONSTRAINT_FOREIGNKEY => error.ConstraintForeignKey,
|
||||
c.SQLITE_CONSTRAINT_FUNCTION => error.ConstraintFunction,
|
||||
c.SQLITE_CONSTRAINT_NOTNULL => error.ConstraintNotNull,
|
||||
c.SQLITE_CONSTRAINT_PRIMARYKEY => error.ConstraintPrimaryKey,
|
||||
c.SQLITE_CONSTRAINT_TRIGGER => error.ConstraintTrigger,
|
||||
c.SQLITE_CONSTRAINT_UNIQUE => error.ConstraintUnique,
|
||||
c.SQLITE_CONSTRAINT_VTAB => error.ConstraintVTab,
|
||||
c.SQLITE_CONSTRAINT_ROWID => error.ConstraintRowId,
|
||||
c.SQLITE_CONSTRAINT_PINNED => error.ConstraintPinned,
|
||||
c.SQLITE_CONSTRAINT_DATATYPE => error.ConstraintDatatype,
|
||||
c.SQLITE_NOTICE_RECOVER_WAL => error.NoticeRecoverWal,
|
||||
c.SQLITE_NOTICE_RECOVER_ROLLBACK => error.NoticeRecoverRollback,
|
||||
c.SQLITE_WARNING_AUTOINDEX => error.WarningAutoIndex,
|
||||
c.SQLITE_AUTH_USER => error.AuthUser,
|
||||
c.SQLITE_OK_LOAD_PERMANENTLY => error.OkLoadPermanently,
|
||||
|
||||
else => std.debug.panic("{s} {d}", .{ c.sqlite3_errstr(result), result }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -175,74 +175,6 @@ pub const tool_list = [_]protocol.Tool{
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "hover",
|
||||
.description = "Hover over an element, triggering mouseover and mouseenter events. Useful for menus, tooltips, and hover states.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "backendNodeId": { "type": "integer", "description": "The backend node ID of the element to hover over." }
|
||||
\\ },
|
||||
\\ "required": ["backendNodeId"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "press",
|
||||
.description = "Press a keyboard key, dispatching keydown and keyup events. Use key names like 'Enter', 'Tab', 'Escape', 'ArrowDown', 'Backspace', or single characters like 'a', '1'.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "key": { "type": "string", "description": "The key to press (e.g. 'Enter', 'Tab', 'a')." },
|
||||
\\ "backendNodeId": { "type": "integer", "description": "Optional backend node ID of the element to target. Defaults to the document." }
|
||||
\\ },
|
||||
\\ "required": ["key"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "selectOption",
|
||||
.description = "Select an option in a <select> dropdown element by its value. Dispatches input and change events.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "backendNodeId": { "type": "integer", "description": "The backend node ID of the <select> element." },
|
||||
\\ "value": { "type": "string", "description": "The value of the option to select." }
|
||||
\\ },
|
||||
\\ "required": ["backendNodeId", "value"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "setChecked",
|
||||
.description = "Check or uncheck a checkbox or radio button. Dispatches input, change, and click events.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "backendNodeId": { "type": "integer", "description": "The backend node ID of the checkbox or radio input element." },
|
||||
\\ "checked": { "type": "boolean", "description": "Whether to check (true) or uncheck (false) the element." }
|
||||
\\ },
|
||||
\\ "required": ["backendNodeId", "checked"]
|
||||
\\}
|
||||
),
|
||||
},
|
||||
.{
|
||||
.name = "findElement",
|
||||
.description = "Find interactive elements by role and/or accessible name. Returns matching elements with their backend node IDs. Useful for locating specific elements without parsing the full semantic tree.",
|
||||
.inputSchema = protocol.minify(
|
||||
\\{
|
||||
\\ "type": "object",
|
||||
\\ "properties": {
|
||||
\\ "role": { "type": "string", "description": "Optional ARIA role to match (e.g. 'button', 'link', 'textbox', 'checkbox')." },
|
||||
\\ "name": { "type": "string", "description": "Optional accessible name substring to match (case-insensitive)." }
|
||||
\\ }
|
||||
\\}
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
pub fn handleList(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||
@@ -350,11 +282,6 @@ const ToolAction = enum {
|
||||
fill,
|
||||
scroll,
|
||||
waitForSelector,
|
||||
hover,
|
||||
press,
|
||||
selectOption,
|
||||
setChecked,
|
||||
findElement,
|
||||
};
|
||||
|
||||
const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
@@ -373,11 +300,6 @@ const tool_map = std.StaticStringMap(ToolAction).initComptime(.{
|
||||
.{ "fill", .fill },
|
||||
.{ "scroll", .scroll },
|
||||
.{ "waitForSelector", .waitForSelector },
|
||||
.{ "hover", .hover },
|
||||
.{ "press", .press },
|
||||
.{ "selectOption", .selectOption },
|
||||
.{ "setChecked", .setChecked },
|
||||
.{ "findElement", .findElement },
|
||||
});
|
||||
|
||||
pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Request) !void {
|
||||
@@ -412,11 +334,6 @@ pub fn handleCall(server: *Server, arena: std.mem.Allocator, req: protocol.Reque
|
||||
.fill => try handleFill(server, arena, req.id.?, call_params.arguments),
|
||||
.scroll => try handleScroll(server, arena, req.id.?, call_params.arguments),
|
||||
.waitForSelector => try handleWaitForSelector(server, arena, req.id.?, call_params.arguments),
|
||||
.hover => try handleHover(server, arena, req.id.?, call_params.arguments),
|
||||
.press => try handlePress(server, arena, req.id.?, call_params.arguments),
|
||||
.selectOption => try handleSelectOption(server, arena, req.id.?, call_params.arguments),
|
||||
.setChecked => try handleSetChecked(server, arena, req.id.?, call_params.arguments),
|
||||
.findElement => try handleFindElement(server, arena, req.id.?, call_params.arguments),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,9 +400,17 @@ fn handleNodeDetails(server: *Server, arena: std.mem.Allocator, id: std.json.Val
|
||||
backendNodeId: CDPNode.Id,
|
||||
};
|
||||
const args = try parseArgs(Params, arena, arguments, server, id, "nodeDetails");
|
||||
const resolved = try resolveNodeAndPage(server, id, args.backendNodeId);
|
||||
|
||||
const details = lp.SemanticTree.getNodeDetails(arena, resolved.node, &server.node_registry, resolved.page) catch {
|
||||
_ = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {
|
||||
return server.sendError(id, .InvalidParams, "Node not found");
|
||||
};
|
||||
|
||||
const page = server.session.currentPage().?;
|
||||
const details = lp.SemanticTree.getNodeDetails(arena, node.dom, &server.node_registry, page) catch {
|
||||
return server.sendError(id, .InternalError, "Failed to get node details");
|
||||
};
|
||||
|
||||
@@ -585,19 +510,26 @@ fn handleClick(server: *Server, arena: std.mem.Allocator, id: std.json.Value, ar
|
||||
backendNodeId: CDPNode.Id,
|
||||
};
|
||||
const args = try parseArgs(ClickParams, arena, arguments, server, id, "click");
|
||||
const resolved = try resolveNodeAndPage(server, id, args.backendNodeId);
|
||||
|
||||
lp.actions.click(resolved.node, resolved.page) catch |err| {
|
||||
const page = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {
|
||||
return server.sendError(id, .InvalidParams, "Node not found");
|
||||
};
|
||||
|
||||
lp.actions.click(node.dom, page) catch |err| {
|
||||
if (err == error.InvalidNodeType) {
|
||||
return server.sendError(id, .InvalidParams, "Node is not an HTML element");
|
||||
}
|
||||
return server.sendError(id, .InternalError, "Failed to click element");
|
||||
};
|
||||
|
||||
const page_title = resolved.page.getTitle() catch null;
|
||||
const page_title = page.getTitle() catch null;
|
||||
const result_text = try std.fmt.allocPrint(arena, "Clicked element (backendNodeId: {d}). Page url: {s}, title: {s}", .{
|
||||
args.backendNodeId,
|
||||
resolved.page.url,
|
||||
page.url,
|
||||
page_title orelse "(none)",
|
||||
});
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }};
|
||||
@@ -610,20 +542,27 @@ fn handleFill(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arg
|
||||
text: []const u8,
|
||||
};
|
||||
const args = try parseArgs(FillParams, arena, arguments, server, id, "fill");
|
||||
const resolved = try resolveNodeAndPage(server, id, args.backendNodeId);
|
||||
|
||||
lp.actions.fill(resolved.node, args.text, resolved.page) catch |err| {
|
||||
const page = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
const node = server.node_registry.lookup_by_id.get(args.backendNodeId) orelse {
|
||||
return server.sendError(id, .InvalidParams, "Node not found");
|
||||
};
|
||||
|
||||
lp.actions.fill(node.dom, args.text, page) catch |err| {
|
||||
if (err == error.InvalidNodeType) {
|
||||
return server.sendError(id, .InvalidParams, "Node is not an input, textarea or select");
|
||||
}
|
||||
return server.sendError(id, .InternalError, "Failed to fill element");
|
||||
};
|
||||
|
||||
const page_title = resolved.page.getTitle() catch null;
|
||||
const page_title = page.getTitle() catch null;
|
||||
const result_text = try std.fmt.allocPrint(arena, "Filled element (backendNodeId: {d}) with \"{s}\". Page url: {s}, title: {s}", .{
|
||||
args.backendNodeId,
|
||||
args.text,
|
||||
resolved.page.url,
|
||||
page.url,
|
||||
page_title orelse "(none)",
|
||||
});
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }};
|
||||
@@ -697,189 +636,6 @@ fn handleWaitForSelector(server: *Server, arena: std.mem.Allocator, id: std.json
|
||||
return server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn handleHover(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const Params = struct {
|
||||
backendNodeId: CDPNode.Id,
|
||||
};
|
||||
const args = try parseArgs(Params, arena, arguments, server, id, "hover");
|
||||
const resolved = try resolveNodeAndPage(server, id, args.backendNodeId);
|
||||
|
||||
lp.actions.hover(resolved.node, resolved.page) catch |err| {
|
||||
if (err == error.InvalidNodeType) {
|
||||
return server.sendError(id, .InvalidParams, "Node is not an HTML element");
|
||||
}
|
||||
return server.sendError(id, .InternalError, "Failed to hover element");
|
||||
};
|
||||
|
||||
const page_title = resolved.page.getTitle() catch null;
|
||||
const result_text = try std.fmt.allocPrint(arena, "Hovered element (backendNodeId: {d}). Page url: {s}, title: {s}", .{
|
||||
args.backendNodeId,
|
||||
resolved.page.url,
|
||||
page_title orelse "(none)",
|
||||
});
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }};
|
||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn handlePress(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const Params = struct {
|
||||
key: []const u8,
|
||||
backendNodeId: ?CDPNode.Id = null,
|
||||
};
|
||||
const args = try parseArgs(Params, arena, arguments, server, id, "press");
|
||||
|
||||
const page = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
var target_node: ?*DOMNode = null;
|
||||
if (args.backendNodeId) |node_id| {
|
||||
const node = server.node_registry.lookup_by_id.get(node_id) orelse {
|
||||
return server.sendError(id, .InvalidParams, "Node not found");
|
||||
};
|
||||
target_node = node.dom;
|
||||
}
|
||||
|
||||
lp.actions.press(target_node, args.key, page) catch |err| {
|
||||
if (err == error.InvalidNodeType) {
|
||||
return server.sendError(id, .InvalidParams, "Node is not an HTML element");
|
||||
}
|
||||
return server.sendError(id, .InternalError, "Failed to press key");
|
||||
};
|
||||
|
||||
const page_title = page.getTitle() catch null;
|
||||
const result_text = try std.fmt.allocPrint(arena, "Pressed key '{s}'. Page url: {s}, title: {s}", .{
|
||||
args.key,
|
||||
page.url,
|
||||
page_title orelse "(none)",
|
||||
});
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }};
|
||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn handleSelectOption(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const Params = struct {
|
||||
backendNodeId: CDPNode.Id,
|
||||
value: []const u8,
|
||||
};
|
||||
const args = try parseArgs(Params, arena, arguments, server, id, "selectOption");
|
||||
const resolved = try resolveNodeAndPage(server, id, args.backendNodeId);
|
||||
|
||||
lp.actions.selectOption(resolved.node, args.value, resolved.page) catch |err| {
|
||||
if (err == error.InvalidNodeType) {
|
||||
return server.sendError(id, .InvalidParams, "Node is not a <select> element");
|
||||
}
|
||||
return server.sendError(id, .InternalError, "Failed to select option");
|
||||
};
|
||||
|
||||
const page_title = resolved.page.getTitle() catch null;
|
||||
const result_text = try std.fmt.allocPrint(arena, "Selected option '{s}' (backendNodeId: {d}). Page url: {s}, title: {s}", .{
|
||||
args.value,
|
||||
args.backendNodeId,
|
||||
resolved.page.url,
|
||||
page_title orelse "(none)",
|
||||
});
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }};
|
||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn handleSetChecked(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const Params = struct {
|
||||
backendNodeId: CDPNode.Id,
|
||||
checked: bool,
|
||||
};
|
||||
const args = try parseArgs(Params, arena, arguments, server, id, "setChecked");
|
||||
const resolved = try resolveNodeAndPage(server, id, args.backendNodeId);
|
||||
|
||||
lp.actions.setChecked(resolved.node, args.checked, resolved.page) catch |err| {
|
||||
if (err == error.InvalidNodeType) {
|
||||
return server.sendError(id, .InvalidParams, "Node is not a checkbox or radio input");
|
||||
}
|
||||
return server.sendError(id, .InternalError, "Failed to set checked state");
|
||||
};
|
||||
|
||||
const state_str = if (args.checked) "checked" else "unchecked";
|
||||
const page_title = resolved.page.getTitle() catch null;
|
||||
const result_text = try std.fmt.allocPrint(arena, "Set element (backendNodeId: {d}) to {s}. Page url: {s}, title: {s}", .{
|
||||
args.backendNodeId,
|
||||
state_str,
|
||||
resolved.page.url,
|
||||
page_title orelse "(none)",
|
||||
});
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = result_text }};
|
||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn handleFindElement(server: *Server, arena: std.mem.Allocator, id: std.json.Value, arguments: ?std.json.Value) !void {
|
||||
const Params = struct {
|
||||
role: ?[]const u8 = null,
|
||||
name: ?[]const u8 = null,
|
||||
};
|
||||
const args = try parseArgsOrDefault(Params, arena, arguments, server, id);
|
||||
|
||||
if (args.role == null and args.name == null) {
|
||||
return server.sendError(id, .InvalidParams, "At least one of 'role' or 'name' must be provided");
|
||||
}
|
||||
|
||||
const page = server.session.currentPage() orelse {
|
||||
return server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
};
|
||||
|
||||
const elements = lp.interactive.collectInteractiveElements(page.document.asNode(), arena, page) catch |err| {
|
||||
log.err(.mcp, "elements collection failed", .{ .err = err });
|
||||
return server.sendError(id, .InternalError, "Failed to collect interactive elements");
|
||||
};
|
||||
|
||||
var matches: std.ArrayList(lp.interactive.InteractiveElement) = .empty;
|
||||
for (elements) |el| {
|
||||
if (args.role) |role| {
|
||||
const el_role = el.role orelse continue;
|
||||
if (!std.ascii.eqlIgnoreCase(el_role, role)) continue;
|
||||
}
|
||||
if (args.name) |name| {
|
||||
const el_name = el.name orelse continue;
|
||||
if (!containsIgnoreCase(el_name, name)) continue;
|
||||
}
|
||||
try matches.append(arena, el);
|
||||
}
|
||||
|
||||
const matched = try matches.toOwnedSlice(arena);
|
||||
lp.interactive.registerNodes(matched, &server.node_registry) catch |err| {
|
||||
log.err(.mcp, "node registration failed", .{ .err = err });
|
||||
return server.sendError(id, .InternalError, "Failed to register element nodes");
|
||||
};
|
||||
|
||||
var aw: std.Io.Writer.Allocating = .init(arena);
|
||||
try std.json.Stringify.value(matched, .{}, &aw.writer);
|
||||
|
||||
const content = [_]protocol.TextContent([]const u8){.{ .text = aw.written() }};
|
||||
try server.sendResult(id, protocol.CallToolResult([]const u8){ .content = &content });
|
||||
}
|
||||
|
||||
fn containsIgnoreCase(haystack: []const u8, needle: []const u8) bool {
|
||||
if (needle.len > haystack.len) return false;
|
||||
if (needle.len == 0) return true;
|
||||
const end = haystack.len - needle.len + 1;
|
||||
for (0..end) |i| {
|
||||
if (std.ascii.eqlIgnoreCase(haystack[i..][0..needle.len], needle)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const NodeAndPage = struct { node: *DOMNode, page: *lp.Page };
|
||||
|
||||
fn resolveNodeAndPage(server: *Server, id: std.json.Value, node_id: CDPNode.Id) !NodeAndPage {
|
||||
const page = server.session.currentPage() orelse {
|
||||
try server.sendError(id, .PageNotLoaded, "Page not loaded");
|
||||
return error.PageNotLoaded;
|
||||
};
|
||||
const node = server.node_registry.lookup_by_id.get(node_id) orelse {
|
||||
try server.sendError(id, .InvalidParams, "Node not found");
|
||||
return error.InvalidParams;
|
||||
};
|
||||
return .{ .node = node.dom, .page = page };
|
||||
}
|
||||
|
||||
fn ensurePage(server: *Server, id: std.json.Value, url: ?[:0]const u8, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) !*lp.Page {
|
||||
if (url) |u| {
|
||||
try performGoto(server, u, id, timeout, waitUntil);
|
||||
@@ -980,7 +736,7 @@ test "MCP - evaluate error reporting" {
|
||||
} }, out.written());
|
||||
}
|
||||
|
||||
test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked" {
|
||||
test "MCP - Actions: click, fill, scroll" {
|
||||
defer testing.reset();
|
||||
const aa = testing.arena_allocator;
|
||||
|
||||
@@ -1041,67 +797,7 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Test Hover
|
||||
const el = page.document.getElementById("hoverTarget", page).?.asNode();
|
||||
const el_id = (try server.node_registry.register(el)).id;
|
||||
var id_buf: [12]u8 = undefined;
|
||||
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
|
||||
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"hover\",\"arguments\":{\"backendNodeId\":", id_str, "}}}" });
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "Hovered element") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Test Press
|
||||
const el = page.document.getElementById("keyTarget", page).?.asNode();
|
||||
const el_id = (try server.node_registry.register(el)).id;
|
||||
var id_buf: [12]u8 = undefined;
|
||||
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
|
||||
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",\"params\":{\"name\":\"press\",\"arguments\":{\"key\":\"Enter\",\"backendNodeId\":", id_str, "}}}" });
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "Pressed key") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Test SelectOption
|
||||
const el = page.document.getElementById("sel2", page).?.asNode();
|
||||
const el_id = (try server.node_registry.register(el)).id;
|
||||
var id_buf: [12]u8 = undefined;
|
||||
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
|
||||
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"tools/call\",\"params\":{\"name\":\"selectOption\",\"arguments\":{\"backendNodeId\":", id_str, ",\"value\":\"b\"}}}" });
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "Selected option") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Test SetChecked (checkbox)
|
||||
const el = page.document.getElementById("chk", page).?.asNode();
|
||||
const el_id = (try server.node_registry.register(el)).id;
|
||||
var id_buf: [12]u8 = undefined;
|
||||
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
|
||||
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"tools/call\",\"params\":{\"name\":\"setChecked\",\"arguments\":{\"backendNodeId\":", id_str, ",\"checked\":true}}}" });
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "checked") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Test SetChecked (radio)
|
||||
const el = page.document.getElementById("rad", page).?.asNode();
|
||||
const el_id = (try server.node_registry.register(el)).id;
|
||||
var id_buf: [12]u8 = undefined;
|
||||
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{el_id}) catch unreachable;
|
||||
const msg = try std.mem.concat(aa, u8, &.{ "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"tools/call\",\"params\":{\"name\":\"setChecked\",\"arguments\":{\"backendNodeId\":", id_str, ",\"checked\":true}}}" });
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "checked") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
// Evaluate JS assertions for all actions
|
||||
// Evaluate assertions
|
||||
var ls: js.Local.Scope = undefined;
|
||||
page.js.localScope(&ls);
|
||||
defer ls.deinit();
|
||||
@@ -1113,66 +809,12 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked
|
||||
const result = try ls.local.exec(
|
||||
\\ window.clicked === true && window.inputVal === 'hello' &&
|
||||
\\ window.changed === true && window.selChanged === 'opt2' &&
|
||||
\\ window.scrolled === true &&
|
||||
\\ window.hovered === true &&
|
||||
\\ window.keyPressed === 'Enter' && window.keyReleased === 'Enter' &&
|
||||
\\ window.sel2Changed === 'b' &&
|
||||
\\ window.chkClicked === true && window.chkChanged === true &&
|
||||
\\ window.radClicked === true && window.radChanged === true
|
||||
\\ window.scrolled === true
|
||||
, null);
|
||||
|
||||
try testing.expect(result.isTrue());
|
||||
}
|
||||
|
||||
test "MCP - findElement" {
|
||||
defer testing.reset();
|
||||
const aa = testing.arena_allocator;
|
||||
|
||||
var out: std.io.Writer.Allocating = .init(aa);
|
||||
const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer);
|
||||
defer server.deinit();
|
||||
|
||||
{
|
||||
// Find by role
|
||||
const msg =
|
||||
\\{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"findElement","arguments":{"role":"button"}}}
|
||||
;
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "Click Me") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Find by name (case-insensitive substring)
|
||||
const msg =
|
||||
\\{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"findElement","arguments":{"name":"click"}}}
|
||||
;
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "Click Me") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Find with no matches
|
||||
const msg =
|
||||
\\{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"findElement","arguments":{"role":"slider"}}}
|
||||
;
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "[]") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
|
||||
{
|
||||
// Error: no params provided
|
||||
const msg =
|
||||
\\{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"findElement","arguments":{}}}
|
||||
;
|
||||
try router.handleMessage(server, aa, msg);
|
||||
try testing.expect(std.mem.indexOf(u8, out.written(), "error") != null);
|
||||
out.clearRetainingCapacity();
|
||||
}
|
||||
}
|
||||
|
||||
test "MCP - waitForSelector: existing element" {
|
||||
defer testing.reset();
|
||||
var out: std.io.Writer.Allocating = .init(testing.arena_allocator);
|
||||
|
||||
13968
src/sqlite3.h
Normal file
13968
src/sqlite3.h
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user