#[cfg(feature = "broker")]
pub mod broker;
pub mod session_test;
use serde::{Deserialize, Serialize};
pub fn token_from_ws_url(ws_url: &str) -> Option<String> {
let (_, query) = ws_url.split_once('?')?;
query.split('&').find_map(|pair| {
let (key, value) = pair.split_once('=')?;
(key == "token" && !value.is_empty()).then(|| value.to_string())
})
}
pub fn ws_url_with_token(ws_url: &str, token: &str) -> String {
if ws_url.contains('?') {
return format!("{ws_url}&token={token}");
}
let has_path = ws_url
.split_once("://")
.is_some_and(|(_, rest)| rest.contains('/'));
let separator = if has_path { "?" } else { "/?" };
format!("{ws_url}{separator}token={token}")
}
pub mod handlers {
pub const ECHO: &str = "echo";
pub mod session {
pub const SHUTDOWN: &str = "session.shutdown";
pub mod test {
pub const START: &str = "session.test.start";
pub const POLL: &str = "session.test.poll";
pub const CANCEL: &str = "session.test.cancel";
}
}
pub mod browser {
pub const OPEN: &str = "browser.open";
pub const TABS: &str = "browser.tabs";
pub const CURRENT: &str = "browser.current";
pub const ACTIVATE: &str = "browser.activate";
pub const CLOSE: &str = "browser.close";
pub const RELOAD: &str = "browser.reload";
pub const BACK: &str = "browser.back";
pub const FORWARD: &str = "browser.forward";
pub const EVAL: &str = "browser.eval";
pub const QUERY: &str = "browser.query";
pub const WAIT: &str = "browser.wait";
pub const WAIT_URL: &str = "browser.wait_url";
pub const WAIT_NAVIGATION: &str = "browser.wait_navigation";
pub const CLICK: &str = "browser.click";
pub const TYPE: &str = "browser.type";
pub const FILL: &str = "browser.fill";
pub const PRESS: &str = "browser.press";
pub const SCROLL: &str = "browser.scroll";
pub const SCROLL_TO: &str = "browser.scroll_to";
pub const COOKIES_LIST: &str = "browser.cookies.list";
pub const COOKIES_SET: &str = "browser.cookies.set";
pub const COOKIES_DELETE: &str = "browser.cookies.delete";
pub const COOKIES_CLEAR: &str = "browser.cookies.clear";
pub const SCREENSHOT: &str = "browser.screenshot";
pub const NETWORK_ENABLE: &str = "browser.network.enable";
pub const NETWORK_DISABLE: &str = "browser.network.disable";
pub const NETWORK_LIST: &str = "browser.network.list";
pub const NETWORK_CLEAR: &str = "browser.network.clear";
}
pub mod lxapp {
pub const LIST: &str = "lxapp.list";
pub const CURRENT: &str = "lxapp.current";
pub const DOCTOR: &str = "lxapp.doctor";
pub const INFO: &str = "lxapp.info";
pub const PAGES: &str = "lxapp.pages";
pub const EVAL: &str = "lxapp.eval";
pub const OPEN: &str = "lxapp.open";
pub const CLOSE: &str = "lxapp.close";
pub const RESTART: &str = "lxapp.restart";
pub const UNINSTALL: &str = "lxapp.uninstall";
pub const BUILD: &str = "lxapp.build";
}
pub mod lxapp_nav {
pub const TO: &str = "lxapp.nav.to";
pub const REDIRECT: &str = "lxapp.nav.redirect";
pub const SWITCH_TAB: &str = "lxapp.nav.switch_tab";
pub const RELAUNCH: &str = "lxapp.nav.relaunch";
pub const BACK: &str = "lxapp.nav.back";
}
pub mod app {
pub const DOCTOR: &str = "app.doctor";
pub const SCREENSHOT: &str = "app.screenshot";
pub const WINDOWS: &str = "app.windows";
pub const MOUSE: &str = "app.mouse";
pub const KEYBOARD: &str = "app.keyboard";
}
pub mod lxapp_device {
pub const LIST: &str = "lxapp.device.list";
pub const GET: &str = "lxapp.device.get";
pub const SET: &str = "lxapp.device.set";
}
pub mod lxapp_page {
pub const CURRENT: &str = "lxapp.page.current";
pub const LIST: &str = "lxapp.page.list";
pub const INFO: &str = "lxapp.page.info";
pub const WAIT: &str = "lxapp.page.wait";
pub const EVAL: &str = "lxapp.page.eval";
pub const QUERY: &str = "lxapp.page.query";
pub const CLICK: &str = "lxapp.page.click";
pub const TYPE: &str = "lxapp.page.type";
pub const FILL: &str = "lxapp.page.fill";
pub const PRESS: &str = "lxapp.page.press";
pub const SCROLL: &str = "lxapp.page.scroll";
pub const SCROLL_TO: &str = "lxapp.page.scroll_to";
pub const BACK: &str = "lxapp.page.back";
pub const SCREENSHOT: &str = "lxapp.page.screenshot";
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DevtoolsPeerRole {
Devtool,
Client,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DevtoolsLogLevel {
Verbose,
Debug,
Info,
Warn,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DevtoolsLogSource {
Native,
WebViewConsole,
LxAppServiceConsole,
BrowserConsole,
Automation,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DevtoolsLogMessage {
pub timestamp_ms: u64,
#[serde(alias = "tag")]
pub source: DevtoolsLogSource,
pub level: DevtoolsLogLevel,
pub appid: Option<String>,
pub path: Option<String>,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum DevtoolsWireMessage {
Hello {
role: DevtoolsPeerRole,
#[serde(default, skip_serializing_if = "Option::is_none")]
token: Option<String>,
},
LogBatch {
logs: Vec<DevtoolsLogMessage>,
},
Command {
command_id: String,
handler: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
args: Option<serde_json::Value>,
},
Result {
command_id: String,
ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
error: Option<String>,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_appends_with_path_separator_on_authority_only_urls() {
let url = ws_url_with_token("ws://192.168.1.20:39142", "abc");
assert_eq!(url, "ws://192.168.1.20:39142/?token=abc");
assert_eq!(token_from_ws_url(&url).as_deref(), Some("abc"));
}
#[test]
fn token_round_trips_with_existing_path_and_query() {
assert_eq!(
ws_url_with_token("ws://h:1/x", "t"),
"ws://h:1/x?token=t".to_string()
);
assert_eq!(
ws_url_with_token("ws://h:1/x?a=b", "t"),
"ws://h:1/x?a=b&token=t".to_string()
);
assert_eq!(
token_from_ws_url("ws://h:1/x?a=b&token=t").as_deref(),
Some("t")
);
assert_eq!(token_from_ws_url("ws://h:1/x?a=b"), None);
assert_eq!(token_from_ws_url("ws://h:1"), None);
}
#[test]
fn hello_without_token_deserializes_as_none() {
let hello: DevtoolsWireMessage =
serde_json::from_str(r#"{"type":"hello","role":"client"}"#).unwrap();
let DevtoolsWireMessage::Hello { role, token } = hello else {
panic!("expected hello");
};
assert_eq!(role, DevtoolsPeerRole::Client);
assert_eq!(token, None);
}
}