1use bao_cdp::servo_bridge::{BridgeCommand, BridgeResponse};
5use base64::Engine;
6use serde_json::Value;
7use servo::{CookieSource, StorageType};
8use std::collections::HashSet;
9
10use crate::config::PageConfig;
11use crate::delegate::{
12 ServiceWorkerHandle, ServiceWorkerRegistrationId, ServiceWorkerRegistrationState,
13};
14use crate::error::BrowserError;
15use crate::page::PageHandle;
16use crate::page_pool::PagePool;
17use crate::screenshot::ScreenshotFormat;
18
19pub fn handle_bridge_command(cmd: BridgeCommand, pool: &PagePool) -> BridgeResponse {
21 let result = match cmd {
22 BridgeCommand::CreateTarget { url } => cmd_create_target(pool, &url),
24 BridgeCommand::ListTargets => cmd_list_targets(pool),
25
26 BridgeCommand::Navigate { target_id, url } => {
28 with_page(pool, &target_id, |page| cmd_navigate(page, &url))
29 }
30 BridgeCommand::EvaluateJs {
31 target_id,
32 expression,
33 return_by_value,
34 } => with_page(pool, &target_id, |page| {
35 cmd_evaluate(page, &expression, return_by_value)
36 }),
37 BridgeCommand::TakeScreenshot {
38 target_id,
39 format,
40 quality: _,
41 } => with_page(pool, &target_id, |page| cmd_screenshot(page, &format)),
42 BridgeCommand::GetTitle { target_id } => with_page(pool, &target_id, cmd_get_title),
43 BridgeCommand::GetUrl { target_id } => with_page(pool, &target_id, cmd_get_url),
44 BridgeCommand::GetDocument { target_id } => with_page(pool, &target_id, cmd_get_document),
45 BridgeCommand::QuerySelector {
46 target_id,
47 selector,
48 } => with_page(pool, &target_id, |page| cmd_query_selector(page, &selector)),
49 BridgeCommand::QuerySelectorAll {
50 target_id,
51 selector,
52 } => with_page(pool, &target_id, |page| {
53 cmd_query_selector_all(page, &selector)
54 }),
55 BridgeCommand::GetOuterHtml { target_id, .. } => {
56 with_page(pool, &target_id, cmd_get_outer_html)
57 }
58 BridgeCommand::SetAttributeValue {
59 target_id,
60 node_id: _,
61 name,
62 value,
63 } => with_page(pool, &target_id, |page| {
64 cmd_set_attribute(page, &name, &value)
65 }),
66 BridgeCommand::DispatchMouseEvent {
67 target_id,
68 event_type,
69 x,
70 y,
71 button,
72 click_count,
73 } => with_page(pool, &target_id, |page| {
74 cmd_mouse_event(page, &event_type, x, y, button, click_count)
75 }),
76 BridgeCommand::DispatchKeyEvent {
77 target_id,
78 event_type,
79 key,
80 code,
81 text,
82 } => with_page(pool, &target_id, |page| {
83 cmd_key_event(page, &event_type, &key, &code, text.as_deref())
84 }),
85 BridgeCommand::InsertText { target_id, text } => {
86 with_page(pool, &target_id, |page| cmd_insert_text(page, &text))
87 }
88 BridgeCommand::SetViewport {
89 target_id,
90 width,
91 height,
92 device_scale_factor: _,
93 } => with_page(pool, &target_id, |page| {
94 cmd_set_viewport(page, width, height)
95 }),
96 BridgeCommand::SetUserAgent {
97 target_id,
98 user_agent,
99 } => with_page(pool, &target_id, |page| {
100 cmd_set_user_agent(page, &user_agent)
101 }),
102 BridgeCommand::AddScriptToEvaluateOnNewDocument { target_id, source } => {
103 with_page(pool, &target_id, |page| cmd_add_script(page, &source))
104 }
105 BridgeCommand::Reload {
106 target_id,
107 ignore_cache: _,
108 } => with_page(pool, &target_id, cmd_reload),
109 BridgeCommand::GoBack { target_id } => with_page(pool, &target_id, cmd_go_back),
110 BridgeCommand::GoForward { target_id } => with_page(pool, &target_id, cmd_go_forward),
111 BridgeCommand::StopLoading { .. } => Err(
114 "Page.stopLoading not supported: servo WebView has no stop-loading API".into(),
115 ),
116 BridgeCommand::ClosePage { target_id } => {
117 let id = parse_target_id(&target_id);
118 match id {
119 Some(id) => {
120 let _ = pool.close_page(id);
121 Ok(serde_json::json!({}))
122 }
123 None => Err(format!("invalid target_id: {target_id}")),
124 }
125 }
126 BridgeCommand::GetCookies { target_id, urls } => {
128 with_page(pool, &target_id, |page| cmd_get_cookies(page, &urls))
129 }
130 BridgeCommand::GetAllCookies { target_id } => {
131 with_page(pool, &target_id, cmd_get_all_cookies)
132 }
133 BridgeCommand::DeleteCookie {
134 target_id,
135 name,
136 url,
137 } => with_page(pool, &target_id, |page| {
138 cmd_delete_cookie(page, &name, url.as_deref())
139 }),
140 BridgeCommand::SetCookie {
141 target_id,
142 name,
143 value,
144 url,
145 domain,
146 } => with_page(pool, &target_id, |page| {
147 cmd_set_cookie(page, &name, &value, url.as_deref(), domain.as_deref())
148 }),
149 BridgeCommand::GetResponseBody { .. } => Err(
152 "Network.getResponseBody not supported: servo does not expose stored response bodies to the embedder".into(),
153 ),
154
155 BridgeCommand::NetworkEnable { .. } => ok_empty(),
157 BridgeCommand::NetworkDisable { .. } => ok_empty(),
158 BridgeCommand::NetworkSetCacheDisabled {
159 target_id,
160 cache_disabled,
161 } => with_page(pool, &target_id, |page| {
162 cmd_network_set_cache_disabled(page, cache_disabled)
163 }),
164 BridgeCommand::NetworkSetExtraHTTPHeaders { .. } => Err(
167 "Network.setExtraHTTPHeaders not supported: servo WebView has no extra-headers injection API".into(),
168 ),
169 BridgeCommand::NetworkClearBrowserCache { target_id } => {
170 with_page(pool, &target_id, cmd_network_clear_browser_cache)
171 }
172 BridgeCommand::NetworkClearBrowserCookies { target_id } => {
173 with_page(pool, &target_id, cmd_network_clear_browser_cookies)
174 }
175
176 BridgeCommand::StorageGetStorageItemsForOrigin {
178 target_id,
179 origin,
180 storage_type,
181 } => with_page(pool, &target_id, |page| {
182 cmd_storage_get_items(page, origin, storage_type)
183 }),
184 BridgeCommand::StorageClearDataForOrigin {
185 target_id,
186 origin,
187 storage_type,
188 } => with_page(pool, &target_id, |page| {
189 cmd_storage_clear_data(page, origin, storage_type)
190 }),
191
192 BridgeCommand::SecurityEnable { .. } => ok_empty(),
194 BridgeCommand::SecurityDisable { .. } => ok_empty(),
195 BridgeCommand::SecuritySetOverrideCertificateErrors { .. } => Err(
199 "Security.setOverrideCertificateErrors not supported at runtime: certificate-error override is startup-only (BaoConfig.ignore_certificate_errors)".into(),
200 ),
201
202 BridgeCommand::DebuggerEnable { target_id } => {
209 with_page(pool, &target_id, |page| cmd_debugger_enable(page))
210 }
211 BridgeCommand::DebuggerDisable { target_id } => {
212 with_page(pool, &target_id, |page| cmd_debugger_disable(page))
213 }
214 BridgeCommand::DebuggerSetBreakpoint {
215 target_id,
216 url,
217 url_regex,
218 line,
219 column,
220 } => with_page(pool, &target_id, |page| {
221 cmd_debugger_set_breakpoint(page, url.as_deref(), url_regex.as_deref(), line, column)
222 }),
223 BridgeCommand::DebuggerRemoveBreakpoint {
224 target_id,
225 breakpoint_id,
226 } => with_page(pool, &target_id, |page| {
227 cmd_debugger_remove_breakpoint(page, &breakpoint_id)
228 }),
229 BridgeCommand::DebuggerInterrupt { target_id } => {
230 with_page(pool, &target_id, |page| cmd_debugger_interrupt(page))
231 }
232 BridgeCommand::DebuggerResume {
233 target_id,
234 step_type,
235 } => with_page(pool, &target_id, |page| {
236 cmd_debugger_resume(page, step_type.as_deref())
237 }),
238 BridgeCommand::DebuggerListFrames { target_id } => {
239 with_page(pool, &target_id, |page| cmd_debugger_list_frames(page))
240 }
241 BridgeCommand::DebuggerGetEnvironment { target_id, .. } => {
242 with_page(pool, &target_id, |page| cmd_debugger_get_environment(page))
243 }
244 BridgeCommand::DebuggerEval {
245 target_id,
246 expression,
247 frame_actor_id: _,
248 } => with_page(pool, &target_id, |page| {
249 cmd_evaluate(page, &expression, true)
250 }),
251 BridgeCommand::DebuggerGetPossibleBreakpoints {
252 target_id,
253 start_script_id,
254 } => with_page(pool, &target_id, |page| {
255 cmd_debugger_get_possible_breakpoints(page, &start_script_id)
256 }),
257 BridgeCommand::DebuggerGetScriptSource {
258 target_id,
259 script_id,
260 } => with_page(pool, &target_id, |page| {
261 cmd_debugger_get_script_source(page, script_id)
262 }),
263 BridgeCommand::DebuggerBlackbox { target_id, .. } => {
264 with_page(pool, &target_id, |page| cmd_debugger_blackbox(page))
265 }
266 BridgeCommand::DebuggerUnblackbox { target_id, .. } => {
267 with_page(pool, &target_id, |page| cmd_debugger_unblackbox(page))
268 }
269 BridgeCommand::ProfilerStart { .. }
274 | BridgeCommand::ProfilerStop { .. }
275 | BridgeCommand::ProfilerSetSamplingInterval { .. } => Err(
276 "Profiler not supported: SpiderMonkey sampling profiler is not exposed through the mozjs FFI surface".into(),
277 ),
278 BridgeCommand::HeapProfilerTakeSnapshot { .. }
281 | BridgeCommand::HeapProfilerStartTracking { .. }
282 | BridgeCommand::HeapProfilerStopTracking { .. } => Err(
283 "HeapProfiler snapshot/tracking not supported: mozjs FFI exposes no heap-snapshot API".into(),
284 ),
285 BridgeCommand::HeapProfilerCollectGarbage { target_id } => {
289 with_page(pool, &target_id, cmd_collect_garbage)
290 }
291 BridgeCommand::MemoryGetDOMCounters { .. } => Err(
295 "Memory.getDOMCounters not supported: jsEventListeners count is not introspectable in SpiderMonkey".into(),
296 ),
297 BridgeCommand::MemoryPurgeJS { target_id } => {
298 with_page(pool, &target_id, cmd_collect_garbage)
299 }
300 BridgeCommand::PerformanceGetMetrics { target_id } => {
302 with_page(pool, &target_id, cmd_performance_get_metrics)
303 }
304
305 BridgeCommand::CssGetComputedStyleForNode { target_id, node_id } => {
307 with_page(pool, &target_id, |page| {
308 cmd_css_get_computed_style(page, node_id)
309 })
310 }
311 BridgeCommand::CssGetMatchedStylesForNode { target_id, node_id } => {
312 with_page(pool, &target_id, |page| {
313 cmd_css_get_matched_styles(page, node_id)
314 })
315 }
316 BridgeCommand::CssGetInlineStylesForNode { target_id, node_id } => {
317 with_page(pool, &target_id, |page| {
318 cmd_css_get_inline_styles(page, node_id)
319 })
320 }
321
322 BridgeCommand::RuntimeGetProperties {
324 target_id,
325 object_id,
326 own_properties,
327 } => with_page(pool, &target_id, |page| {
328 cmd_runtime_get_properties(page, &object_id, own_properties)
329 }),
330 BridgeCommand::RuntimeCallFunctionOn {
331 target_id,
332 object_id,
333 execution_context_id,
334 function_declaration,
335 arguments,
336 return_by_value,
337 await_promise,
338 object_group,
339 } => with_page(pool, &target_id, |page| {
340 cmd_runtime_call_function_on(
341 page,
342 object_id.as_deref(),
343 execution_context_id,
344 &function_declaration,
345 arguments.as_ref(),
346 return_by_value,
347 await_promise,
348 object_group.as_deref(),
349 )
350 }),
351 BridgeCommand::RuntimeReleaseObject {
352 target_id,
353 object_id,
354 } => with_page(pool, &target_id, |page| {
355 cmd_runtime_release_object(page, &object_id)
356 }),
357 BridgeCommand::RuntimeReleaseObjectGroup {
358 target_id,
359 object_group,
360 } => with_page(pool, &target_id, |page| {
361 cmd_runtime_release_object_group(page, &object_group)
362 }),
363
364 BridgeCommand::TerminateServiceWorker {
367 target_id,
368 registration_id,
369 } => with_page(pool, &target_id, |page| {
370 cmd_terminate_service_worker(page, ®istration_id)
371 }),
372
373 BridgeCommand::ListWorkerTargets { target_id } => {
376 with_page(pool, &target_id, |page| {
377 let state = page.webview_state();
382 let st = state.borrow();
383 let mut workers: Vec<Value> = st
384 .dedicated_worker_scopes()
385 .into_iter()
386 .map(|scope| worker_target_json(&scope.worker_id.0, "worker"))
387 .collect();
388 workers.extend(
389 st.shared_worker_scopes()
390 .into_iter()
391 .map(|scope| worker_target_json(&scope.shared_worker_id.script_url, "shared_worker")),
392 );
393 Ok(serde_json::json!({ "workerTargets": workers }))
394 })
395 }
396 BridgeCommand::GetWorkerTargetInfo {
397 target_id,
398 worker_id,
399 } => with_page(pool, &target_id, |page| {
400 let state = page.webview_state();
404 let st = state.borrow();
405 if let Some(scope) = st.dedicated_worker_scope_by_url(&worker_id) {
406 return Ok(serde_json::json!({
407 "targetInfo": worker_target_json(&scope.worker_id.0, "worker")
408 }));
409 }
410 if let Some(scope) = st.shared_worker_scope_by_script_url(&worker_id) {
411 return Ok(serde_json::json!({
412 "targetInfo": worker_target_json(
413 &scope.shared_worker_id.script_url,
414 "shared_worker"
415 )
416 }));
417 }
418 Err(format!("unknown worker targetId: {worker_id}"))
419 }),
420 BridgeCommand::ListServiceWorkerRegistrations { target_id } => {
421 with_page(pool, &target_id, |page| {
422 let state = page.webview_state();
426 let st = state.borrow();
427 let registrations: Vec<Value> = st
428 .controlling_service_worker()
429 .map(|h| sw_registration_to_json(h.clone()))
430 .into_iter()
431 .collect();
432 Ok(serde_json::json!({ "registrations": registrations }))
433 })
434 }
435 BridgeCommand::GetServiceWorkerRegistrationInfo {
436 target_id,
437 registration_id,
438 } => {
439 with_page(pool, &target_id, |page| {
440 cmd_sw_registration_info(page, ®istration_id)
441 })
442 }
443 BridgeCommand::StopServiceWorker {
444 target_id,
445 registration_id,
446 } => with_page(pool, &target_id, |page| {
447 cmd_terminate_service_worker(page, ®istration_id)
448 }),
449 };
450 BridgeResponse { result }
451}
452
453fn parse_target_id(target_id: &str) -> Option<usize> {
455 target_id.parse::<usize>().ok()
456}
457
458fn with_page<F>(pool: &PagePool, target_id: &str, f: F) -> Result<Value, String>
460where
461 F: FnOnce(&PageHandle) -> Result<Value, String>,
462{
463 let id = parse_target_id(target_id).ok_or_else(|| format!("invalid target_id: {target_id}"))?;
464 let page = pool
465 .get_page(id)
466 .ok_or_else(|| format!("page not found: {target_id}"))?;
467 f(&page)
468}
469
470fn parse_sw_registration_id(registration_id: &str) -> Result<ServiceWorkerRegistrationId, String> {
477 let parts: Vec<&str> = registration_id.splitn(2, "::").collect();
478 if parts.len() != 2 {
479 return Err(format!(
480 "invalid registrationId format: {registration_id} (expected 'script_url::scope')"
481 ));
482 }
483 Ok(ServiceWorkerRegistrationId {
484 script_url: parts[0].to_string(),
485 scope: parts[1].to_string(),
486 })
487}
488
489fn sw_registration_to_json(handle: ServiceWorkerHandle) -> Value {
493 let is_active = handle.registration_state() == ServiceWorkerRegistrationState::Activated;
496 serde_json::json!({
497 "registrationId": format!("{}::{}", handle.script_url, handle.scope),
498 "scriptURL": handle.script_url,
499 "scope": handle.scope,
500 "state": match handle.registration_state() {
501 ServiceWorkerRegistrationState::Idle => "idle",
502 ServiceWorkerRegistrationState::Installing => "installing",
503 ServiceWorkerRegistrationState::Installed => "installed",
504 ServiceWorkerRegistrationState::Activating => "activating",
505 ServiceWorkerRegistrationState::Activated => "activated",
506 ServiceWorkerRegistrationState::Redundant => "redundant",
507 },
508 "isFetchIntercepting": handle.is_intercepting_fetch(),
509 "isActive": is_active,
510 })
511}
512
513fn cmd_create_target(pool: &PagePool, url: &str) -> Result<Value, String> {
514 let config = PageConfig {
515 url: if url.is_empty() {
516 None
517 } else {
518 Some(url.to_string())
519 },
520 ..Default::default()
521 };
522 let page = pool.create_page(&config).map_err(|e| format!("{e}"))?;
523 let page_id = page.id();
524 Ok(serde_json::json!({ "targetId": page_id.to_string() }))
525}
526
527fn cmd_list_targets(pool: &PagePool) -> Result<Value, String> {
528 let stats = pool.stats();
533 let mut targets: Vec<Value> = Vec::new();
534 for id in 1..=(stats.active + stats.idle) {
535 if let Some(page) = pool.get_page(id) {
536 targets.push(serde_json::json!({
537 "id": id.to_string(),
538 "title": page.page_title().unwrap_or_default(),
539 "url": page.current_url().unwrap_or_else(|| "about:blank".into()),
540 }));
541 }
542 }
543 Ok(serde_json::json!(targets))
544}
545
546fn worker_target_json(target_id: &str, target_type: &str) -> Value {
550 serde_json::json!({
551 "targetId": target_id,
552 "type": target_type,
553 "title": target_id,
554 "url": target_id,
555 "attached": false,
556 })
557}
558
559fn to_browser_error(e: BrowserError) -> String {
560 format!("{e}")
561}
562
563fn next_cdp_id(prefix: &str) -> String {
569 static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
570 let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
571 format!("{prefix}-{n:016x}")
572}
573
574fn cmd_navigate(page: &PageHandle, url: &str) -> Result<Value, String> {
575 page.navigate(url).map_err(to_browser_error)?;
576 Ok(serde_json::json!({
577 "frameId": page.id().to_string(),
578 "loaderId": next_cdp_id("loader"),
579 }))
580}
581
582fn cmd_evaluate(
583 page: &PageHandle,
584 expression: &str,
585 return_by_value: bool,
586) -> Result<Value, String> {
587 let result = page.evaluate_js_web(expression).map_err(to_browser_error)?;
592 if return_by_value {
593 let parsed: Result<Value, _> = serde_json::from_str(&result);
594 let (value_type, value) = match parsed {
595 Ok(v) => (json_type(&v), v),
596 Err(_) => (json_type_string(&result), serde_json::json!(result)),
597 };
598 Ok(serde_json::json!({
599 "result": {
600 "type": value_type,
601 "value": value,
602 },
603 "exceptionDetails": null
604 }))
605 } else {
606 page.evaluate_js_web(CDP_REGISTRY_PRELUDE)
611 .map_err(to_browser_error)?;
612 let expr_json = serde_json::to_string(expression).unwrap_or_default();
613 let js = format!(
614 r#"(function() {{
615 try {{
616 var r = eval({expr_json});
617 return JSON.stringify({{ result: window.__bao_cdp.wrap(r, false, ''), exceptionDetails: null }});
618 }} catch (e) {{
619 var exObj = (e !== null && typeof e === 'object') ? window.__bao_cdp.wrap(e, false, '') : undefined;
620 return JSON.stringify({{ result: {{ type: 'undefined' }}, exceptionDetails: {{ text: String((e && e.message) || e), exception: exObj, exceptionId: 0 }} }});
621 }}
622 }})()"#,
623 );
624 let out = page.evaluate_js_web(&js).map_err(to_browser_error)?;
625 serde_json::from_str(&out).map_err(|e| {
626 format!("Runtime.evaluate: handle wrapper unparseable: {e} (got: {out:.200})")
627 })
628 }
629}
630
631fn cmd_screenshot(page: &PageHandle, format: &str) -> Result<Value, String> {
632 let fmt = match format {
633 "jpeg" => ScreenshotFormat::Jpeg,
634 "webp" => ScreenshotFormat::WebP,
635 _ => ScreenshotFormat::Png,
636 };
637 let bytes = page.take_screenshot(fmt).map_err(to_browser_error)?;
638 let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
639 Ok(serde_json::json!({ "data": b64 }))
640}
641
642fn cmd_get_title(page: &PageHandle) -> Result<Value, String> {
643 let title = page.page_title().unwrap_or_default();
644 Ok(serde_json::json!(title))
645}
646
647fn cmd_get_url(page: &PageHandle) -> Result<Value, String> {
648 let url = page.current_url().unwrap_or_else(|| "about:blank".into());
649 Ok(serde_json::json!(url))
650}
651
652fn cmd_get_document(page: &PageHandle) -> Result<Value, String> {
653 let js = r#"
655 (function() {
656 function walk(node, id) {
657 var result = {
658 nodeId: id,
659 backendNodeId: id,
660 nodeType: node.nodeType,
661 nodeName: node.nodeName,
662 localName: node.localName || '',
663 nodeValue: node.nodeValue || '',
664 };
665 if (node.childNodes && node.childNodes.length > 0) {
666 result.childNodeCount = node.childNodes.length;
667 result.children = [];
668 for (var i = 0; i < Math.min(node.childNodes.length, 20); i++) {
669 result.children.push(walk(node.childNodes[i], id * 100 + i + 1));
670 }
671 }
672 return result;
673 }
674 return JSON.stringify(walk(document, 1));
675 })()
676 "#;
677 let doc_str = page.evaluate_js(js).map_err(to_browser_error)?;
678 let doc_val: Value = serde_json::from_str(&doc_str).unwrap_or_else(|_| serde_json::json!({}));
679 Ok(serde_json::json!({ "root": doc_val }))
680}
681
682fn cmd_query_selector(page: &PageHandle, selector: &str) -> Result<Value, String> {
683 let js = format!(
684 "(function() {{ var e = document.querySelector({}); return e ? 1 : 0; }})()",
685 serde_json::to_string(selector).unwrap_or_default()
686 );
687 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
688 let node_id: i64 = result.trim().parse().unwrap_or(0);
689 Ok(serde_json::json!({ "nodeId": node_id }))
690}
691
692fn cmd_query_selector_all(page: &PageHandle, selector: &str) -> Result<Value, String> {
693 let js = format!(
694 "(function() {{ return document.querySelectorAll({}).length; }})()",
695 serde_json::to_string(selector).unwrap_or_default()
696 );
697 let count_str = page.evaluate_js(&js).map_err(to_browser_error)?;
698 let count: i64 = count_str.trim().parse().unwrap_or(0);
699 let ids: Vec<i64> = (1..=count).collect();
700 Ok(serde_json::json!({ "nodeIds": ids }))
701}
702
703fn cmd_get_outer_html(page: &PageHandle) -> Result<Value, String> {
704 let js = "document.documentElement.outerHTML";
705 let html = page.evaluate_js(js).map_err(to_browser_error)?;
706 Ok(serde_json::json!({ "outerHTML": html }))
707}
708
709fn cmd_set_attribute(page: &PageHandle, name: &str, value: &str) -> Result<Value, String> {
710 let js = format!(
711 "(function() {{ document.querySelector('[data-cdp]')?.setAttribute({}, {}); }})()",
712 serde_json::to_string(name).unwrap_or_default(),
713 serde_json::to_string(value).unwrap_or_default(),
714 );
715 let _ = page.evaluate_js(&js).map_err(to_browser_error)?;
716 Ok(serde_json::json!({}))
717}
718
719fn cmd_mouse_event(
720 _page: &PageHandle,
721 _event_type: &str,
722 _x: f64,
723 _y: f64,
724 _button: Option<i64>,
725 _click_count: Option<i64>,
726) -> Result<Value, String> {
727 Ok(serde_json::json!({}))
730}
731
732fn cmd_key_event(
733 _page: &PageHandle,
734 _event_type: &str,
735 _key: &str,
736 _code: &str,
737 _text: Option<&str>,
738) -> Result<Value, String> {
739 Ok(serde_json::json!({}))
740}
741
742fn cmd_insert_text(page: &PageHandle, text: &str) -> Result<Value, String> {
743 let js = format!(
744 "(function() {{ var el = document.activeElement; if (el && 'value' in el) el.value += {}; }})()",
745 serde_json::to_string(text).unwrap_or_default(),
746 );
747 let _ = page.evaluate_js(&js).map_err(to_browser_error)?;
748 Ok(serde_json::json!({}))
749}
750
751fn cmd_set_viewport(_page: &PageHandle, _width: u32, _height: u32) -> Result<Value, String> {
752 Ok(serde_json::json!({}))
754}
755
756fn cmd_set_user_agent(page: &PageHandle, ua: &str) -> Result<Value, String> {
757 let js = format!(
758 "Object.defineProperty(navigator, 'userAgent', {{ get: function() {{ return {}; }} }});",
759 serde_json::to_string(ua).unwrap_or_default(),
760 );
761 let _ = page.evaluate_js(&js).map_err(to_browser_error)?;
762 Ok(serde_json::json!({}))
763}
764
765fn cmd_add_script(page: &PageHandle, source: &str) -> Result<Value, String> {
766 page.add_script_to_evaluate_on_new_document(source)
772 .map_err(to_browser_error)?;
773 let _ = page.evaluate_js_web(source).map_err(to_browser_error)?;
776 Ok(serde_json::json!({ "identifier": next_cdp_id("script") }))
777}
778
779fn cmd_reload(page: &PageHandle) -> Result<Value, String> {
781 page.reload().map_err(to_browser_error)?;
782 Ok(serde_json::json!({
783 "frameId": page.id().to_string(),
784 "loaderId": next_cdp_id("loader"),
785 }))
786}
787
788fn cmd_go_back(page: &PageHandle) -> Result<Value, String> {
790 if !page.can_go_back() {
791 return Err("cannot go back: no previous entry in session history".into());
792 }
793 page.go_back().map_err(to_browser_error)?;
794 Ok(serde_json::json!({ "frameId": page.id().to_string() }))
795}
796
797fn cmd_go_forward(page: &PageHandle) -> Result<Value, String> {
799 if !page.can_go_forward() {
800 return Err("cannot go forward: no forward entry in session history".into());
801 }
802 page.go_forward().map_err(to_browser_error)?;
803 Ok(serde_json::json!({ "frameId": page.id().to_string() }))
804}
805
806fn cmd_collect_garbage(page: &PageHandle) -> Result<Value, String> {
811 page.evaluate_js_web("navigator.servo.GarbageCollectAllContexts()")
812 .map_err(to_browser_error)?;
813 Ok(serde_json::json!({}))
814}
815
816fn cmd_performance_get_metrics(page: &PageHandle) -> Result<Value, String> {
821 let js = r#"(function() {
822 return JSON.stringify({
823 Timestamp: Date.now(),
824 Documents: 1 + window.frames.length,
825 Frames: 1 + window.frames.length,
826 Nodes: document.getElementsByTagName('*').length
827 });
828 })()"#;
829 let result = page.evaluate_js(js).map_err(to_browser_error)?;
830 let v: Value = serde_json::from_str(result.trim())
831 .map_err(|e| format!("Performance.getMetrics: page did not return JSON: {e}"))?;
832 let metrics: Vec<Value> = v
833 .as_object()
834 .map(|o| {
835 o.iter()
836 .map(|(name, val)| serde_json::json!({ "name": name, "value": val }))
837 .collect()
838 })
839 .unwrap_or_default();
840 Ok(serde_json::json!({ "metrics": metrics }))
841}
842
843fn cmd_terminate_service_worker(
847 page: &PageHandle,
848 registration_id: &str,
849) -> Result<Value, String> {
850 let id = parse_sw_registration_id(registration_id)?;
851 let state = page.webview_state();
852 let mut st = state.borrow_mut();
853 let is_match = st
854 .controlling_service_worker()
855 .map(|h| h.script_url == id.script_url && h.scope == id.scope)
856 .unwrap_or(false);
857 if !is_match {
858 return Err(format!(
859 "no controlling ServiceWorker registration '{registration_id}'"
860 ));
861 }
862 if let Some(handle) = st.controlling_service_worker() {
863 handle.terminate();
864 }
865 st.clear_controlling_service_worker();
866 Ok(serde_json::json!({}))
867}
868
869fn cmd_sw_registration_info(
872 page: &PageHandle,
873 registration_id: &str,
874) -> Result<Value, String> {
875 let id = parse_sw_registration_id(registration_id)?;
876 let state = page.webview_state();
877 let st = state.borrow();
878 match st.controlling_service_worker() {
879 Some(handle) if handle.script_url == id.script_url && handle.scope == id.scope => {
880 Ok(serde_json::json!({
881 "registration": sw_registration_to_json(handle.clone())
882 }))
883 }
884 _ => Err(format!(
885 "no controlling ServiceWorker registration '{registration_id}'"
886 )),
887 }
888}
889
890const DEBUGGER_SETUP: &str = r#"
899(function() {
900 if (window.__bao_dbg_active) return;
901 window.__bao_dbg_active = true;
902 try {
903 const dbg = new Debugger();
904 window.__bao_dbg = dbg;
905 dbg.onNewScript = function(script) {
906 const info = JSON.stringify({
907 id: script.id || ('s-' + Date.now()),
908 url: script.url || '',
909 startLine: script.startLine || 0,
910 endLine: script.startLine + (script.lineCount || 1) - 1,
911 });
912 console.log('__BAO_EVT__Debugger.scriptParsed\n' + info);
913 };
914 dbg.onDebuggerStatement = function(frame) {
915 const callFrames = [];
916 let f = frame;
917 let idx = 0;
918 while (f && idx < 100) {
919 const s = f.script;
920 callFrames.push({
921 callFrameId: 'frame-' + idx + '-' + (s ? s.id : 'x'),
922 functionName: f.callee ? (f.callee.name || '(anonymous)') : '(anonymous)',
923 location: { scriptId: s ? String(s.id) : '', lineNumber: 0, columnNumber: 0 },
924 scopeChain: [{ type: 'local', object: { type: 'object', objectId: 'local-' + idx } }],
925 });
926 f = f.older;
927 idx++;
928 }
929 const paused = JSON.stringify({ callFrames, reason: 'debuggerStatement', hitBreakpoints: [] });
930 console.log('__BAO_EVT__Debugger.paused\n' + paused);
931 };
932 dbg.findScripts().forEach(function(script) {
933 const info = JSON.stringify({
934 id: script.id || ('s-' + Date.now()),
935 url: script.url || '',
936 startLine: script.startLine || 0,
937 endLine: script.startLine + (script.lineCount || 1) - 1,
938 });
939 console.log('__BAO_EVT__Debugger.scriptParsed\n' + info);
940 });
941 } catch(e) {}
942})();
943"#;
944
945fn cmd_debugger_enable(page: &PageHandle) -> Result<Value, String> {
946 let _ = page.evaluate_js(DEBUGGER_SETUP).map_err(to_browser_error)?;
947 Ok(serde_json::json!({}))
948}
949
950fn cmd_debugger_disable(page: &PageHandle) -> Result<Value, String> {
951 let js = "if (window.__bao_dbg) { window.__bao_dbg.onNewScript = undefined; window.__bao_dbg.onDebuggerStatement = undefined; window.__bao_dbg = null; window.__bao_dbg_active = false; }";
952 let _ = page.evaluate_js(js).map_err(to_browser_error)?;
953 Ok(serde_json::json!({}))
954}
955
956fn cmd_debugger_set_breakpoint(
957 page: &PageHandle,
958 url: Option<&str>,
959 url_regex: Option<&str>,
960 line: u32,
961 column: Option<u32>,
962) -> Result<Value, String> {
963 let col = column.unwrap_or(0);
964 let url_filter = match (url, url_regex) {
966 (Some(u), _) => format!("s.url === {}", serde_json::to_string(u).unwrap_or_default()),
967 (None, Some(r)) => format!(
968 "new RegExp({}).test(s.url)",
969 serde_json::to_string(r).unwrap_or_default()
970 ),
971 (None, None) => format!("s.startLine <= {line} && {line} <= s.startLine + s.lineCount - 1"),
972 };
973 let js = format!(
974 "(function() {{ try {{ if (!window.__bao_dbg) return '{{}}'; var scripts = window.__bao_dbg.findScripts(); for (var i = 0; i < scripts.length; i++) {{ var s = scripts[i]; if ({url_filter}) {{ var offset = s.offsetLine ? s.offsetLine({line}, {col}) : 0; var bpId = 'bp-' + String(s.id) + '-' + {line} + '-' + {col}; s.setBreakpoint(offset, {{ hit: function(frame) {{ console.log('__BAO_EVT__Debugger.paused\\n' + JSON.stringify({{ callFrames: [], reason: 'breakpoint', hitBreakpoints: [bpId] }})); }} }}); if (!window.__bao_bps) window.__bao_bps = {{}}; window.__bao_bps[bpId] = {{ scriptId: String(s.id), offset: offset }}; return JSON.stringify({{ breakpointId: bpId, actualLocation: {{ scriptId: String(s.id), lineNumber: {line}, columnNumber: {col} }} }}); }} }} }} catch(e) {{}} return '{{}}'; }})()",
975 url_filter = url_filter, line = line, col = col
976 );
977 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
978 parse_js_result(&result)
979}
980
981fn cmd_debugger_remove_breakpoint(page: &PageHandle, breakpoint_id: &str) -> Result<Value, String> {
982 let js = format!(
983 "(function() {{ try {{ if (!window.__bao_dbg) return; if (window.__bao_bps && window.__bao_bps[{}]) {{ var info = window.__bao_bps[{}]; var scripts = window.__bao_dbg.findScripts(); for (var i = 0; i < scripts.length; i++) {{ if (String(scripts[i].id) === info.scriptId) {{ scripts[i].clearAllBreakpoints(); break; }} }} delete window.__bao_bps[{}]; }} else {{ var scripts = window.__bao_dbg.findScripts(); scripts.forEach(function(s) {{ s.clearAllBreakpoints(); }}); }} }} catch(e) {{}} }})()",
984 serde_json::to_string(breakpoint_id).unwrap_or_default(),
985 serde_json::to_string(breakpoint_id).unwrap_or_default(),
986 serde_json::to_string(breakpoint_id).unwrap_or_default(),
987 );
988 let _ = page.evaluate_js(&js).map_err(to_browser_error)?;
989 Ok(serde_json::json!({}))
990}
991
992fn cmd_debugger_interrupt(page: &PageHandle) -> Result<Value, String> {
993 let js = "(function() { try { if (!window.__bao_dbg) return; window.__bao_dbg.onEnterFrame = function(frame) { window.__bao_dbg.onEnterFrame = undefined; frame.onStep = function() { frame.onStep = undefined; console.log('__BAO_EVT__Debugger.paused\n' + JSON.stringify({ callFrames: [], reason: 'interrupt', hitBreakpoints: [] })); return undefined; }; return undefined; }; } catch(e) {} })()";
994 let _ = page.evaluate_js(js).map_err(to_browser_error)?;
995 Ok(serde_json::json!({}))
996}
997
998fn cmd_debugger_resume(page: &PageHandle, step_type: Option<&str>) -> Result<Value, String> {
999 let js = match step_type {
1000 Some("next") => "(function() { try { if (window.__bao_dbg) { window.__bao_dbg.onEnterFrame = function(frame) { window.__bao_dbg.onEnterFrame = undefined; frame.onPop = function() { frame.onPop = undefined; console.log('__BAO_EVT__Debugger.paused\n' + JSON.stringify({callFrames:[],reason:'step',hitBreakpoints:[]})); }; return undefined; }; } } catch(e) {} })()",
1001 Some("step") => "(function() { try { if (window.__bao_dbg) { window.__bao_dbg.onEnterFrame = function(frame) { window.__bao_dbg.onEnterFrame = undefined; frame.onStep = function() { frame.onStep = undefined; console.log('__BAO_EVT__Debugger.paused\n' + JSON.stringify({callFrames:[],reason:'step',hitBreakpoints:[]})); }; return undefined; }; } } catch(e) {} })()",
1002 Some("finish") => "(function() { try { if (window.__bao_dbg) { window.__bao_dbg.onEnterFrame = function(frame) { window.__bao_dbg.onEnterFrame = undefined; frame.onPop = function() { frame.onPop = undefined; console.log('__BAO_EVT__Debugger.paused\n' + JSON.stringify({callFrames:[],reason:'step',hitBreakpoints:[]})); }; return undefined; }; } } catch(e) {} })()",
1003 _ => "(function() { /* resume: clear step hooks */ try { if (window.__bao_dbg) { window.__bao_dbg.onEnterFrame = undefined; } } catch(e) {} })()",
1004 };
1005 let _ = page.evaluate_js(js).map_err(to_browser_error)?;
1006 Ok(serde_json::json!({}))
1007}
1008
1009fn cmd_debugger_list_frames(page: &PageHandle) -> Result<Value, String> {
1010 let js = "(function() { try { if (!window.__bao_dbg) return JSON.stringify({frames:[]}); var f = window.__bao_dbg.getNewestFrame(); var frames = []; var idx = 0; while (f && idx < 100) { frames.push({callFrameId: 'frame-' + idx, functionName: f.callee ? (f.callee.name || '(anonymous)') : '(anonymous)', location: {scriptId: f.script ? String(f.script.id) : '', lineNumber: 0}}); f = f.older; idx++; } return JSON.stringify({frames: frames}); } catch(e) { return JSON.stringify({frames: []}); } })()";
1011 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1012 parse_js_result(&result)
1013}
1014
1015fn cmd_debugger_get_environment(page: &PageHandle) -> Result<Value, String> {
1016 let js = "(function() { try { if (!window.__bao_dbg) return '{}'; var f = window.__bao_dbg.getNewestFrame(); if (!f || !f.environment) return '{}'; return JSON.stringify({environment: {}}); } catch(e) { return '{}'; } })()";
1017 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1018 parse_js_result(&result)
1019}
1020
1021fn cmd_debugger_get_possible_breakpoints(
1022 page: &PageHandle,
1023 start_script_id: &str,
1024) -> Result<Value, String> {
1025 let filter_by_script = if start_script_id.is_empty() {
1026 "true".to_string()
1027 } else {
1028 format!(
1029 "String(s.id) === {}",
1030 serde_json::to_string(start_script_id).unwrap_or_default()
1031 )
1032 };
1033 let js = format!(
1034 "(function() {{ try {{ if (!window.__bao_dbg) return JSON.stringify({{locations: []}}); var scripts = window.__bao_dbg.findScripts(); var locs = []; scripts.forEach(function(s) {{ if ({filter}) {{ for (var line = s.startLine; line < s.startLine + s.lineCount; line++) {{ locs.push({{scriptId: String(s.id), lineNumber: line}}); }} }} }}); return JSON.stringify({{locations: locs}}); }} catch(e) {{ return JSON.stringify({{locations: []}}); }} }})()",
1035 filter = filter_by_script
1036 );
1037 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1038 parse_js_result(&result)
1039}
1040
1041fn cmd_debugger_get_script_source(page: &PageHandle, script_id: u32) -> Result<Value, String> {
1042 let js = format!(
1043 "(function() {{ try {{ if (!window.__bao_dbg) return JSON.stringify({{scriptSource: ''}}); var scripts = window.__bao_dbg.findScripts(); for (var i = 0; i < scripts.length; i++) {{ if (String(scripts[i].id) === '{}') return JSON.stringify({{scriptSource: scripts[i].source.text || ''}}); }} return JSON.stringify({{scriptSource: ''}}); }} catch(e) {{ return JSON.stringify({{scriptSource: ''}}); }} }})()",
1044 script_id
1045 );
1046 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1047 parse_js_result(&result)
1048}
1049
1050fn cmd_debugger_blackbox(page: &PageHandle) -> Result<Value, String> {
1051 let _ = page
1052 .evaluate_js("(function() { /* blackbox: not yet supported */ })()")
1053 .map_err(to_browser_error)?;
1054 Ok(serde_json::json!({}))
1055}
1056
1057fn cmd_debugger_unblackbox(page: &PageHandle) -> Result<Value, String> {
1058 let _ = page
1059 .evaluate_js("(function() { /* unblackbox: not yet supported */ })()")
1060 .map_err(to_browser_error)?;
1061 Ok(serde_json::json!({}))
1062}
1063
1064fn resolve_node_by_id(page: &PageHandle, node_id: i64) -> Result<String, String> {
1072 if node_id <= 0 {
1073 let js: String = match node_id {
1075 0 | 1 => "document".to_string(),
1076 2 => "document.documentElement".to_string(),
1077 _ => {
1078 let idx = node_id - 3;
1079 format!("document.documentElement.childNodes[{}]", idx)
1080 }
1081 };
1082 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1083 Ok(result)
1084 } else {
1085 let js = format!(
1087 "(function() {{ var el = document.querySelector('[data-node-id=\"{}\"]'); if (el) return 'found'; return 'not-found'; }})()",
1088 node_id
1089 );
1090 let found = page.evaluate_js(&js).map_err(to_browser_error)?;
1091 if found.trim() == "found" {
1092 Ok(format!(
1093 "document.querySelector('[data-node-id=\"{}\"]')",
1094 node_id
1095 ))
1096 } else {
1097 Ok(format!("document.body.childNodes[{}]", node_id - 3))
1099 }
1100 }
1101}
1102
1103fn cmd_css_get_computed_style(page: &PageHandle, node_id: i64) -> Result<Value, String> {
1104 let node_ref = resolve_node_by_id(page, node_id)?;
1105 let js = format!(
1106 r#"(function() {{
1107 var el = {node_ref};
1108 if (!el || !el.nodeType || el.nodeType !== 1) return JSON.stringify({{"computedStyle": []}});
1109 try {{
1110 var styles = getComputedStyle(el);
1111 var result = [];
1112 for (var i = 0; i < styles.length; i++) {{
1113 var name = styles[i];
1114 result.push({{ name: name, value: styles.getPropertyValue(name) }});
1115 }}
1116 return JSON.stringify({{"computedStyle": result}});
1117 }} catch(e) {{
1118 return JSON.stringify({{"computedStyle": []}});
1119 }}
1120 }})()"#,
1121 node_ref = node_ref
1122 );
1123 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1124 parse_js_result(&result)
1125}
1126
1127fn cmd_css_get_matched_styles(page: &PageHandle, node_id: i64) -> Result<Value, String> {
1128 let node_ref = resolve_node_by_id(page, node_id)?;
1129 let js = format!(
1130 r#"(function() {{
1131 var el = {node_ref};
1132 if (!el || !el.nodeType || el.nodeType !== 1) return JSON.stringify({{"matchedCSSRules": [], "inlineStyle": null, "attributesStyle": null}});
1133 try {{
1134 var rules = [];
1135 var sheets = document.styleSheets;
1136 for (var s = 0; s < sheets.length; s++) {{
1137 try {{
1138 var cssRules = sheets[s].cssRules || sheets[s].rules;
1139 for (var r = 0; r < cssRules.length; r++) {{
1140 try {{
1141 if (cssRules[r].selectorText && el.matches(cssRules[r].selectorText)) {{
1142 var rule = {{
1143 rule: {{
1144 selectorList: {{ selectors: [{{ text: cssRules[r].selectorText }}] }},
1145 style: {{ cssProperties: [], shorthandEntries: [] }},
1146 origin: sheets[s].href ? "regular" : "user-agent",
1147 sourceURL: sheets[s].href || ""
1148 }},
1149 matchingSelectors: [r]
1150 }};
1151 var decls = cssRules[r].style;
1152 for (var d = 0; d < decls.length; d++) {{
1153 rule.rule.style.cssProperties.push({{
1154 name: decls[d],
1155 value: decls.getPropertyValue(decls[d]),
1156 important: decls.getPropertyPriority(decls[d]) === "important"
1157 }});
1158 }}
1159 rules.push(rule);
1160 }}
1161 }} catch(e2) {{}}
1162 }}
1163 }} catch(e1) {{}}
1164 }}
1165 var inlineStyle = null;
1166 if (el.style && el.style.length > 0) {{
1167 inlineStyle = {{ cssProperties: [], shorthandEntries: [] }};
1168 for (var i = 0; i < el.style.length; i++) {{
1169 inlineStyle.cssProperties.push({{
1170 name: el.style[i],
1171 value: el.style.getPropertyValue(el.style[i]),
1172 important: el.style.getPropertyPriority(el.style[i]) === "important"
1173 }});
1174 }}
1175 }}
1176 return JSON.stringify({{"matchedCSSRules": rules, "inlineStyle": inlineStyle, "attributesStyle": null}});
1177 }} catch(e) {{
1178 return JSON.stringify({{"matchedCSSRules": [], "inlineStyle": null, "attributesStyle": null}});
1179 }}
1180 }})()"#,
1181 node_ref = node_ref
1182 );
1183 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1184 parse_js_result(&result)
1185}
1186
1187fn cmd_css_get_inline_styles(page: &PageHandle, node_id: i64) -> Result<Value, String> {
1188 let node_ref = resolve_node_by_id(page, node_id)?;
1189 let js = format!(
1190 r#"(function() {{
1191 var el = {node_ref};
1192 if (!el || !el.nodeType || el.nodeType !== 1) return JSON.stringify({{"inlineStyle": null}});
1193 try {{
1194 var inlineStyle = null;
1195 if (el.style && el.style.length > 0) {{
1196 inlineStyle = {{ cssProperties: [], shorthandEntries: [] }};
1197 for (var i = 0; i < el.style.length; i++) {{
1198 inlineStyle.cssProperties.push({{
1199 name: el.style[i],
1200 value: el.style.getPropertyValue(el.style[i]),
1201 important: el.style.getPropertyPriority(el.style[i]) === "important"
1202 }});
1203 }}
1204 }}
1205 var attributesStyle = null;
1206 if (el.getAttribute('style')) {{
1207 attributesStyle = {{ cssProperties: [], shorthandEntries: [] }};
1208 var styleText = el.getAttribute('style');
1209 var pairs = styleText.split(';');
1210 for (var p = 0; p < pairs.length; p++) {{
1211 var kv = pairs[p].trim();
1212 if (kv) {{
1213 var colon = kv.indexOf(':');
1214 if (colon > 0) {{
1215 var name = kv.substring(0, colon).trim();
1216 var value = kv.substring(colon + 1).trim();
1217 var important = value.endsWith(' !important');
1218 if (important) value = value.substring(0, value.length - 11).trim();
1219 attributesStyle.cssProperties.push({{
1220 name: name, value: value, important: important
1221 }});
1222 }}
1223 }}
1224 }}
1225 }}
1226 return JSON.stringify({{"inlineStyle": inlineStyle, "attributesStyle": attributesStyle}});
1227 }} catch(e) {{
1228 return JSON.stringify({{"inlineStyle": null}});
1229 }}
1230 }})()"#,
1231 node_ref = node_ref
1232 );
1233 let result = page.evaluate_js(&js).map_err(to_browser_error)?;
1234 parse_js_result(&result)
1235}
1236
1237const CDP_REGISTRY_PRELUDE: &str = r#"(function() {
1253 if (window.__bao_cdp) return 'ok';
1254 var objs = Object.create(null);
1255 var seq = 0;
1256 function alloc(v, group) {
1257 seq++;
1258 var oid = 'obj-' + seq + '-' + Math.floor(Math.random() * 1e9).toString(36);
1259 objs[oid] = { v: v, g: group || '' };
1260 return oid;
1261 }
1262 function get(oid) { return (oid in objs) ? objs[oid].v : undefined; }
1263 function release(oid) { delete objs[oid]; }
1264 function releaseGroup(g) { for (var k in objs) { if (objs[k].g === g) delete objs[k]; } }
1265 function wrap(v, byValue, group) {
1266 var ro;
1267 if (v === null) {
1268 ro = { type: 'object', subtype: 'null', description: 'null' };
1269 } else if (v === undefined) {
1270 ro = { type: 'undefined' };
1271 } else {
1272 var t = typeof v;
1273 if (t === 'number') {
1274 var us = null;
1275 if (v !== v) us = 'NaN';
1276 else if (v === Infinity) us = 'Infinity';
1277 else if (v === -Infinity) us = '-Infinity';
1278 else if (v === 0 && 1 / v < 0) us = '-0';
1279 ro = us
1280 ? { type: 'number', unserializableValue: us, description: us }
1281 : { type: 'number', value: v, description: String(v) };
1282 } else if (t === 'string') {
1283 ro = { type: 'string', value: v };
1284 } else if (t === 'boolean') {
1285 ro = { type: 'boolean', value: v };
1286 } else if (t === 'bigint') {
1287 ro = { type: 'bigint', unserializableValue: String(v), description: String(v) + 'n' };
1288 } else if (t === 'symbol') {
1289 ro = { type: 'symbol', description: String(v) };
1290 } else if (t === 'function') {
1291 ro = { type: 'function', className: 'Function', description: (v.name ? 'function ' + v.name + '()' : 'function ()'), objectId: alloc(v, group) };
1292 } else {
1293 var sub;
1294 if (Array.isArray(v)) sub = 'array';
1295 else if (v instanceof Date) sub = 'date';
1296 else if (v instanceof RegExp) sub = 'regexp';
1297 else if (v instanceof Error) sub = 'error';
1298 else if (v === window) sub = 'window';
1299 else if (typeof Node !== 'undefined' && v instanceof Node) sub = 'node';
1300 var desc;
1301 if (sub === 'array') desc = 'Array(' + v.length + ')';
1302 else if (sub === 'date') desc = String(v);
1303 else if (sub === 'regexp') desc = String(v);
1304 else if (sub === 'error') desc = ((v.constructor && v.constructor.name) ? (v.constructor.name + ': ') : '') + (v.message || '');
1305 else if (sub === 'node') { try { desc = v.nodeName.toLowerCase() + (v.id ? '#' + v.id : ''); } catch (e2) { desc = String(v.nodeName); } }
1306 else { try { desc = String(v); } catch (e2) { desc = 'Object'; } }
1307 var cn = 'Object';
1308 try { if (v.constructor && v.constructor.name) cn = v.constructor.name; } catch (e2) {}
1309 ro = { type: 'object', className: cn, description: desc, objectId: alloc(v, group) };
1310 if (sub) ro.subtype = sub;
1311 }
1312 }
1313 if (byValue && v !== null && v !== undefined && (typeof v === 'object' || typeof v === 'function')) {
1314 try { ro.value = v; } catch (e) {}
1315 }
1316 return ro;
1317 }
1318 window.__bao_cdp = { alloc: alloc, get: get, release: release, releaseGroup: releaseGroup, wrap: wrap };
1319 return 'ok';
1320})()"#;
1321
1322fn resolve_object_by_id(object_id: &str) -> String {
1327 if object_id.starts_with("node-") {
1328 let idx: i64 = object_id[5..].parse().unwrap_or(0);
1329 match idx {
1330 0 | 1 => "document".to_string(),
1331 2 => "document.documentElement".to_string(),
1332 _ => format!("document.body.childNodes[{}]", idx - 3),
1333 }
1334 } else {
1335 format!(
1336 "window.__bao_cdp.get({})",
1337 serde_json::to_string(object_id).unwrap_or_default()
1338 )
1339 }
1340}
1341
1342fn cmd_runtime_get_properties(
1343 page: &PageHandle,
1344 object_id: &str,
1345 own_properties: Option<bool>,
1346) -> Result<Value, String> {
1347 page.evaluate_js_web(CDP_REGISTRY_PRELUDE)
1348 .map_err(to_browser_error)?;
1349 let obj_ref = resolve_object_by_id(object_id);
1350 let own = own_properties.unwrap_or(true);
1351 let js = format!(
1359 r#"(function() {{
1360 try {{
1361 var obj = {obj_ref};
1362 if (obj === null || obj === undefined) return JSON.stringify({{ "result": [] }});
1363 var names;
1364 if ({own}) {{
1365 names = Object.getOwnPropertyNames(obj);
1366 }} else {{
1367 names = [];
1368 for (var n in obj) names.push(n);
1369 }}
1370 var result = [];
1371 for (var i = 0; i < names.length; i++) {{
1372 var name = names[i];
1373 try {{
1374 var desc = Object.getOwnPropertyDescriptor(obj, name);
1375 if (!desc) continue;
1376 var entry = {{ name: name, configurable: !!desc.configurable, enumerable: !!desc.enumerable, isOwn: {own} }};
1377 if ('value' in desc) {{
1378 entry.value = window.__bao_cdp.wrap(desc.value, false, '');
1379 entry.writable = !!desc.writable;
1380 }} else {{
1381 if (desc.get) entry.get = window.__bao_cdp.wrap(desc.get, false, '');
1382 if (desc.set) entry.set = window.__bao_cdp.wrap(desc.set, false, '');
1383 }}
1384 result.push(entry);
1385 }} catch (e2) {{
1386 result.push({{ name: name, value: {{ type: 'undefined' }}, configurable: false, enumerable: false }});
1387 }}
1388 }}
1389 return JSON.stringify({{ "result": result }});
1390 }} catch (e) {{
1391 return JSON.stringify({{ "result": [] }});
1392 }}
1393 }})()"#,
1394 obj_ref = obj_ref,
1395 own = own,
1396 );
1397 let result = page.evaluate_js_web(&js).map_err(to_browser_error)?;
1398 parse_js_result(&result)
1399}
1400
1401fn call_argument_expr(arg: &Value) -> Result<String, String> {
1405 let Some(obj) = arg.as_object() else {
1406 return Err("callFunctionOn arguments entries must be CallArgument objects".into());
1407 };
1408 if let Some(v) = obj.get("value") {
1409 return Ok(serde_json::to_string(v).unwrap_or_else(|_| "undefined".into()));
1410 }
1411 if let Some(us) = obj.get("unserializableValue").and_then(|v| v.as_str()) {
1412 return match us {
1413 "NaN" | "Infinity" | "-Infinity" | "-0" => Ok(us.to_string()),
1414 _ if us.ends_with('n') && us.len() >= 2 => {
1415 let digits = us[..us.len() - 1]
1416 .strip_prefix('-')
1417 .unwrap_or(&us[..us.len() - 1]);
1418 if !digits.is_empty() && digits.chars().all(|c| c.is_ascii_digit()) {
1419 Ok(us.to_string())
1421 } else {
1422 Err(format!("unsupported unserializableValue: {us}"))
1423 }
1424 }
1425 _ => Err(format!("unsupported unserializableValue: {us}")),
1426 };
1427 }
1428 if let Some(oid) = obj.get("objectId").and_then(|v| v.as_str()) {
1429 return Ok(resolve_object_by_id(oid));
1430 }
1431 Ok("undefined".to_string())
1432}
1433
1434fn cmd_runtime_call_function_on(
1435 page: &PageHandle,
1436 object_id: Option<&str>,
1437 execution_context_id: Option<i64>,
1438 function_declaration: &str,
1439 arguments: Option<&Value>,
1440 return_by_value: Option<bool>,
1441 await_promise: Option<bool>,
1442 object_group: Option<&str>,
1443) -> Result<Value, String> {
1444 page.evaluate_js_web(CDP_REGISTRY_PRELUDE)
1449 .map_err(to_browser_error)?;
1450
1451 let _ctx = execution_context_id; let this_expr = match object_id {
1457 Some(oid) => resolve_object_by_id(oid),
1458 None => "undefined".to_string(),
1459 };
1460
1461 let mut args_js = String::from("[");
1464 if let Some(Value::Array(arr)) = arguments {
1465 let parts: Vec<String> = arr
1466 .iter()
1467 .map(call_argument_expr)
1468 .collect::<Result<_, _>>()?;
1469 args_js.push_str(&parts.join(", "));
1470 } else if let Some(other) = arguments {
1471 return Err(format!(
1472 "callFunctionOn arguments must be an array, got: {other:.200}"
1473 ));
1474 }
1475 args_js.push(']');
1476
1477 let rbv = return_by_value.unwrap_or(false);
1478 let await_js = await_promise.unwrap_or(false);
1479 let group_json =
1480 serde_json::to_string(object_group.unwrap_or("")).unwrap_or_else(|_| "\"..\"".into());
1481 let func_json = serde_json::to_string(function_declaration).unwrap_or_default();
1482
1483 let js = format!(
1486 r#"(function() {{
1487 try {{
1488 var fn = Function('return (' + {func_json} + ')')();
1489 if (typeof fn !== 'function') {{
1490 return JSON.stringify({{ result: {{ type: 'undefined' }}, exceptionDetails: {{ text: 'functionDeclaration did not evaluate to a function', exceptionId: 0 }} }});
1491 }}
1492 var r = fn.apply({this_expr}, {args_js});
1493 if ({await_js} && r !== null && typeof r === 'object' && typeof r.then === 'function') {{
1494 window.__bao_async = {{ state: 'pending' }};
1495 Promise.resolve(r).then(
1496 function(v) {{ window.__bao_async = {{ state: 'ok', v: v }}; }},
1497 function(e) {{ window.__bao_async = {{ state: 'err', e: e }}; }}
1498 );
1499 return JSON.stringify({{ __baoAsync: true }});
1500 }}
1501 return JSON.stringify({{ result: window.__bao_cdp.wrap(r, {rbv}, {group_json}), exceptionDetails: null }});
1502 }} catch (e) {{
1503 var exObj = (e !== null && typeof e === 'object') ? window.__bao_cdp.wrap(e, false, {group_json}) : undefined;
1504 return JSON.stringify({{ result: {{ type: 'undefined' }}, exceptionDetails: {{ text: String((e && e.message) || e), exception: exObj, exceptionId: 0 }} }});
1505 }}
1506 }})()"#,
1507 this_expr = this_expr,
1508 args_js = args_js,
1509 func_json = func_json,
1510 rbv = rbv,
1511 await_js = await_js,
1512 group_json = group_json,
1513 );
1514 let result = page.evaluate_js_web(&js).map_err(to_browser_error)?;
1515 let parsed: Value = serde_json::from_str(&result).map_err(|e| {
1518 format!("Runtime.callFunctionOn: page did not return the wrapper JSON: {e} (got: {result:.200})")
1519 })?;
1520 if parsed.get("__baoAsync").and_then(|v| v.as_bool()) == Some(true) {
1521 return wait_bao_async_promise(page, rbv, &group_json);
1522 }
1523 Ok(parsed)
1524}
1525
1526fn wait_bao_async_promise(
1532 page: &PageHandle,
1533 return_by_value: bool,
1534 group_json: &str,
1535) -> Result<Value, String> {
1536 let poll = format!(
1537 r#"(function() {{
1538 var a = window.__bao_async;
1539 if (!a || a.state === 'pending') return JSON.stringify({{ pending: true }});
1540 if (a.state === 'ok') return JSON.stringify({{ result: window.__bao_cdp.wrap(a.v, {rbv}, {group_json}), exceptionDetails: null }});
1541 var exObj = (a.e !== null && typeof a.e === 'object') ? window.__bao_cdp.wrap(a.e, false, {group_json}) : undefined;
1542 return JSON.stringify({{ result: {{ type: 'undefined' }}, exceptionDetails: {{ text: String((a.e && a.e.message) || a.e), exception: exObj, exceptionId: 0 }} }});
1543 }})()"#,
1544 rbv = return_by_value,
1545 group_json = group_json,
1546 );
1547 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
1548 loop {
1549 let out = page.evaluate_js_web(&poll).map_err(to_browser_error)?;
1550 let parsed: Value = serde_json::from_str(&out).map_err(|e| {
1551 format!("Runtime.callFunctionOn: async poll wrapper unparseable: {e} (got: {out:.200})")
1552 })?;
1553 if parsed.get("pending").and_then(|v| v.as_bool()) != Some(true) {
1554 return Ok(parsed);
1555 }
1556 if std::time::Instant::now() >= deadline {
1557 return Err(
1558 "Runtime.callFunctionOn: awaitPromise did not settle within 20s".into(),
1559 );
1560 }
1561 std::thread::sleep(std::time::Duration::from_millis(10));
1562 }
1563}
1564
1565fn cmd_runtime_release_object(page: &PageHandle, object_id: &str) -> Result<Value, String> {
1568 let oid_json = serde_json::to_string(object_id).unwrap_or_default();
1569 let js = format!(
1570 r#"(function() {{ if (window.__bao_cdp) window.__bao_cdp.release({oid_json}); return 'ok'; }})()"#
1571 );
1572 page.evaluate_js_web(&js).map_err(to_browser_error)?;
1573 Ok(serde_json::json!({}))
1574}
1575
1576fn cmd_runtime_release_object_group(
1580 page: &PageHandle,
1581 object_group: &str,
1582) -> Result<Value, String> {
1583 let group_json = serde_json::to_string(object_group).unwrap_or_default();
1584 let js = format!(
1585 r#"(function() {{ if (window.__bao_cdp) window.__bao_cdp.releaseGroup({group_json}); return 'ok'; }})()"#
1586 );
1587 page.evaluate_js_web(&js).map_err(to_browser_error)?;
1588 Ok(serde_json::json!({}))
1589}
1590
1591fn json_type(v: &Value) -> &'static str {
1592 match v {
1593 Value::Null => "undefined",
1594 Value::Bool(_) => "boolean",
1595 Value::Number(_) => "number",
1596 Value::String(_) => "string",
1597 Value::Array(_) => "object",
1598 Value::Object(_) => "object",
1599 }
1600}
1601
1602fn parse_js_result(result: &str) -> Result<Value, String> {
1604 serde_json::from_str(result)
1610 .map_err(|e| format!("unparseable JS wrapper output: {e} (got: {result:.200})"))
1611}
1612
1613fn json_type_string(s: &str) -> &'static str {
1614 if s.is_empty() || s == "undefined" {
1615 "undefined"
1616 } else if s == "null" {
1617 "object"
1618 } else if s == "true" || s == "false" {
1619 "boolean"
1620 } else if s.parse::<f64>().is_ok() {
1621 "number"
1622 } else if s.starts_with('{') || s.starts_with('[') {
1623 "object"
1624 } else {
1625 "string"
1626 }
1627}
1628
1629fn cookie_to_cdp(c: &cookie::Cookie) -> Value {
1635 let same_site = match c.same_site() {
1636 Some(cookie::SameSite::Strict) => "Strict",
1637 Some(cookie::SameSite::Lax) => "Lax",
1638 Some(cookie::SameSite::None) => "None",
1639 None => "None",
1640 };
1641 let expires = c
1642 .expires_datetime()
1643 .map(|dt| dt.unix_timestamp() as f64)
1644 .unwrap_or(-1.0);
1645 serde_json::json!({
1646 "name": c.name(),
1647 "value": c.value(),
1648 "domain": c.domain().unwrap_or(""),
1649 "path": c.path().unwrap_or("/"),
1650 "expires": expires,
1651 "size": c.name().len() + c.value().len(),
1652 "httpOnly": c.http_only().unwrap_or(false),
1653 "secure": c.secure().unwrap_or(false),
1654 "sameSite": same_site,
1655 "session": expires == -1.0,
1656 })
1657}
1658
1659fn cdp_params_to_cookie(
1661 name: &str,
1662 value: &str,
1663 _url: Option<&str>,
1664 domain: Option<&str>,
1665) -> cookie::Cookie<'static> {
1666 let mut builder = cookie::Cookie::build((name.to_string(), value.to_string()));
1667 if let Some(d) = domain {
1668 if d.starts_with('.') {
1669 builder = builder.domain(d.to_string());
1670 } else {
1671 builder = builder.domain(format!(".{d}"));
1672 }
1673 }
1674 builder = builder.path("/");
1675 builder.build()
1676}
1677
1678fn cmd_get_cookies(page: &PageHandle, urls: &[String]) -> Result<Value, String> {
1680 let servo = page.servo();
1681 let sdm = servo.site_data_manager();
1682 let cookies: Vec<Value> = if urls.is_empty() {
1683 let current_url = page.current_url().unwrap_or_default();
1685 if current_url.is_empty() || current_url == "about:blank" {
1686 Vec::new()
1687 } else {
1688 match url::Url::parse(¤t_url) {
1689 Ok(parsed) => {
1690 let servo_cookies = sdm.cookies_for_url(parsed, CookieSource::HTTP);
1691 servo_cookies.iter().map(cookie_to_cdp).collect()
1692 }
1693 Err(_) => Vec::new(),
1694 }
1695 }
1696 } else {
1697 let mut seen = HashSet::new();
1699 let mut result = Vec::new();
1700 for url_str in urls {
1701 if let Ok(parsed) = url::Url::parse(url_str) {
1702 for c in sdm.cookies_for_url(parsed, CookieSource::HTTP) {
1703 let key = (
1704 c.name().to_string(),
1705 c.domain().unwrap_or("").to_string(),
1706 c.path().unwrap_or("").to_string(),
1707 );
1708 if seen.insert(key) {
1709 result.push(cookie_to_cdp(&c));
1710 }
1711 }
1712 }
1713 }
1714 result
1715 };
1716 Ok(serde_json::json!({ "cookies": cookies }))
1717}
1718
1719fn cmd_get_all_cookies(page: &PageHandle) -> Result<Value, String> {
1721 let servo = page.servo();
1722 let sdm = servo.site_data_manager();
1723 let site_data = sdm.site_data(StorageType::Cookies);
1725 let mut cookies: Vec<Value> = Vec::new();
1726 let mut seen = HashSet::new();
1727 for sd in site_data {
1728 let site_name = sd.name();
1729 let url_str = if site_name.starts_with("http://") || site_name.starts_with("https://") {
1731 site_name.clone()
1732 } else {
1733 format!("https://{site_name}")
1734 };
1735 if let Ok(parsed) = url::Url::parse(&url_str) {
1736 for c in sdm.cookies_for_url(parsed, CookieSource::HTTP) {
1737 let key = (
1738 c.name().to_string(),
1739 c.domain().unwrap_or("").to_string(),
1740 c.path().unwrap_or("").to_string(),
1741 );
1742 if seen.insert(key) {
1743 cookies.push(cookie_to_cdp(&c));
1744 }
1745 }
1746 }
1747 }
1748 Ok(serde_json::json!({ "cookies": cookies }))
1749}
1750
1751fn cmd_set_cookie(
1753 page: &PageHandle,
1754 name: &str,
1755 value: &str,
1756 url: Option<&str>,
1757 domain: Option<&str>,
1758) -> Result<Value, String> {
1759 let servo = page.servo();
1760 let sdm = servo.site_data_manager();
1761 let cookie = cdp_params_to_cookie(name, value, url, domain);
1762 let fallback_url = page.current_url().unwrap_or_default();
1764 let url_str = url.unwrap_or_else(|| {
1765 if fallback_url.is_empty() || fallback_url == "about:blank" {
1766 "https://localhost/"
1767 } else {
1768 fallback_url.as_str()
1769 }
1770 });
1771 let parsed = url::Url::parse(url_str).map_err(|e| format!("invalid URL for setCookie: {e}"))?;
1772 sdm.set_cookie_for_url(parsed, cookie, None);
1773 Ok(serde_json::json!({ "success": true }))
1774}
1775
1776fn cmd_delete_cookie(page: &PageHandle, name: &str, url: Option<&str>) -> Result<Value, String> {
1778 let servo = page.servo();
1779 let sdm = servo.site_data_manager();
1780 if let Some(url_str) = url {
1781 let parsed =
1782 url::Url::parse(url_str).map_err(|e| format!("invalid URL for deleteCookies: {e}"))?;
1783 let current = sdm.cookies_for_url(parsed.clone(), CookieSource::HTTP);
1785 let site = parsed.host_str().unwrap_or("");
1787 sdm.clear_site_data(&[site], StorageType::Cookies);
1788 for c in current {
1790 if c.name() != name {
1791 sdm.set_cookie_for_url(parsed.clone(), c, None);
1792 }
1793 }
1794 } else {
1795 let site_data = sdm.site_data(StorageType::Cookies);
1797 for sd in site_data {
1798 let site_name = sd.name();
1799 let url_str = if site_name.starts_with("http://") || site_name.starts_with("https://") {
1800 site_name.clone()
1801 } else {
1802 format!("https://{site_name}")
1803 };
1804 if let Ok(parsed) = url::Url::parse(&url_str) {
1805 let current = sdm.cookies_for_url(parsed.clone(), CookieSource::HTTP);
1806 let has_match = current.iter().any(|c| c.name() == name);
1807 if has_match {
1808 sdm.clear_site_data(&[&site_name], StorageType::Cookies);
1809 for c in current {
1810 if c.name() != name {
1811 sdm.set_cookie_for_url(parsed.clone(), c, None);
1812 }
1813 }
1814 }
1815 }
1816 }
1817 }
1818 Ok(serde_json::json!({}))
1819}
1820
1821fn cmd_network_set_cache_disabled(
1823 page: &PageHandle,
1824 cache_disabled: bool,
1825) -> Result<Value, String> {
1826 if cache_disabled {
1827 let servo = page.servo();
1828 let nm = servo.network_manager();
1829 nm.clear_cache();
1830 }
1831 Ok(serde_json::json!({}))
1832}
1833
1834fn cmd_network_clear_browser_cache(page: &PageHandle) -> Result<Value, String> {
1836 let servo = page.servo();
1837 let nm = servo.network_manager();
1838 nm.clear_cache();
1839 Ok(serde_json::json!({}))
1840}
1841
1842fn cmd_network_clear_browser_cookies(page: &PageHandle) -> Result<Value, String> {
1844 let servo = page.servo();
1845 let sdm = servo.site_data_manager();
1846 sdm.clear_cookies(None);
1847 Ok(serde_json::json!({}))
1848}
1849
1850fn cmd_storage_get_items(
1852 page: &PageHandle,
1853 origin: String,
1854 storage_type: String,
1855) -> Result<Value, String> {
1856 let servo = page.servo();
1857 let sdm = servo.site_data_manager();
1858 let st = parse_storage_type(&storage_type);
1859 let site_data = sdm.site_data(st);
1860 let items: Vec<Value> = site_data
1861 .iter()
1862 .filter(|sd| {
1863 let site_name = sd.name();
1864 origin.is_empty()
1865 || site_name == origin
1866 || site_name.ends_with(&format!(".{origin}"))
1867 || origin.ends_with(&format!(".{site_name}"))
1868 })
1869 .map(|sd| {
1870 serde_json::json!({
1871 "origin": sd.name(),
1872 "storageType": storage_type,
1873 })
1874 })
1875 .collect();
1876 Ok(serde_json::json!({ "storageItems": items }))
1877}
1878
1879fn cmd_storage_clear_data(
1881 page: &PageHandle,
1882 origin: String,
1883 storage_type: String,
1884) -> Result<Value, String> {
1885 let servo = page.servo();
1886 let sdm = servo.site_data_manager();
1887 let st = parse_storage_type(&storage_type);
1888 if origin.is_empty() {
1889 sdm.clear_cookies(None);
1890 } else {
1891 sdm.clear_site_data(&[&origin], st);
1892 }
1893 Ok(serde_json::json!({}))
1894}
1895
1896fn parse_storage_type(storage_type: &str) -> StorageType {
1898 match storage_type {
1899 "cookies" | "cookie" => StorageType::Cookies,
1900 "local_storage" | "local" => StorageType::Local,
1901 "session_storage" | "session" => StorageType::Session,
1902 "all" => StorageType::Cookies | StorageType::Local | StorageType::Session,
1903 _ => StorageType::Cookies | StorageType::Local | StorageType::Session,
1904 }
1905}
1906
1907fn ok_empty() -> Result<Value, String> {
1908 Ok(serde_json::json!({}))
1909}
1910
1911#[cfg(test)]
1912mod tests {
1913 use crate::delegate::{ServiceWorkerHandle, ServiceWorkerRegistrationState};
1914 use serde_json::{json, Value};
1915
1916 #[test]
1917 fn json_type_null_returns_undefined() {
1918 assert_eq!(super::json_type(&json!(null)), "undefined");
1919 }
1920
1921 #[test]
1922 fn json_type_bool_returns_boolean() {
1923 assert_eq!(super::json_type(&json!(true)), "boolean");
1924 assert_eq!(super::json_type(&json!(false)), "boolean");
1925 }
1926
1927 #[test]
1928 fn json_type_number_returns_number() {
1929 assert_eq!(super::json_type(&json!(42)), "number");
1930 assert_eq!(super::json_type(&json!(3.14)), "number");
1931 assert_eq!(super::json_type(&json!(0)), "number");
1932 assert_eq!(super::json_type(&json!(-1)), "number");
1933 }
1934
1935 #[test]
1936 fn json_type_string_returns_string() {
1937 assert_eq!(super::json_type(&json!("hello")), "string");
1938 assert_eq!(super::json_type(&json!("")), "string");
1939 }
1940
1941 #[test]
1942 fn json_type_array_returns_object() {
1943 assert_eq!(super::json_type(&json!([1, 2, 3])), "object");
1944 assert_eq!(super::json_type(&json!([])), "object");
1945 }
1946
1947 #[test]
1948 fn json_type_object_returns_object() {
1949 assert_eq!(super::json_type(&json!({"a": 1})), "object");
1950 assert_eq!(super::json_type(&json!({})), "object");
1951 }
1952
1953 #[test]
1954 fn json_type_string_empty_returns_undefined() {
1955 assert_eq!(super::json_type_string(""), "undefined");
1956 }
1957
1958 #[test]
1959 fn json_type_string_undefined_returns_undefined() {
1960 assert_eq!(super::json_type_string("undefined"), "undefined");
1961 }
1962
1963 #[test]
1964 fn json_type_string_null_returns_object() {
1965 assert_eq!(super::json_type_string("null"), "object");
1966 }
1967
1968 #[test]
1969 fn json_type_string_true_returns_boolean() {
1970 assert_eq!(super::json_type_string("true"), "boolean");
1971 }
1972
1973 #[test]
1974 fn json_type_string_false_returns_boolean() {
1975 assert_eq!(super::json_type_string("false"), "boolean");
1976 }
1977
1978 #[test]
1979 fn json_type_string_integer_returns_number() {
1980 assert_eq!(super::json_type_string("42"), "number");
1981 assert_eq!(super::json_type_string("0"), "number");
1982 assert_eq!(super::json_type_string("-7"), "number");
1983 }
1984
1985 #[test]
1986 fn json_type_string_float_returns_number() {
1987 assert_eq!(super::json_type_string("3.14"), "number");
1988 assert_eq!(super::json_type_string("-0.5"), "number");
1989 }
1990
1991 #[test]
1992 fn json_type_string_object_brace_returns_object() {
1993 assert_eq!(super::json_type_string("{\"a\":1}"), "object");
1994 }
1995
1996 #[test]
1997 fn json_type_string_array_bracket_returns_object() {
1998 assert_eq!(super::json_type_string("[1,2,3]"), "object");
1999 }
2000
2001 #[test]
2002 fn json_type_string_regular_text_returns_string() {
2003 assert_eq!(super::json_type_string("hello world"), "string");
2004 assert_eq!(super::json_type_string("some result"), "string");
2005 }
2006
2007 #[test]
2011 fn json_type_large_number() {
2012 assert_eq!(super::json_type(&json!(i64::MAX)), "number");
2013 assert_eq!(super::json_type(&json!(f64::MAX)), "number");
2014 }
2015
2016 #[test]
2017 fn json_type_nested_object() {
2018 assert_eq!(super::json_type(&json!({"a": {"b": 1}})), "object");
2019 }
2020
2021 #[test]
2022 fn json_type_nested_array() {
2023 assert_eq!(super::json_type(&json!([[1, 2], [3, 4]])), "object");
2024 }
2025
2026 #[test]
2030 fn json_type_string_scientific_notation() {
2031 assert_eq!(super::json_type_string("1e10"), "number");
2032 assert_eq!(super::json_type_string("-2.5e-3"), "number");
2033 }
2034
2035 #[test]
2036 fn json_type_string_whitespace_is_string() {
2037 assert_eq!(super::json_type_string(" "), "string");
2038 assert_eq!(super::json_type_string(" 42"), "string");
2039 }
2040
2041 #[test]
2042 fn json_type_string_special_strings() {
2043 assert_eq!(super::json_type_string("NaN"), "number");
2045 assert_eq!(super::json_type_string("Infinity"), "number");
2046 assert_eq!(super::json_type_string("[object Object]"), "object");
2047 }
2048
2049 #[test]
2050 fn json_type_string_negative_zero() {
2051 assert_eq!(super::json_type_string("-0"), "number");
2052 assert_eq!(super::json_type_string("0.0"), "number");
2053 }
2054
2055 #[test]
2059 fn to_browser_error_init_variant() {
2060 let err = crate::error::BrowserError::Init("failed to start".into());
2061 let msg = super::to_browser_error(err);
2062 assert!(msg.contains("browser init error"));
2063 assert!(msg.contains("failed to start"));
2064 }
2065
2066 #[test]
2067 fn to_browser_error_navigation_variant() {
2068 let err = crate::error::BrowserError::Navigation("invalid url".into());
2069 let msg = super::to_browser_error(err);
2070 assert!(msg.contains("navigation error"));
2071 assert!(msg.contains("invalid url"));
2072 }
2073
2074 #[test]
2075 fn to_browser_error_rendering_variant() {
2076 let err = crate::error::BrowserError::Rendering("gpu lost".into());
2077 let msg = super::to_browser_error(err);
2078 assert!(msg.contains("rendering error"));
2079 assert!(msg.contains("gpu lost"));
2080 }
2081
2082 #[test]
2083 fn to_browser_error_javascript_variant() {
2084 let err = crate::error::BrowserError::JavaScript("syntax error".into());
2085 let msg = super::to_browser_error(err);
2086 assert!(msg.contains("javascript error"));
2087 assert!(msg.contains("syntax error"));
2088 }
2089
2090 #[test]
2091 fn to_browser_error_cdp_variant() {
2092 let err = crate::error::BrowserError::CDP("connection refused".into());
2093 let msg = super::to_browser_error(err);
2094 assert!(msg.contains("cdp error"));
2095 assert!(msg.contains("connection refused"));
2096 }
2097
2098 #[test]
2099 fn to_browser_error_empty_message() {
2100 let err = crate::error::BrowserError::Init(String::new());
2101 let msg = super::to_browser_error(err);
2102 assert!(msg.contains("browser init error"));
2103 }
2104
2105 #[test]
2106 fn to_browser_error_unicode_message() {
2107 let err = crate::error::BrowserError::Navigation("页面加载失败".into());
2108 let msg = super::to_browser_error(err);
2109 assert!(msg.contains("页面加载失败"));
2110 }
2111
2112 #[test]
2116 fn next_cdp_id_is_unique_and_prefixed() {
2117 let a = super::next_cdp_id("loader");
2121 let b = super::next_cdp_id("loader");
2122 assert!(a.starts_with("loader-"), "id must carry its prefix: {a}");
2123 assert_ne!(a, b, "ids must be unique per call");
2124 let s = super::next_cdp_id("script");
2125 assert!(s.starts_with("script-"), "id must carry its prefix: {s}");
2126 }
2127
2128 #[test]
2129 fn cmd_navigate_uses_page_id_frame_and_generated_loader() {
2130 let source = include_str!("cdp_handler.rs");
2133 assert!(
2134 source.contains("\"frameId\": page.id().to_string()"),
2135 "cmd_navigate/cmd_reload must return the real page id as frameId"
2136 );
2137 assert!(
2138 source.contains("\"loaderId\": next_cdp_id(\"loader\")"),
2139 "cmd_navigate/cmd_reload must generate a fresh loaderId per load"
2140 );
2141 assert!(
2142 !source.contains("\"loaderId\": \"0\""),
2143 "no canned loaderId \"0\" may remain"
2144 );
2145 }
2146
2147 #[test]
2151 fn cmd_evaluate_return_by_value_true_json_parse() {
2152 let result_str = r#"{"a":1}"#;
2154 let parsed: Result<Value, _> = serde_json::from_str(result_str);
2155 assert!(parsed.is_ok());
2156 assert_eq!(super::json_type(&parsed.unwrap()), "object");
2157 }
2158
2159 #[test]
2160 fn cmd_evaluate_return_by_value_true_non_json_falls_back() {
2161 let result_str = "hello world";
2163 let parsed: Result<Value, _> = serde_json::from_str(result_str);
2164 assert!(parsed.is_err());
2165 assert_eq!(super::json_type_string(result_str), "string");
2166 }
2167
2168 #[test]
2169 fn cmd_evaluate_return_by_value_true_null_json() {
2170 let parsed: Result<Value, _> = serde_json::from_str("null");
2171 assert!(parsed.is_ok());
2172 assert_eq!(super::json_type(&parsed.unwrap()), "undefined");
2173 }
2174
2175 #[test]
2176 fn cmd_evaluate_return_by_value_true_number_json() {
2177 let parsed: Result<Value, _> = serde_json::from_str("42");
2178 assert!(parsed.is_ok());
2179 assert_eq!(super::json_type(&parsed.unwrap()), "number");
2180 }
2181
2182 #[test]
2183 fn cmd_evaluate_return_by_value_true_boolean_json() {
2184 let parsed: Result<Value, _> = serde_json::from_str("true");
2185 assert!(parsed.is_ok());
2186 assert_eq!(super::json_type(&parsed.unwrap()), "boolean");
2187 }
2188
2189 #[test]
2190 fn cmd_evaluate_return_by_value_false_uses_description() {
2191 let result_str = "some JS output";
2193 assert_eq!(super::json_type_string(result_str), "string");
2194 }
2195
2196 #[test]
2200 fn cmd_screenshot_format_jpeg_mapping() {
2201 let fmt = match "jpeg" {
2203 "jpeg" => "Jpeg",
2204 _ => "Png",
2205 };
2206 assert_eq!(fmt, "Jpeg");
2207 }
2208
2209 #[test]
2210 fn cmd_screenshot_format_png_mapping() {
2211 let fmt = match "png" {
2212 "jpeg" => "Jpeg",
2213 _ => "Png",
2214 };
2215 assert_eq!(fmt, "Png");
2216 }
2217
2218 #[test]
2219 fn cmd_screenshot_format_unknown_defaults_to_png() {
2220 let fmt = match "bmp" {
2221 "jpeg" => "Jpeg",
2222 "webp" => "WebP",
2223 _ => "Png",
2224 };
2225 assert_eq!(fmt, "Png");
2226 }
2227
2228 #[test]
2229 fn cmd_screenshot_format_webp_mapping() {
2230 let fmt = match "webp" {
2231 "jpeg" => "Jpeg",
2232 "webp" => "WebP",
2233 _ => "Png",
2234 };
2235 assert_eq!(fmt, "WebP");
2236 }
2237
2238 #[test]
2239 fn cmd_screenshot_format_empty_defaults_to_png() {
2240 let fmt = match "" {
2241 "jpeg" => "Jpeg",
2242 _ => "Png",
2243 };
2244 assert_eq!(fmt, "Png");
2245 }
2246
2247 #[test]
2248 fn cmd_screenshot_base64_encoding() {
2249 let bytes: Vec<u8> = vec![0x89, 0x50, 0x4E, 0x47]; let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
2252 assert!(!b64.is_empty());
2253 let decoded = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &b64);
2255 assert!(decoded.is_ok());
2256 assert_eq!(decoded.unwrap(), bytes);
2257 }
2258
2259 #[test]
2260 fn cmd_screenshot_base64_empty_bytes() {
2261 let bytes: Vec<u8> = vec![];
2262 let b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes);
2263 assert_eq!(b64, ""); }
2265
2266 #[test]
2270 fn cmd_query_selector_js_construction_valid_selector() {
2271 let selector = "div.main";
2272 let js = format!(
2273 "(function() {{ var e = document.querySelector({}); return e ? 1 : 0; }})()",
2274 serde_json::to_string(selector).unwrap_or_default()
2275 );
2276 assert!(js.contains("document.querySelector"));
2277 assert!(js.contains("\"div.main\""));
2278 }
2279
2280 #[test]
2281 fn cmd_query_selector_js_construction_empty_selector() {
2282 let selector = "";
2283 let json_str = serde_json::to_string(selector).unwrap_or_default();
2284 assert_eq!(json_str, "\"\"");
2285 }
2286
2287 #[test]
2288 fn cmd_query_selector_js_construction_special_chars() {
2289 let selector = "div[data-attr='value']";
2290 let json_str = serde_json::to_string(selector).unwrap_or_default();
2291 assert!(json_str.contains("div[data-attr"));
2293 }
2294
2295 #[test]
2296 fn cmd_query_selector_js_construction_unicode() {
2297 let selector = "div.中文类名";
2298 let json_str = serde_json::to_string(selector).unwrap_or_default();
2299 assert!(json_str.contains("中文类名"));
2300 }
2301
2302 #[test]
2306 fn cmd_query_selector_all_js_construction() {
2307 let selector = "li.item";
2308 let js = format!(
2309 "(function() {{ return document.querySelectorAll({}).length; }})()",
2310 serde_json::to_string(selector).unwrap_or_default()
2311 );
2312 assert!(js.contains("document.querySelectorAll"));
2313 assert!(js.contains(".length"));
2314 }
2315
2316 #[test]
2317 fn cmd_query_selector_all_count_to_node_ids() {
2318 let count: i64 = 3;
2320 let ids: Vec<i64> = (1..=count).collect();
2321 assert_eq!(ids, vec![1, 2, 3]);
2322 }
2323
2324 #[test]
2325 fn cmd_query_selector_all_zero_count() {
2326 let count: i64 = 0;
2327 let ids: Vec<i64> = (1..=count).collect();
2328 assert!(ids.is_empty());
2329 }
2330
2331 #[test]
2332 fn cmd_query_selector_all_large_count() {
2333 let count: i64 = 100;
2334 let ids: Vec<i64> = (1..=count).collect();
2335 assert_eq!(ids.len(), 100);
2336 assert_eq!(ids[0], 1);
2337 assert_eq!(ids[99], 100);
2338 }
2339
2340 #[test]
2344 fn cmd_set_attribute_js_construction() {
2345 let name = "class";
2346 let value = "active";
2347 let js = format!(
2348 "(function() {{ document.querySelector('[data-cdp]')?.setAttribute({}, {}); }})()",
2349 serde_json::to_string(name).unwrap_or_default(),
2350 serde_json::to_string(value).unwrap_or_default(),
2351 );
2352 assert!(js.contains("setAttribute"));
2353 assert!(js.contains("\"class\""));
2354 assert!(js.contains("\"active\""));
2355 }
2356
2357 #[test]
2358 fn cmd_set_attribute_js_with_quotes_in_value() {
2359 let name = "data-info";
2360 let value = r#"he said "hello""#;
2361 let _json_name = serde_json::to_string(name).unwrap_or_default();
2362 let json_value = serde_json::to_string(value).unwrap_or_default();
2363 assert!(json_value.contains("\\\""));
2365 }
2366
2367 #[test]
2371 fn cmd_insert_text_js_construction() {
2372 let text = "hello";
2373 let js = format!(
2374 "(function() {{ var el = document.activeElement; if (el && 'value' in el) el.value += {}; }})()",
2375 serde_json::to_string(text).unwrap_or_default(),
2376 );
2377 assert!(js.contains("document.activeElement"));
2378 assert!(js.contains("el.value"));
2379 }
2380
2381 #[test]
2382 fn cmd_insert_text_js_empty_string() {
2383 let text = "";
2384 let json_str = serde_json::to_string(text).unwrap_or_default();
2385 assert_eq!(json_str, "\"\"");
2386 }
2387
2388 #[test]
2389 fn cmd_insert_text_js_newline_escaped() {
2390 let text = "line1\nline2";
2391 let json_str = serde_json::to_string(text).unwrap_or_default();
2392 assert!(json_str.contains("\\n"));
2393 }
2394
2395 #[test]
2399 fn cmd_set_user_agent_js_construction() {
2400 let ua = "Mozilla/5.0 Test";
2401 let js = format!(
2402 "Object.defineProperty(navigator, 'userAgent', {{ get: function() {{ return {}; }} }});",
2403 serde_json::to_string(ua).unwrap_or_default(),
2404 );
2405 assert!(js.contains("Object.defineProperty"));
2406 assert!(js.contains("navigator"));
2407 assert!(js.contains("userAgent"));
2408 }
2409
2410 #[test]
2411 fn cmd_set_user_agent_js_empty_string() {
2412 let ua = "";
2413 let json_str = serde_json::to_string(ua).unwrap_or_default();
2414 assert_eq!(json_str, "\"\"");
2415 }
2416
2417 #[test]
2421 fn cmd_get_document_js_template_structure() {
2422 let js = r#"
2423 (function() {
2424 function walk(node, id) {
2425 var result = {
2426 nodeId: id,
2427 backendNodeId: id,
2428 nodeType: node.nodeType,
2429 nodeName: node.nodeName,
2430 localName: node.localName || '',
2431 nodeValue: node.nodeValue || '',
2432 };
2433 if (node.childNodes && node.childNodes.length > 0) {
2434 result.childNodeCount = node.childNodes.length;
2435 result.children = [];
2436 for (var i = 0; i < Math.min(node.childNodes.length, 20); i++) {
2437 result.children.push(walk(node.childNodes[i], id * 100 + i + 1));
2438 }
2439 }
2440 return result;
2441 }
2442 return JSON.stringify(walk(document, 1));
2443 })()
2444 "#;
2445 assert!(js.contains("walk"));
2446 assert!(js.contains("nodeId"));
2447 assert!(js.contains("nodeType"));
2448 assert!(js.contains("nodeName"));
2449 assert!(js.contains("childNodeCount"));
2450 assert!(js.contains("Math.min"));
2451 assert!(js.contains("JSON.stringify"));
2452 }
2453
2454 #[test]
2455 fn cmd_get_document_js_limits_children_to_20() {
2456 let _js = r#"(function() { return Math.min(50, 20); })()"#;
2458 assert_eq!(50usize.min(20), 20);
2460 }
2461
2462 #[test]
2466 fn cmd_get_outer_html_js_is_simple_expression() {
2467 let js = "document.documentElement.outerHTML";
2468 assert!(js.contains("document.documentElement"));
2469 assert!(js.contains("outerHTML"));
2470 }
2471
2472 #[test]
2476 fn cmd_add_script_response_has_generated_identifier() {
2477 let resp = json!({ "identifier": super::next_cdp_id("script") });
2479 assert!(resp["identifier"].as_str().unwrap().starts_with("script-"));
2480 }
2481
2482 #[test]
2486 fn cmd_reload_response_structure() {
2487 let source = include_str!("cdp_handler.rs");
2490 assert!(
2491 source.contains("page.reload().map_err(to_browser_error)?;"),
2492 "cmd_reload must call PageHandle::reload"
2493 );
2494 }
2495
2496 #[test]
2500 fn handle_bridge_command_go_back_forward_wired_stop_loading_errors() {
2501 let source = include_str!("cdp_handler.rs");
2504 assert!(
2505 source.contains("BridgeCommand::GoBack { target_id } => with_page"),
2506 "GoBack must be dispatched to a page handler"
2507 );
2508 assert!(
2509 source.contains("BridgeCommand::GoForward { target_id } => with_page"),
2510 "GoForward must be dispatched to a page handler"
2511 );
2512 assert!(
2513 source.contains("Page.stopLoading not supported"),
2514 "StopLoading must return an explicit error, never a fake ok"
2515 );
2516 }
2517
2518 #[test]
2522 fn no_canned_successes_remain_in_dispatch() {
2523 let source = include_str!("cdp_handler.rs");
2526 assert!(!source.contains("\"snapshot\": {}"));
2527 assert!(!source.contains("\"profile\": {}"));
2528 assert!(!source.contains("\"jsEventListeners\": 0"));
2529 assert!(!source.contains("\"body\": \"\""));
2530 assert!(!source.contains("\"identifier\": \"1\""));
2531 }
2532
2533 #[test]
2534 fn profiler_and_heapprofiler_report_explicit_errors() {
2535 let source = include_str!("cdp_handler.rs");
2536 assert!(source.contains("Profiler not supported"));
2537 assert!(source.contains("HeapProfiler snapshot/tracking not supported"));
2538 assert!(source.contains("Memory.getDOMCounters not supported"));
2539 assert!(source.contains("Network.getResponseBody not supported"));
2540 assert!(source.contains("Network.setExtraHTTPHeaders not supported"));
2541 assert!(source.contains("Security.setOverrideCertificateErrors not supported"));
2542 }
2543
2544 #[test]
2545 fn collect_garbage_uses_servo_gc_api() {
2546 let source = include_str!("cdp_handler.rs");
2549 assert!(
2550 source.contains("navigator.servo.GarbageCollectAllContexts()"),
2551 "GC must go through servo's real GC DOM API"
2552 );
2553 }
2554
2555 #[test]
2556 fn handle_bridge_command_close_page_returns_empty() {
2557 let expected = json!({});
2558 assert_eq!(expected, json!({}));
2559 }
2560
2561 #[test]
2562 fn handle_bridge_command_unsupported_returns_error() {
2563 let err_msg = "unsupported bridge command";
2565 assert!(!err_msg.is_empty());
2566 }
2567
2568 #[test]
2572 fn json_type_string_leading_dot_is_string() {
2573 let result = ".5".parse::<f64>();
2575 if result.is_ok() {
2576 assert_eq!(super::json_type_string(".5"), "number");
2577 } else {
2578 assert_eq!(super::json_type_string(".5"), "string");
2579 }
2580 }
2581
2582 #[test]
2583 fn json_type_string_positive_infinity() {
2584 assert_eq!(super::json_type_string("inf"), "number");
2585 }
2586
2587 #[test]
2588 fn json_type_string_negative_infinity() {
2589 assert_eq!(super::json_type_string("-inf"), "number");
2590 }
2591
2592 #[test]
2593 fn json_type_string_hex_string_is_string() {
2594 assert_eq!(super::json_type_string("0x1A"), "string");
2596 }
2597
2598 #[test]
2599 fn json_type_string_very_long_number() {
2600 let long_num = "123456789012345678901234567890";
2601 assert_eq!(super::json_type_string(long_num), "number");
2603 }
2604
2605 #[test]
2606 fn json_type_string_mixed_alphanumeric_is_string() {
2607 assert_eq!(super::json_type_string("abc123"), "string");
2608 }
2609
2610 #[test]
2611 fn json_type_string_empty_object_string() {
2612 assert_eq!(super::json_type_string("{}"), "object");
2613 }
2614
2615 #[test]
2616 fn json_type_string_empty_array_string() {
2617 assert_eq!(super::json_type_string("[]"), "object");
2618 }
2619
2620 #[test]
2624 fn json_type_negative_number() {
2625 assert_eq!(super::json_type(&json!(-999)), "number");
2626 }
2627
2628 #[test]
2629 fn json_type_large_float() {
2630 assert_eq!(super::json_type(&json!(f64::MIN)), "number");
2631 }
2632
2633 #[test]
2634 fn json_type_deeply_nested_value() {
2635 let deep = json!({"a": {"b": {"c": {"d": [1, 2, {"e": true}]}}}});
2636 assert_eq!(super::json_type(&deep), "object");
2637 }
2638
2639 #[test]
2640 fn json_type_string_with_special_chars() {
2641 assert_eq!(super::json_type(&json!("\n\t\r")), "string");
2642 assert_eq!(super::json_type(&json!("\0")), "string");
2643 }
2644
2645 #[test]
2646 fn json_type_mixed_array() {
2647 assert_eq!(
2648 super::json_type(&json!([1, "two", null, true, {}])),
2649 "object"
2650 );
2651 }
2652
2653 #[test]
2657 fn sw_registration_id_parses_double_colon_format() {
2658 let id = super::parse_sw_registration_id("sw.js::/").unwrap();
2660 assert_eq!(id.script_url, "sw.js");
2661 assert_eq!(id.scope, "/");
2662 }
2663
2664 #[test]
2665 fn sw_registration_id_parses_complex_scope() {
2666 let id = super::parse_sw_registration_id("https://example.com/sw.js::/app/").unwrap();
2667 assert_eq!(id.script_url, "https://example.com/sw.js");
2668 assert_eq!(id.scope, "/app/");
2669 }
2670
2671 #[test]
2672 fn sw_registration_id_rejects_missing_separator() {
2673 assert!(super::parse_sw_registration_id("sw.js").is_err());
2674 assert!(super::parse_sw_registration_id("sw.js/").is_err());
2675 }
2676
2677 #[test]
2678 fn sw_registration_to_json_activated() {
2679 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
2681 handle.transition_state(ServiceWorkerRegistrationState::Activated);
2682 handle.enable_fetch_interception();
2683
2684 let json_val = super::sw_registration_to_json(handle);
2685 assert_eq!(json_val["registrationId"], "sw.js::/");
2686 assert_eq!(json_val["scriptURL"], "sw.js");
2687 assert_eq!(json_val["scope"], "/");
2688 assert_eq!(json_val["state"], "activated");
2689 assert_eq!(json_val["isActive"], true);
2690 assert_eq!(json_val["isFetchIntercepting"], true);
2693 }
2694
2695 #[test]
2696 fn sw_registration_to_json_installing_state() {
2697 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
2698 let json_val = super::sw_registration_to_json(handle);
2700 assert_eq!(json_val["state"], "installing");
2701 assert_eq!(json_val["isActive"], false);
2702 assert_eq!(json_val["isFetchIntercepting"], false);
2703 }
2704
2705 #[test]
2706 fn sw_registration_to_json_all_states() {
2707 let states_and_expected = vec![
2709 (ServiceWorkerRegistrationState::Idle, "idle"),
2710 (ServiceWorkerRegistrationState::Installing, "installing"),
2711 (ServiceWorkerRegistrationState::Installed, "installed"),
2712 (ServiceWorkerRegistrationState::Activating, "activating"),
2713 (ServiceWorkerRegistrationState::Activated, "activated"),
2714 (ServiceWorkerRegistrationState::Redundant, "redundant"),
2715 ];
2716 for (state, expected) in states_and_expected {
2717 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
2718 handle.transition_state(state.clone());
2719 let json_val = super::sw_registration_to_json(handle);
2720 assert_eq!(json_val["state"], expected, "state mapping for {:?}", state);
2721 }
2722 }
2723
2724 #[test]
2725 fn sw_registration_terminate_disables_fetch_interception() {
2726 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
2729 handle.enable_fetch_interception();
2730 assert!(handle.is_intercepting_fetch());
2731
2732 handle.terminate();
2733
2734 assert!(handle.is_closing());
2735 assert!(!handle.is_intercepting_fetch());
2737 }
2738
2739 #[test]
2740 fn sw_registration_terminate_is_idempotent() {
2741 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
2742 handle.terminate();
2743 handle.terminate();
2744 handle.terminate();
2745 assert!(handle.is_closing());
2746 }
2747
2748 #[test]
2749 fn sw_registration_stealth_profile_inherited() {
2750 let profile = bao_stealth::StealthProfile::chrome_default();
2753 let handle =
2754 ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), Some(profile.clone()));
2755 assert!(handle.stealth_profile.is_some());
2756 let json_val = super::sw_registration_to_json(handle.clone());
2758 assert_eq!(json_val["scriptURL"], "sw.js");
2761 assert!(handle.stealth_profile.is_some());
2762 }
2763
2764 #[test]
2765 fn sw_registration_id_format_with_colon_in_url() {
2766 let id = super::parse_sw_registration_id("https://a.io/sw.js::/scope").unwrap();
2768 assert_eq!(id.script_url, "https://a.io/sw.js");
2769 assert_eq!(id.scope, "/scope");
2770 }
2771}