lingxia_devtool_protocol/
lib.rs1#[cfg(feature = "broker")]
2pub mod broker;
3pub mod session_test;
4
5use serde::{Deserialize, Serialize};
6
7pub fn token_from_ws_url(ws_url: &str) -> Option<String> {
11 let (_, query) = ws_url.split_once('?')?;
12 query.split('&').find_map(|pair| {
13 let (key, value) = pair.split_once('=')?;
14 (key == "token" && !value.is_empty()).then(|| value.to_string())
15 })
16}
17
18pub fn ws_url_with_token(ws_url: &str, token: &str) -> String {
22 if ws_url.contains('?') {
23 return format!("{ws_url}&token={token}");
24 }
25 let has_path = ws_url
26 .split_once("://")
27 .is_some_and(|(_, rest)| rest.contains('/'));
28 let separator = if has_path { "?" } else { "/?" };
29 format!("{ws_url}{separator}token={token}")
30}
31
32pub mod handlers {
33 pub const ECHO: &str = "echo";
34
35 pub mod session {
36 pub const SHUTDOWN: &str = "session.shutdown";
39
40 pub mod test {
44 pub const START: &str = "session.test.start";
45 pub const POLL: &str = "session.test.poll";
46 pub const CANCEL: &str = "session.test.cancel";
47 }
48 }
49
50 pub mod browser {
51 pub const OPEN: &str = "browser.open";
52 pub const TABS: &str = "browser.tabs";
53 pub const CURRENT: &str = "browser.current";
54 pub const ACTIVATE: &str = "browser.activate";
55 pub const CLOSE: &str = "browser.close";
56 pub const RELOAD: &str = "browser.reload";
57 pub const BACK: &str = "browser.back";
58 pub const FORWARD: &str = "browser.forward";
59 pub const EVAL: &str = "browser.eval";
60 pub const QUERY: &str = "browser.query";
61 pub const WAIT: &str = "browser.wait";
62 pub const WAIT_URL: &str = "browser.wait_url";
63 pub const WAIT_NAVIGATION: &str = "browser.wait_navigation";
64 pub const CLICK: &str = "browser.click";
65 pub const TYPE: &str = "browser.type";
66 pub const FILL: &str = "browser.fill";
67 pub const PRESS: &str = "browser.press";
68 pub const SCROLL: &str = "browser.scroll";
69 pub const SCROLL_TO: &str = "browser.scroll_to";
70 pub const COOKIES_LIST: &str = "browser.cookies.list";
71 pub const COOKIES_SET: &str = "browser.cookies.set";
72 pub const COOKIES_DELETE: &str = "browser.cookies.delete";
73 pub const COOKIES_CLEAR: &str = "browser.cookies.clear";
74 pub const SCREENSHOT: &str = "browser.screenshot";
75 pub const NETWORK_ENABLE: &str = "browser.network.enable";
77 pub const NETWORK_DISABLE: &str = "browser.network.disable";
78 pub const NETWORK_LIST: &str = "browser.network.list";
79 pub const NETWORK_CLEAR: &str = "browser.network.clear";
80 }
81
82 pub mod lxapp {
83 pub const LIST: &str = "lxapp.list";
84 pub const CURRENT: &str = "lxapp.current";
85 pub const DOCTOR: &str = "lxapp.doctor";
87 pub const INFO: &str = "lxapp.info";
88 pub const PAGES: &str = "lxapp.pages";
89 pub const EVAL: &str = "lxapp.eval";
90 pub const OPEN: &str = "lxapp.open";
91 pub const CLOSE: &str = "lxapp.close";
92 pub const RESTART: &str = "lxapp.restart";
93 pub const UNINSTALL: &str = "lxapp.uninstall";
94 pub const BUILD: &str = "lxapp.build";
98 }
99
100 pub mod lxapp_nav {
101 pub const TO: &str = "lxapp.nav.to";
102 pub const REDIRECT: &str = "lxapp.nav.redirect";
103 pub const SWITCH_TAB: &str = "lxapp.nav.switch_tab";
104 pub const RELAUNCH: &str = "lxapp.nav.relaunch";
105 pub const BACK: &str = "lxapp.nav.back";
106 }
107
108 pub mod app {
109 pub const DOCTOR: &str = "app.doctor";
111
112 pub const SCREENSHOT: &str = "app.screenshot";
119
120 pub const WINDOWS: &str = "app.windows";
123
124 pub const MOUSE: &str = "app.mouse";
128
129 pub const KEYBOARD: &str = "app.keyboard";
133 }
134
135 pub mod lxapp_device {
136 pub const LIST: &str = "lxapp.device.list";
138 pub const GET: &str = "lxapp.device.get";
140 pub const SET: &str = "lxapp.device.set";
142 }
143
144 pub mod lxapp_page {
145 pub const CURRENT: &str = "lxapp.page.current";
146 pub const LIST: &str = "lxapp.page.list";
147 pub const INFO: &str = "lxapp.page.info";
148 pub const WAIT: &str = "lxapp.page.wait";
149 pub const EVAL: &str = "lxapp.page.eval";
150 pub const QUERY: &str = "lxapp.page.query";
151 pub const CLICK: &str = "lxapp.page.click";
152 pub const TYPE: &str = "lxapp.page.type";
153 pub const FILL: &str = "lxapp.page.fill";
154 pub const PRESS: &str = "lxapp.page.press";
155 pub const SCROLL: &str = "lxapp.page.scroll";
156 pub const SCROLL_TO: &str = "lxapp.page.scroll_to";
157 pub const BACK: &str = "lxapp.page.back";
158 pub const SCREENSHOT: &str = "lxapp.page.screenshot";
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "snake_case")]
164pub enum DevtoolsPeerRole {
165 Devtool,
166 Client,
167}
168
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171pub enum DevtoolsLogLevel {
172 Verbose,
173 Debug,
174 Info,
175 Warn,
176 Error,
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181pub enum DevtoolsLogSource {
182 Native,
183 WebViewConsole,
184 LxAppServiceConsole,
185 BrowserConsole,
186 Automation,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct DevtoolsLogMessage {
193 pub timestamp_ms: u64,
194 #[serde(alias = "tag")]
195 pub source: DevtoolsLogSource,
196 pub level: DevtoolsLogLevel,
197 pub appid: Option<String>,
198 pub path: Option<String>,
199 pub message: String,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
203#[serde(tag = "type", rename_all = "snake_case")]
204pub enum DevtoolsWireMessage {
205 Hello {
206 role: DevtoolsPeerRole,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
211 token: Option<String>,
212 },
213 LogBatch {
214 logs: Vec<DevtoolsLogMessage>,
215 },
216 Command {
217 command_id: String,
218 handler: String,
219 #[serde(default, skip_serializing_if = "Option::is_none")]
220 args: Option<serde_json::Value>,
221 },
222 Result {
223 command_id: String,
224 ok: bool,
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 data: Option<serde_json::Value>,
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 error: Option<String>,
229 },
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn token_appends_with_path_separator_on_authority_only_urls() {
238 let url = ws_url_with_token("ws://192.168.1.20:39142", "abc");
239 assert_eq!(url, "ws://192.168.1.20:39142/?token=abc");
240 assert_eq!(token_from_ws_url(&url).as_deref(), Some("abc"));
241 }
242
243 #[test]
244 fn token_round_trips_with_existing_path_and_query() {
245 assert_eq!(
246 ws_url_with_token("ws://h:1/x", "t"),
247 "ws://h:1/x?token=t".to_string()
248 );
249 assert_eq!(
250 ws_url_with_token("ws://h:1/x?a=b", "t"),
251 "ws://h:1/x?a=b&token=t".to_string()
252 );
253 assert_eq!(
254 token_from_ws_url("ws://h:1/x?a=b&token=t").as_deref(),
255 Some("t")
256 );
257 assert_eq!(token_from_ws_url("ws://h:1/x?a=b"), None);
258 assert_eq!(token_from_ws_url("ws://h:1"), None);
259 }
260
261 #[test]
262 fn hello_without_token_deserializes_as_none() {
263 let hello: DevtoolsWireMessage =
264 serde_json::from_str(r#"{"type":"hello","role":"client"}"#).unwrap();
265 let DevtoolsWireMessage::Hello { role, token } = hello else {
266 panic!("expected hello");
267 };
268 assert_eq!(role, DevtoolsPeerRole::Client);
269 assert_eq!(token, None);
270 }
271}