///|
/// Browser Control API
///
/// This module provides a typed API for browser automation.
/// It can be used:
/// 1. Directly from MoonBit code
/// 2. Via WebDriver BiDi (WebSocket, primary)
/// 3. Via JSON-RPC over WebSocket/stdio
/// 4. Via WebDriver protocol (HTTP, legacy)
/// 5. Via CDP-like protocol for Playwright compatibility
// =============================================================================
// Core Types
// =============================================================================
///|
/// Unique identifier for browser elements
pub(all) struct ElementId {
id : String
} derive(Debug, Eq)
///|
/// Unique identifier for browser pages/tabs
pub(all) struct PageId {
id : String
} derive(Debug, Eq)
///|
/// Unique identifier for browser contexts
pub(all) struct ContextId {
id : String
} derive(Debug, Eq)
///|
/// Point in 2D space
pub(all) struct Point {
x : Double
y : Double
} derive(Debug)
///|
/// Rectangle with position and size
pub(all) struct Rect {
x : Double
y : Double
width : Double
height : Double
} derive(Debug)
///|
/// Viewport size
pub(all) struct Viewport {
width : Int
height : Int
} derive(Debug)
///|
/// Mouse button
pub(all) enum MouseButton {
Left
Right
Middle
} derive(Debug, Eq)
///|
/// Keyboard modifier keys
pub(all) struct Modifiers {
ctrl : Bool
shift : Bool
alt : Bool
meta : Bool
} derive(Debug)
///|
/// Default modifiers (none pressed)
pub fn Modifiers::none() -> Modifiers {
{ ctrl: false, shift: false, alt: false, meta: false }
}
// =============================================================================
// Element Selector
// =============================================================================
///|
/// Element selector strategy
pub(all) enum Selector {
/// CSS selector (e.g., "#id", ".class", "div > p")
Css(String)
/// XPath expression
XPath(String)
/// Text content (exact match)
Text(String)
/// Text content (contains)
TextContains(String)
/// ARIA role
Role(role~ : String, name~ : String?)
/// Test ID attribute
TestId(String)
/// Accessibility label
Label(String)
/// Placeholder text
Placeholder(String)
/// Alt text (for images)
AltText(String)
/// Title attribute
Title(String)
} derive(Debug)
///|
/// Convert selector to string representation
pub fn Selector::to_string(self : Selector) -> String {
match self {
Css(s) => "css=" + s
XPath(s) => "xpath=" + s
Text(s) => "text=\"" + s + "\""
TextContains(s) => "text=" + s
Role(role~, name~) =>
match name {
Some(n) => "role=" + role + "[name=\"" + n + "\"]"
None => "role=" + role
}
TestId(s) => "data-testid=" + s
Label(s) => "label=" + s
Placeholder(s) => "placeholder=" + s
AltText(s) => "alt=" + s
Title(s) => "title=" + s
}
}
// =============================================================================
// API Error Types
// =============================================================================
///|
/// API error
pub(all) enum ApiError {
/// Element not found
ElementNotFound(selector~ : String)
/// Page not found
PageNotFound(page_id~ : String)
/// Context not found
ContextNotFound(context_id~ : String)
/// Timeout waiting for condition
Timeout(message~ : String)
/// Navigation failed
NavigationFailed(url~ : String, reason~ : String)
/// Element not visible
ElementNotVisible(element_id~ : String)
/// Element not enabled
ElementNotEnabled(element_id~ : String)
/// JavaScript evaluation error
EvalError(message~ : String)
/// Invalid argument
InvalidArgument(message~ : String)
/// Operation not supported
NotSupported(message~ : String)
/// Internal error
InternalError(message~ : String)
} derive(Debug)
///|
pub impl Show for ApiError with fn output(self, logger) {
match self {
ElementNotFound(selector~) => {
logger.write_string("ElementNotFound(selector=")
selector.output(logger)
logger.write_string(")")
}
PageNotFound(page_id~) => {
logger.write_string("PageNotFound(page_id=")
page_id.output(logger)
logger.write_string(")")
}
ContextNotFound(context_id~) => {
logger.write_string("ContextNotFound(context_id=")
context_id.output(logger)
logger.write_string(")")
}
Timeout(message~) => {
logger.write_string("Timeout(message=")
message.output(logger)
logger.write_string(")")
}
NavigationFailed(url~, reason~) => {
logger.write_string("NavigationFailed(url=")
url.output(logger)
logger.write_string(", reason=")
reason.output(logger)
logger.write_string(")")
}
ElementNotVisible(element_id~) => {
logger.write_string("ElementNotVisible(element_id=")
element_id.output(logger)
logger.write_string(")")
}
ElementNotEnabled(element_id~) => {
logger.write_string("ElementNotEnabled(element_id=")
element_id.output(logger)
logger.write_string(")")
}
EvalError(message~) => {
logger.write_string("EvalError(message=")
message.output(logger)
logger.write_string(")")
}
InvalidArgument(message~) => {
logger.write_string("InvalidArgument(message=")
message.output(logger)
logger.write_string(")")
}
NotSupported(message~) => {
logger.write_string("NotSupported(message=")
message.output(logger)
logger.write_string(")")
}
InternalError(message~) => {
logger.write_string("InternalError(message=")
message.output(logger)
logger.write_string(")")
}
}
}
///|
/// API result - success or error
pub(all) enum ApiResult[T] {
Ok(T)
Err(ApiError)
} derive(Debug)
// =============================================================================
// API Method Enum (for JSON-RPC dispatch)
// =============================================================================
///|
/// All API methods as an enum for JSON-RPC dispatch
pub(all) enum ApiMethod {
// Context
NewContext
CloseContext(context_id~ : String)
// Page
NewPage(context_id~ : String)
ClosePage(page_id~ : String)
Pages(context_id~ : String)
// Navigation
Goto(page_id~ : String, url~ : String)
GoBack(page_id~ : String)
GoForward(page_id~ : String)
Reload(page_id~ : String)
Url(page_id~ : String)
Title(page_id~ : String)
// Element queries
Query(page_id~ : String, selector~ : String)
QueryAll(page_id~ : String, selector~ : String)
WaitFor(page_id~ : String, selector~ : String, timeout~ : Int)
// Element properties
BoundingBox(element_id~ : String)
TextContent(element_id~ : String)
InnerText(element_id~ : String)
InnerHtml(element_id~ : String)
GetAttribute(element_id~ : String, name~ : String)
IsVisible(element_id~ : String)
IsEnabled(element_id~ : String)
IsChecked(element_id~ : String)
// Element actions
Click(element_id~ : String)
DblClick(element_id~ : String)
RightClick(element_id~ : String)
Hover(element_id~ : String)
Focus(element_id~ : String)
Fill(element_id~ : String, value~ : String)
Press(element_id~ : String, key~ : String)
Clear(element_id~ : String)
SelectOption(element_id~ : String, value~ : String)
Check(element_id~ : String)
Uncheck(element_id~ : String)
// Mouse
MouseMove(page_id~ : String, x~ : Double, y~ : Double)
MouseClick(page_id~ : String, x~ : Double, y~ : Double, button~ : String)
MouseDown(page_id~ : String, button~ : String)
MouseUp(page_id~ : String, button~ : String)
// Keyboard
KeyboardPress(page_id~ : String, key~ : String)
KeyboardType(page_id~ : String, text~ : String)
// Page state
Content(page_id~ : String)
SetContent(page_id~ : String, html~ : String)
ViewportSize(page_id~ : String)
SetViewportSize(page_id~ : String, width~ : Int, height~ : Int)
// JavaScript
Evaluate(page_id~ : String, expression~ : String)
// Screenshots
Screenshot(page_id~ : String)
ElementScreenshot(element_id~ : String)
// Accessibility
AccessibilitySnapshot(page_id~ : String)
} derive(Debug)
///|
pub impl Show for ApiMethod with fn output(self, logger) {
match self {
NewContext => logger.write_string("NewContext")
CloseContext(context_id~) => {
logger.write_string("CloseContext(context_id=")
context_id.output(logger)
logger.write_string(")")
}
NewPage(context_id~) => {
logger.write_string("NewPage(context_id=")
context_id.output(logger)
logger.write_string(")")
}
ClosePage(page_id~) => {
logger.write_string("ClosePage(page_id=")
page_id.output(logger)
logger.write_string(")")
}
Pages(context_id~) => {
logger.write_string("Pages(context_id=")
context_id.output(logger)
logger.write_string(")")
}
Goto(page_id~, url~) => {
logger.write_string("Goto(page_id=")
page_id.output(logger)
logger.write_string(", url=")
url.output(logger)
logger.write_string(")")
}
GoBack(page_id~) => {
logger.write_string("GoBack(page_id=")
page_id.output(logger)
logger.write_string(")")
}
GoForward(page_id~) => {
logger.write_string("GoForward(page_id=")
page_id.output(logger)
logger.write_string(")")
}
Reload(page_id~) => {
logger.write_string("Reload(page_id=")
page_id.output(logger)
logger.write_string(")")
}
Url(page_id~) => {
logger.write_string("Url(page_id=")
page_id.output(logger)
logger.write_string(")")
}
Title(page_id~) => {
logger.write_string("Title(page_id=")
page_id.output(logger)
logger.write_string(")")
}
Query(page_id~, selector~) => {
logger.write_string("Query(page_id=")
page_id.output(logger)
logger.write_string(", selector=")
selector.output(logger)
logger.write_string(")")
}
QueryAll(page_id~, selector~) => {
logger.write_string("QueryAll(page_id=")
page_id.output(logger)
logger.write_string(", selector=")
selector.output(logger)
logger.write_string(")")
}
WaitFor(page_id~, selector~, timeout~) => {
logger.write_string("WaitFor(page_id=")
page_id.output(logger)
logger.write_string(", selector=")
selector.output(logger)
logger.write_string(", timeout=")
timeout.output(logger)
logger.write_string(")")
}
BoundingBox(element_id~) => {
logger.write_string("BoundingBox(element_id=")
element_id.output(logger)
logger.write_string(")")
}
TextContent(element_id~) => {
logger.write_string("TextContent(element_id=")
element_id.output(logger)
logger.write_string(")")
}
InnerText(element_id~) => {
logger.write_string("InnerText(element_id=")
element_id.output(logger)
logger.write_string(")")
}
InnerHtml(element_id~) => {
logger.write_string("InnerHtml(element_id=")
element_id.output(logger)
logger.write_string(")")
}
GetAttribute(element_id~, name~) => {
logger.write_string("GetAttribute(element_id=")
element_id.output(logger)
logger.write_string(", name=")
name.output(logger)
logger.write_string(")")
}
IsVisible(element_id~) => {
logger.write_string("IsVisible(element_id=")
element_id.output(logger)
logger.write_string(")")
}
IsEnabled(element_id~) => {
logger.write_string("IsEnabled(element_id=")
element_id.output(logger)
logger.write_string(")")
}
IsChecked(element_id~) => {
logger.write_string("IsChecked(element_id=")
element_id.output(logger)
logger.write_string(")")
}
Click(element_id~) => {
logger.write_string("Click(element_id=")
element_id.output(logger)
logger.write_string(")")
}
DblClick(element_id~) => {
logger.write_string("DblClick(element_id=")
element_id.output(logger)
logger.write_string(")")
}
RightClick(element_id~) => {
logger.write_string("RightClick(element_id=")
element_id.output(logger)
logger.write_string(")")
}
Hover(element_id~) => {
logger.write_string("Hover(element_id=")
element_id.output(logger)
logger.write_string(")")
}
Focus(element_id~) => {
logger.write_string("Focus(element_id=")
element_id.output(logger)
logger.write_string(")")
}
Fill(element_id~, value~) => {
logger.write_string("Fill(element_id=")
element_id.output(logger)
logger.write_string(", value=")
value.output(logger)
logger.write_string(")")
}
Press(element_id~, key~) => {
logger.write_string("Press(element_id=")
element_id.output(logger)
logger.write_string(", key=")
key.output(logger)
logger.write_string(")")
}
Clear(element_id~) => {
logger.write_string("Clear(element_id=")
element_id.output(logger)
logger.write_string(")")
}
SelectOption(element_id~, value~) => {
logger.write_string("SelectOption(element_id=")
element_id.output(logger)
logger.write_string(", value=")
value.output(logger)
logger.write_string(")")
}
Check(element_id~) => {
logger.write_string("Check(element_id=")
element_id.output(logger)
logger.write_string(")")
}
Uncheck(element_id~) => {
logger.write_string("Uncheck(element_id=")
element_id.output(logger)
logger.write_string(")")
}
MouseMove(page_id~, x~, y~) => {
logger.write_string("MouseMove(page_id=")
page_id.output(logger)
logger.write_string(", x=")
x.output(logger)
logger.write_string(", y=")
y.output(logger)
logger.write_string(")")
}
MouseClick(page_id~, x~, y~, button~) => {
logger.write_string("MouseClick(page_id=")
page_id.output(logger)
logger.write_string(", x=")
x.output(logger)
logger.write_string(", y=")
y.output(logger)
logger.write_string(", button=")
button.output(logger)
logger.write_string(")")
}
MouseDown(page_id~, button~) => {
logger.write_string("MouseDown(page_id=")
page_id.output(logger)
logger.write_string(", button=")
button.output(logger)
logger.write_string(")")
}
MouseUp(page_id~, button~) => {
logger.write_string("MouseUp(page_id=")
page_id.output(logger)
logger.write_string(", button=")
button.output(logger)
logger.write_string(")")
}
KeyboardPress(page_id~, key~) => {
logger.write_string("KeyboardPress(page_id=")
page_id.output(logger)
logger.write_string(", key=")
key.output(logger)
logger.write_string(")")
}
KeyboardType(page_id~, text~) => {
logger.write_string("KeyboardType(page_id=")
page_id.output(logger)
logger.write_string(", text=")
text.output(logger)
logger.write_string(")")
}
Content(page_id~) => {
logger.write_string("Content(page_id=")
page_id.output(logger)
logger.write_string(")")
}
SetContent(page_id~, html~) => {
logger.write_string("SetContent(page_id=")
page_id.output(logger)
logger.write_string(", html=")
html.output(logger)
logger.write_string(")")
}
ViewportSize(page_id~) => {
logger.write_string("ViewportSize(page_id=")
page_id.output(logger)
logger.write_string(")")
}
SetViewportSize(page_id~, width~, height~) => {
logger.write_string("SetViewportSize(page_id=")
page_id.output(logger)
logger.write_string(", width=")
width.output(logger)
logger.write_string(", height=")
height.output(logger)
logger.write_string(")")
}
Evaluate(page_id~, expression~) => {
logger.write_string("Evaluate(page_id=")
page_id.output(logger)
logger.write_string(", expression=")
expression.output(logger)
logger.write_string(")")
}
Screenshot(page_id~) => {
logger.write_string("Screenshot(page_id=")
page_id.output(logger)
logger.write_string(")")
}
ElementScreenshot(element_id~) => {
logger.write_string("ElementScreenshot(element_id=")
element_id.output(logger)
logger.write_string(")")
}
AccessibilitySnapshot(page_id~) => {
logger.write_string("AccessibilitySnapshot(page_id=")
page_id.output(logger)
logger.write_string(")")
}
}
}