1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6use std::sync::mpsc;
7use std::time::{Duration, Instant};
8
9use dpi::PhysicalSize;
10use servo::{
11 CSSPixel, Code, CookieSource, InputEvent, Key, KeyState, KeyboardEvent, Location, Modifiers,
12 MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, NamedKey, RenderingContext,
13 Servo, SoftwareRenderingContext, StorageType, WebView, WebViewBuilder, WebViewPoint,
14};
15
16use crate::config::PageConfig;
17use crate::delegate::{BaoWebViewDelegate, BaoWebViewState};
18use crate::error::BrowserError;
19use crate::permission::PermissionGuard;
20use crate::screenshot::{encode_image, ScreenshotFormat};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum PageState {
24 Created,
25 Navigating,
26 Interactive,
27 Idle,
28 Closing,
33 Closed,
34}
35
36pub struct PageInner {
37 pub id: usize,
38 pub webview: WebView,
39 pub servo: Rc<Servo>,
40 pub rendering_context: Rc<SoftwareRenderingContext>,
41 pub delegate: Rc<BaoWebViewDelegate>,
42 pub state: Rc<RefCell<PageState>>,
43 pub webview_state: Rc<RefCell<BaoWebViewState>>,
44 pub nav_seq: ::std::cell::Cell<u64>,
49 pub viewport: PhysicalSize<u32>,
50 pub stealth_profile: Option<bao_stealth::StealthProfile>,
51 pub permission: PermissionGuard,
52 pub user_content_manager: Option<Rc<servo::UserContentManager>>,
57 pub last_active_at: RefCell<Instant>,
58 pub created_at: Instant,
59 pub node_realm_global: RefCell<*mut mozjs::jsapi::JSObject>,
63 pub page_global: RefCell<*mut mozjs::jsapi::JSObject>,
66}
67
68impl PageInner {
69 pub fn touch(&self) {
70 *self.last_active_at.borrow_mut() = Instant::now();
71 }
72
73 pub fn webview_id_opt(&self) -> Option<servo::WebViewId> {
77 Some(self.webview.id())
78 }
79
80 pub fn navigate(&self, url: &str) -> Result<(), BrowserError> {
81 let parsed = url::Url::parse(url)
82 .map_err(|e| BrowserError::Navigation(format!("invalid URL: {e}")))?;
83 self.webview.load(parsed);
84 self.touch();
85 *self.state.borrow_mut() = PageState::Navigating;
86 self.webview_state.borrow_mut().load_status = servo::LoadStatus::Started;
92 self.nav_seq.set(self.nav_seq.get() + 1);
93 Ok(())
94 }
95
96 pub fn drain_callbacks(&self) -> Result<String, BrowserError> {
109 let max_attempts = 100;
110
111 for attempt in 0..max_attempts {
112 match self.evaluate_js_web(";") {
113 Ok(result) => return Ok(result),
114 Err(BrowserError::JavaScript(msg)) if msg.contains("InternalError") => {
115 if attempt % 5 == 4 {
118 self.servo.spin_event_loop();
119 self.webview.paint();
120 }
121 continue;
122 }
123 Err(other) => return Err(other),
124 }
125 }
126
127 Err(BrowserError::Init(
128 "callback drain failed: pipeline not ready after timeout".into(),
129 ))
130 }
131
132 pub fn evaluate_js(&self, script: &str) -> Result<String, BrowserError> {
160 let webview_id = self.webview.id();
161
162 if self.webview_state.borrow().dom_proxies_dirty {
167 let old_pg = *self.page_global.borrow();
168 crate::runtime_bridge::register_refresh_dom_proxies(webview_id, old_pg);
169 self.drain_callbacks()?;
170 let new_pg = crate::runtime_bridge::get_page_global(webview_id);
172 let new_node = crate::runtime_bridge::get_node_realm_global(webview_id);
173 *self.page_global.borrow_mut() = new_pg;
174 *self.node_realm_global.borrow_mut() = new_node;
175 self.webview_state.borrow_mut().dom_proxies_dirty = false;
176 }
177
178 let node_global = crate::runtime_bridge::get_node_realm_global(webview_id);
180 if node_global.is_null() {
181 return Err(BrowserError::JavaScript(
184 "Node Realm not initialized — this is a bug, eager init failed".into(),
185 ));
186 }
187
188 let result = crate::runtime_bridge::evaluate_js_via_node_realm(webview_id, script);
191 self.drain_callbacks()?;
192
193 let eval_result = result.get().expect("evaluate result not set after drain");
194 match (&eval_result.value, &eval_result.error) {
195 (Some(val), _) => Ok(val.clone()),
196 (_, Some(err)) => Err(BrowserError::JavaScript(err.clone())),
197 (None, None) => Ok(String::new()),
198 }
199 }
200
201 pub fn evaluate_js_web(&self, script: &str) -> Result<String, BrowserError> {
206 let saved = Rc::new(RefCell::new(None));
207 let cb_saved = saved.clone();
208 self.webview
209 .evaluate_javascript(script.to_string(), move |result| {
210 *cb_saved.borrow_mut() = Some(result);
211 });
212
213 self.spin_servo(Duration::from_secs(15), || saved.borrow().is_none())?;
214
215 let result = saved
216 .borrow()
217 .clone()
218 .ok_or_else(|| BrowserError::JavaScript("no evaluation result".into()))?
219 .map_err(|e| BrowserError::JavaScript(format!("{e:?}")))?;
220
221 self.touch();
222 Ok(format_js_value(&result))
223 }
224
225 pub fn take_screenshot(&self, format: ScreenshotFormat) -> Result<Vec<u8>, BrowserError> {
226 self.webview.paint();
227
228 let saved = Rc::new(RefCell::new(None));
229 let cb_saved = saved.clone();
230 self.webview.take_screenshot(None, move |result| {
231 *cb_saved.borrow_mut() = Some(result);
232 });
233
234 self.spin_servo(Duration::from_secs(15), || saved.borrow().is_none())?;
235
236 let image = saved
237 .borrow()
238 .clone()
239 .ok_or_else(|| BrowserError::Rendering("no screenshot result".into()))?
240 .map_err(|e| BrowserError::Rendering(format!("{e:?}")))?;
241
242 self.touch();
243 encode_image(&image, format)
244 }
245
246 pub fn reload(&self) -> Result<(), BrowserError> {
248 self.webview.reload();
249 self.touch();
250 *self.state.borrow_mut() = PageState::Navigating;
251 self.webview_state.borrow_mut().load_status = servo::LoadStatus::Started;
253 self.nav_seq.set(self.nav_seq.get() + 1);
254 Ok(())
255 }
256
257 pub fn go_back(&self) -> Result<(), BrowserError> {
259 self.webview.go_back(1);
260 self.touch();
261 *self.state.borrow_mut() = PageState::Navigating;
262 self.webview_state.borrow_mut().load_status = servo::LoadStatus::Started;
264 self.nav_seq.set(self.nav_seq.get() + 1);
265 Ok(())
266 }
267
268 pub fn go_forward(&self) -> Result<(), BrowserError> {
270 self.webview.go_forward(1);
271 self.touch();
272 *self.state.borrow_mut() = PageState::Navigating;
273 self.webview_state.borrow_mut().load_status = servo::LoadStatus::Started;
275 self.nav_seq.set(self.nav_seq.get() + 1);
276 Ok(())
277 }
278
279 pub fn can_go_back(&self) -> bool {
281 self.webview.can_go_back()
282 }
283
284 pub fn can_go_forward(&self) -> bool {
286 self.webview.can_go_forward()
287 }
288
289 pub fn set_viewport(&self, width: u32, height: u32) {
291 let new_size = PhysicalSize::new(width, height);
292 self.webview.resize(new_size);
293 self.touch();
294 }
295
296 pub fn focus(&self) {
298 self.webview.focus();
299 }
300
301 pub fn dispatch_mouse_event(
303 &self,
304 action: MouseButtonAction,
305 button: MouseButton,
306 x: f32,
307 y: f32,
308 ) {
309 let point = WebViewPoint::Page(euclid::Point2D::<f32, CSSPixel>::new(x, y));
310 let event = InputEvent::MouseButton(MouseButtonEvent::new(action, button, point));
311 self.webview.notify_input_event(event);
312 self.touch();
313 }
314
315 pub fn dispatch_mouse_move(&self, x: f32, y: f32) {
317 let point = WebViewPoint::Page(euclid::Point2D::<f32, CSSPixel>::new(x, y));
318 let event = InputEvent::MouseMove(MouseMoveEvent::new(point));
319 self.webview.notify_input_event(event);
320 self.touch();
321 }
322
323 pub fn dispatch_key_event(&self, state: KeyState, key: Key, code: Code) {
325 let keyboard_event = KeyboardEvent::new_without_event(
326 state,
327 key,
328 code,
329 Location::Standard,
330 Modifiers::empty(),
331 false,
332 false,
333 );
334 let event = InputEvent::Keyboard(keyboard_event);
335 self.webview.notify_input_event(event);
336 self.touch();
337 }
338
339 pub fn dispatch_key_event_full(
341 &self,
342 state: KeyState,
343 key: Key,
344 code: Code,
345 location: Location,
346 modifiers: Modifiers,
347 repeat: bool,
348 ) {
349 let keyboard_event =
350 KeyboardEvent::new_without_event(state, key, code, location, modifiers, repeat, false);
351 let event = InputEvent::Keyboard(keyboard_event);
352 self.webview.notify_input_event(event);
353 self.touch();
354 }
355
356 pub fn cookies(&self, urls: &[String]) -> Result<Vec<cookie::Cookie<'static>>, BrowserError> {
358 let sdm = self.servo.site_data_manager();
359 if urls.is_empty() {
360 let current_url = self.current_url().unwrap_or_default();
361 if current_url.is_empty() || current_url == "about:blank" {
362 return Ok(Vec::new());
363 }
364 match url::Url::parse(¤t_url) {
365 Ok(parsed) => Ok(sdm.cookies_for_url(parsed, CookieSource::HTTP)),
366 Err(_) => Ok(Vec::new()),
367 }
368 } else {
369 let mut seen = std::collections::HashSet::new();
370 let mut result = Vec::new();
371 for url_str in urls {
372 if let Ok(parsed) = url::Url::parse(url_str) {
373 for c in sdm.cookies_for_url(parsed, CookieSource::HTTP) {
374 let key = (
375 c.name().to_string(),
376 c.domain().unwrap_or("").to_string(),
377 c.path().unwrap_or("").to_string(),
378 );
379 if seen.insert(key) {
380 result.push(c);
381 }
382 }
383 }
384 }
385 Ok(result)
386 }
387 }
388
389 pub fn set_cookie(
391 &self,
392 url: &str,
393 cookie: cookie::Cookie<'static>,
394 ) -> Result<(), BrowserError> {
395 let sdm = self.servo.site_data_manager();
396 let parsed = url::Url::parse(url)
397 .map_err(|e| BrowserError::Navigation(format!("invalid URL for setCookie: {e}")))?;
398 sdm.set_cookie_for_url(parsed, cookie, None);
399 self.touch();
400 Ok(())
401 }
402
403 pub fn delete_cookie(&self, name: &str, url: Option<&str>) -> Result<(), BrowserError> {
406 let sdm = self.servo.site_data_manager();
407 if let Some(url_str) = url {
408 let parsed = url::Url::parse(url_str).map_err(|e| {
409 BrowserError::Navigation(format!("invalid URL for deleteCookie: {e}"))
410 })?;
411 let current = sdm.cookies_for_url(parsed.clone(), CookieSource::HTTP);
412 let site = parsed.host_str().unwrap_or("");
413 sdm.clear_site_data(&[site], StorageType::Cookies);
414 for c in current {
415 if c.name() != name {
416 sdm.set_cookie_for_url(parsed.clone(), c, None);
417 }
418 }
419 } else {
420 let site_data = sdm.site_data(StorageType::Cookies);
421 for sd in site_data {
422 let site_name = sd.name();
423 let url_str =
424 if site_name.starts_with("http://") || site_name.starts_with("https://") {
425 site_name.clone()
426 } else {
427 format!("https://{site_name}")
428 };
429 if let Ok(parsed) = url::Url::parse(&url_str) {
430 let current = sdm.cookies_for_url(parsed.clone(), CookieSource::HTTP);
431 let has_match = current.iter().any(|c| c.name() == name);
432 if has_match {
433 sdm.clear_site_data(&[&site_name], StorageType::Cookies);
434 for c in current {
435 if c.name() != name {
436 sdm.set_cookie_for_url(parsed.clone(), c, None);
437 }
438 }
439 }
440 }
441 }
442 }
443 self.touch();
444 Ok(())
445 }
446
447 pub fn wait_for_selector(&self, selector: &str, timeout: Duration) -> Result<(), BrowserError> {
450 let js = format!(
451 "(function() {{ return document.querySelector({}) !== null; }})()",
452 serde_json::to_string(selector).unwrap_or_default()
453 );
454 let start = Instant::now();
455 while start.elapsed() < timeout {
456 match self.evaluate_js_web(&js) {
457 Ok(ref result) if result == "true" => {
458 self.touch();
459 return Ok(());
460 }
461 Ok(_) => {}
462 Err(BrowserError::JavaScript(ref msg)) if msg.contains("InternalError") => {
463 self.servo.spin_event_loop();
465 self.webview.paint();
466 continue;
467 }
468 Err(e) => return Err(e),
469 }
470 self.servo.spin_event_loop();
471 self.webview.paint();
472 std::thread::yield_now();
473 }
474 Err(BrowserError::Init(format!(
475 "waitForSelector timed out after {}ms for: {selector}",
476 timeout.as_millis()
477 )))
478 }
479
480 pub fn wait_for_function(
483 &self,
484 fn_expression: &str,
485 timeout: Duration,
486 ) -> Result<(), BrowserError> {
487 let js = format!("(function() {{ return !!({fn_expression}); }})()");
488 let start = Instant::now();
489 while start.elapsed() < timeout {
490 match self.evaluate_js_web(&js) {
491 Ok(ref result) if result == "true" => {
492 self.touch();
493 return Ok(());
494 }
495 Ok(_) => {}
496 Err(BrowserError::JavaScript(ref msg)) if msg.contains("InternalError") => {
497 self.servo.spin_event_loop();
498 self.webview.paint();
499 continue;
500 }
501 Err(e) => return Err(e),
502 }
503 self.servo.spin_event_loop();
504 self.webview.paint();
505 std::thread::yield_now();
506 }
507 Err(BrowserError::Init(format!(
508 "waitForFunction timed out after {}ms",
509 timeout.as_millis()
510 )))
511 }
512
513 pub fn wait_for_navigation(&self, timeout: Duration) -> Result<(), BrowserError> {
519 let start = Instant::now();
520 let initial_status = self.webview_state.borrow().load_status;
523 let mut saw_new_navigation = initial_status != servo::LoadStatus::Started;
524
525 while start.elapsed() < timeout {
526 let current_status = self.webview_state.borrow().load_status;
527
528 if current_status == servo::LoadStatus::Started {
529 saw_new_navigation = true;
531 }
532
533 if saw_new_navigation && current_status == servo::LoadStatus::Complete {
534 self.touch();
535 return Ok(());
536 }
537
538 self.servo.spin_event_loop();
539 self.webview.paint();
540 std::thread::yield_now();
541 }
542 Err(BrowserError::Init(format!(
543 "waitForNavigation timed out after {}ms",
544 timeout.as_millis()
545 )))
546 }
547
548 pub fn click_element(&self, selector: &str) -> Result<(), BrowserError> {
552 let js = format!(
554 "(function() {{ var e = document.querySelector({}); if (!e) return null; var r = e.getBoundingClientRect(); return JSON.stringify({{x: r.x + r.width/2, y: r.y + r.height/2}}); }})()",
555 serde_json::to_string(selector).unwrap_or_default()
556 );
557 let pos_str = self.evaluate_js_web(&js)?;
558 if pos_str == "null" || pos_str.is_empty() {
559 return Err(BrowserError::JavaScript(format!(
560 "element not found for click: {selector}"
561 )));
562 }
563 let pos: serde_json::Value = serde_json::from_str(&pos_str)
564 .map_err(|e| BrowserError::JavaScript(format!("invalid position JSON: {e}")))?;
565 let x = pos["x"].as_f64().unwrap_or(0.0) as f32;
566 let y = pos["y"].as_f64().unwrap_or(0.0) as f32;
567
568 self.dispatch_mouse_event(MouseButtonAction::Down, MouseButton::Left, x, y);
570 self.servo.spin_event_loop();
571 self.webview.paint();
572 self.dispatch_mouse_event(MouseButtonAction::Up, MouseButton::Left, x, y);
573 Ok(())
574 }
575
576 pub fn type_text(&self, text: &str) -> Result<(), BrowserError> {
579 for ch in text.chars() {
580 let key = match ch {
581 '\n' => Key::Named(NamedKey::Enter),
582 '\t' => Key::Named(NamedKey::Tab),
583 '\u{8}' => Key::Named(NamedKey::Backspace),
584 '\u{7f}' => Key::Named(NamedKey::Delete),
585 ' ' => Key::Character(" ".into()),
586 c => Key::Character(c.to_string()),
587 };
588 let code = key_code_for_char(ch);
589 self.dispatch_key_event_full(
590 KeyState::Down,
591 key.clone(),
592 code.clone(),
593 Location::Standard,
594 Modifiers::empty(),
595 false,
596 );
597 self.servo.spin_event_loop();
598 self.webview.paint();
599 self.dispatch_key_event_full(
600 KeyState::Up,
601 key,
602 code,
603 Location::Standard,
604 Modifiers::empty(),
605 false,
606 );
607 }
608 Ok(())
609 }
610
611 pub fn fill(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
614 let js = format!(
615 "(function() {{ var e = document.querySelector({}); if (!e) return false; e.value = {}; e.dispatchEvent(new Event('input', {{bubbles: true}})); e.dispatchEvent(new Event('change', {{bubbles: true}})); return true; }})()",
616 serde_json::to_string(selector).unwrap_or_default(),
617 serde_json::to_string(value).unwrap_or_default(),
618 );
619 let result = self.evaluate_js_web(&js)?;
620 if result == "false" {
621 return Err(BrowserError::JavaScript(format!(
622 "element not found for fill: {selector}"
623 )));
624 }
625 Ok(())
626 }
627
628 pub fn set_content(&self, html: &str) -> Result<(), BrowserError> {
630 let js = format!(
631 "(function() {{ document.open(); document.write({}); document.close(); }})()",
632 serde_json::to_string(html).unwrap_or_default(),
633 );
634 self.evaluate_js_web(&js)?;
635 Ok(())
636 }
637
638 pub fn content(&self) -> Result<String, BrowserError> {
640 self.evaluate_js_web("document.documentElement.outerHTML")
641 }
642
643 pub fn select(&self, selector: &str, values: &[&str]) -> Result<(), BrowserError> {
646 let values_json = serde_json::to_string(&values).unwrap_or_default();
647 let js = format!(
648 "(function() {{ var e = document.querySelector({}); if (!e) return false; var vals = {values_json}; Array.from(e.options).forEach(function(o) {{ o.selected = vals.indexOf(o.value) !== -1; }}); e.dispatchEvent(new Event('change', {{bubbles: true}})); return true; }})()",
649 serde_json::to_string(selector).unwrap_or_default(),
650 );
651 let result = self.evaluate_js_web(&js)?;
652 if result == "false" {
653 return Err(BrowserError::JavaScript(format!(
654 "element not found for select: {selector}"
655 )));
656 }
657 Ok(())
658 }
659
660 pub fn press(&self, key: &str) -> Result<(), BrowserError> {
662 let (key_val, code_val) = parse_key_name(key);
663 self.dispatch_key_event_full(
664 KeyState::Down,
665 key_val.clone(),
666 code_val.clone(),
667 Location::Standard,
668 Modifiers::empty(),
669 false,
670 );
671 self.servo.spin_event_loop();
672 self.webview.paint();
673 self.dispatch_key_event_full(
674 KeyState::Up,
675 key_val,
676 code_val,
677 Location::Standard,
678 Modifiers::empty(),
679 false,
680 );
681 Ok(())
682 }
683
684 pub fn hover(&self, selector: &str) -> Result<(), BrowserError> {
687 let js = format!(
688 "(function() {{ var e = document.querySelector({}); if (!e) return null; var r = e.getBoundingClientRect(); return JSON.stringify({{x: r.x + r.width/2, y: r.y + r.height/2}}); }})()",
689 serde_json::to_string(selector).unwrap_or_default()
690 );
691 let pos_str = self.evaluate_js_web(&js)?;
692 if pos_str == "null" || pos_str.is_empty() {
693 return Err(BrowserError::JavaScript(format!(
694 "element not found for hover: {selector}"
695 )));
696 }
697 let pos: serde_json::Value = serde_json::from_str(&pos_str)
698 .map_err(|e| BrowserError::JavaScript(format!("invalid position JSON: {e}")))?;
699 let x = pos["x"].as_f64().unwrap_or(0.0) as f32;
700 let y = pos["y"].as_f64().unwrap_or(0.0) as f32;
701 self.dispatch_mouse_move(x, y);
702 Ok(())
703 }
704
705 pub fn focus_element(&self, selector: &str) -> Result<(), BrowserError> {
707 let js = format!(
708 "(function() {{ var e = document.querySelector({}); if (!e) return false; e.focus(); return true; }})()",
709 serde_json::to_string(selector).unwrap_or_default()
710 );
711 let result = self.evaluate_js_web(&js)?;
712 if result == "false" {
713 return Err(BrowserError::JavaScript(format!(
714 "element not found for focus: {selector}"
715 )));
716 }
717 Ok(())
718 }
719
720 pub fn take_screenshot_advanced(
722 &self,
723 format: ScreenshotFormat,
724 clip: Option<(f64, f64, f64, f64)>,
725 full_page: bool,
726 ) -> Result<Vec<u8>, BrowserError> {
727 let original_viewport = self.viewport;
728
729 if full_page {
730 let height_js = "document.documentElement.scrollHeight";
732 let height_str = self.evaluate_js_web(height_js).unwrap_or_default();
733 let full_height: u32 = height_str
734 .trim()
735 .parse()
736 .unwrap_or(original_viewport.height);
737 let capped_height = full_height.max(original_viewport.height);
738 if capped_height != original_viewport.height {
739 self.set_viewport(original_viewport.width, capped_height);
740 self.servo.spin_event_loop();
742 self.webview.paint();
743 }
744 }
745
746 let result = self.take_screenshot(format);
747
748 if full_page && original_viewport != self.viewport {
750 self.set_viewport(original_viewport.width, original_viewport.height);
751 }
752
753 let image_bytes = result?;
754
755 if let Some((x, y, w, h)) = clip {
757 let mut img = image::load_from_memory(&image_bytes).map_err(|e| {
758 BrowserError::Rendering(format!("failed to decode screenshot for clip: {e}"))
759 })?;
760 let crop_x = x.max(0.0) as u32;
761 let crop_y = y.max(0.0) as u32;
762 let crop_w = (w as u32).min(img.width().saturating_sub(crop_x));
763 let crop_h = (h as u32).min(img.height().saturating_sub(crop_y));
764 if crop_w == 0 || crop_h == 0 {
765 return Err(BrowserError::Rendering(
766 "clip region has zero dimensions".into(),
767 ));
768 }
769 let cropped = img.crop(crop_x, crop_y, crop_w, crop_h);
770 let rgba = cropped.to_rgba8();
771 let fmt = if image_bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
773 ScreenshotFormat::Png
774 } else {
775 ScreenshotFormat::Jpeg
776 };
777 return encode_image(&rgba, fmt);
778 }
779
780 Ok(image_bytes)
781 }
782
783 pub fn page_title(&self) -> Option<String> {
784 self.webview_state.borrow().title.clone()
785 }
786
787 pub fn current_url(&self) -> Option<String> {
788 self.webview_state
789 .borrow()
790 .url
791 .as_ref()
792 .map(|u| u.to_string())
793 }
794
795 pub fn get_state(&self) -> PageState {
796 let stored = *self.state.borrow();
808 if stored == PageState::Navigating
809 && self.webview_state.borrow().load_status == servo::LoadStatus::Complete
810 {
811 return PageState::Interactive;
812 }
813 stored
814 }
815
816 fn spin_servo(
820 &self,
821 timeout: Duration,
822 callback: impl Fn() -> bool,
823 ) -> Result<(), BrowserError> {
824 let start = Instant::now();
825 while callback() {
826 self.servo.spin_event_loop();
827 self.webview.paint();
828 if start.elapsed() > timeout {
829 return Err(BrowserError::Init("operation timed out".into()));
830 }
831 std::thread::yield_now();
834 }
835 Ok(())
836 }
837}
838
839#[derive(Clone)]
840pub struct PageHandle {
841 inner: Rc<RefCell<Option<PageInner>>>,
842 id: usize,
843 servo: Rc<Servo>,
844 delegate: Rc<crate::delegate::BaoServoDelegate>,
845}
846
847impl PageHandle {
848 pub(crate) fn new(
849 servo: Rc<Servo>,
850 servo_delegate: Rc<crate::delegate::BaoServoDelegate>,
851 config: &PageConfig,
852 default_viewport: PhysicalSize<u32>,
853 id: usize,
854 ) -> Result<Self, BrowserError> {
855 let viewport = PhysicalSize::new(
856 config.viewport_width.unwrap_or(default_viewport.width),
857 config.viewport_height.unwrap_or(default_viewport.height),
858 );
859
860 let rendering_context = Rc::new(
861 SoftwareRenderingContext::new(viewport)
862 .map_err(|e| BrowserError::Init(format!("rendering context failed: {e:?}")))?,
863 );
864
865 let webview_state = Rc::new(RefCell::new(BaoWebViewState::default()));
866 if let Some(tx) = servo_delegate.console_log_tx() {
868 webview_state.borrow_mut().console_log_tx = Some(tx);
869 }
870 if let Some(ref profile) = config.stealth_profile {
876 webview_state.borrow_mut().set_worker_scope_config(
877 crate::delegate::WorkerScopeConfig::from(profile as &bao_stealth::StealthProfile),
878 );
879 }
880 let webview_delegate =
881 Rc::new(BaoWebViewDelegate::new(Rc::clone(&webview_state), viewport));
882 let state = Rc::new(RefCell::new(PageState::Created));
883
884 let mut builder = WebViewBuilder::new(
885 &servo,
886 rendering_context.clone() as Rc<dyn RenderingContext>,
887 )
888 .delegate(Rc::clone(&webview_delegate) as Rc<dyn servo::WebViewDelegate>);
889
890 let user_content_manager = Rc::new(servo::UserContentManager::new(&servo));
895 builder = builder.user_content_manager(Rc::clone(&user_content_manager));
896
897 if let Some(ref url_str) = config.url {
898 let url = url::Url::parse(url_str)
899 .map_err(|e| BrowserError::Init(format!("invalid URL: {e}")))?;
900 builder = builder.url(url);
901 }
902
903 let webview = builder.build();
904
905 let inner = PageInner {
906 id,
907 webview,
908 servo: Rc::clone(&servo),
909 rendering_context,
910 delegate: webview_delegate,
911 state,
912 webview_state,
913 nav_seq: ::std::cell::Cell::new(0),
914 viewport,
915 stealth_profile: config.stealth_profile.clone(),
916 permission: match &config.permission {
917 Some(perm) => PermissionGuard::new(perm.clone()),
918 None => PermissionGuard::none(),
919 },
920 user_content_manager: Some(user_content_manager),
921 last_active_at: RefCell::new(Instant::now()),
922 created_at: Instant::now(),
923 node_realm_global: RefCell::new(std::ptr::null_mut()),
924 page_global: RefCell::new(std::ptr::null_mut()),
925 };
926
927 Ok(PageHandle {
928 inner: Rc::new(RefCell::new(Some(inner))),
929 id,
930 servo,
931 delegate: servo_delegate,
932 })
933 }
934
935 pub fn id(&self) -> usize {
936 self.id
937 }
938
939 pub fn navigate(&self, url: &str) -> Result<(), BrowserError> {
940 self.with_inner(|inner| inner.navigate(url))
941 }
942
943 pub fn wait_for_pipeline_ready(&self, timeout: Duration) -> Result<(), BrowserError> {
954 let start = Instant::now();
955
956 while start.elapsed() < timeout {
959 let ready = self
961 .with_inner_opt(|inner| Some(inner.webview_state.borrow().frame_ready))
962 .unwrap_or(false);
963 if ready {
964 let nav_seq = self
977 .with_inner_opt(|inner| Some(inner.nav_seq.get()))
978 .unwrap_or(0);
979 if nav_seq > 0 {
980 while start.elapsed() < timeout {
981 let loaded = self
982 .with_inner_opt(|inner| {
983 Some(
984 inner.webview_state.borrow().load_status
985 == servo::LoadStatus::Complete,
986 )
987 })
988 .unwrap_or(false);
989 if loaded {
990 break;
991 }
992 self.with_inner(|inner| {
993 inner.servo.spin_event_loop();
994 Ok(())
995 })?;
996 std::thread::yield_now();
997 }
998 }
999 return self.drain_callbacks().map(|_| ());
1000 }
1001
1002 self.with_inner(|inner| {
1004 inner.servo.spin_event_loop();
1005 Ok(())
1006 })?;
1007
1008 std::thread::yield_now();
1010 }
1011
1012 Err(BrowserError::Init(
1013 "pipeline not ready after timeout".into(),
1014 ))
1015 }
1016
1017 pub fn drain_callbacks(&self) -> Result<String, BrowserError> {
1018 self.with_inner(|inner| inner.drain_callbacks())
1019 }
1020
1021 pub fn evaluate_js(&self, script: &str) -> Result<String, BrowserError> {
1024 self.with_inner(|inner| inner.evaluate_js(script))
1025 }
1026
1027 pub fn evaluate_js_web(&self, script: &str) -> Result<String, BrowserError> {
1032 self.with_inner(|inner| inner.evaluate_js_web(script))
1033 }
1034
1035 pub fn add_script_to_evaluate_on_new_document(&self, source: &str) -> Result<(), BrowserError> {
1043 self.with_inner(|inner| {
1044 let ucm = inner
1045 .user_content_manager
1046 .clone()
1047 .ok_or_else(|| BrowserError::Init("page has no UserContentManager".into()))?;
1048 ucm.add_script(Rc::new(servo::UserScript::new(source.to_string(), None)));
1049 Ok(())
1050 })
1051 }
1052
1053 pub fn take_screenshot(&self, format: ScreenshotFormat) -> Result<Vec<u8>, BrowserError> {
1054 self.with_inner(|inner| inner.take_screenshot(format))
1055 }
1056
1057 pub fn page_title(&self) -> Option<String> {
1058 self.with_inner_opt(|inner| inner.page_title())
1059 }
1060
1061 pub fn current_url(&self) -> Option<String> {
1062 self.with_inner_opt(|inner| inner.current_url())
1063 }
1064
1065 pub fn servo(&self) -> &Rc<Servo> {
1068 &self.servo
1069 }
1070
1071 pub fn get_state(&self) -> PageState {
1072 self.inner
1073 .borrow()
1074 .as_ref()
1075 .map_or(PageState::Closed, |inner| inner.get_state())
1076 }
1077
1078 pub fn is_alive(&self) -> bool {
1079 self.inner.borrow().is_some()
1080 }
1081
1082 pub(crate) fn webview_id(&self) -> Option<servo::WebViewId> {
1083 self.inner.borrow().as_ref().map(|inner| inner.webview.id())
1084 }
1085
1086 pub fn permission(&self) -> PermissionGuard {
1087 let borrow = self.inner.borrow();
1088 match borrow.as_ref() {
1089 Some(inner) => inner.permission.clone(),
1090 None => PermissionGuard::none(),
1091 }
1092 }
1093
1094 pub fn stealth_profile(&self) -> Option<bao_stealth::StealthProfile> {
1095 self.inner
1096 .borrow()
1097 .as_ref()
1098 .and_then(|inner| inner.stealth_profile.clone())
1099 }
1100
1101 pub fn webview_state(&self) -> Rc<RefCell<BaoWebViewState>> {
1108 self.inner
1109 .borrow()
1110 .as_ref()
1111 .map(|inner| inner.webview_state.clone())
1112 .unwrap_or_else(|| Rc::new(RefCell::new(BaoWebViewState::default())))
1113 }
1114
1115 pub fn wait_for_selector(&self, selector: &str, timeout: Duration) -> Result<(), BrowserError> {
1119 self.with_inner(|inner| inner.wait_for_selector(selector, timeout))
1120 }
1121
1122 pub fn wait_for_function(
1124 &self,
1125 fn_expression: &str,
1126 timeout: Duration,
1127 ) -> Result<(), BrowserError> {
1128 self.with_inner(|inner| inner.wait_for_function(fn_expression, timeout))
1129 }
1130
1131 pub fn wait_for_navigation(&self, timeout: Duration) -> Result<(), BrowserError> {
1133 self.with_inner(|inner| inner.wait_for_navigation(timeout))
1134 }
1135
1136 pub fn click(&self, selector: &str) -> Result<(), BrowserError> {
1138 self.with_inner(|inner| inner.click_element(selector))
1139 }
1140
1141 pub fn type_text(&self, text: &str) -> Result<(), BrowserError> {
1143 self.with_inner(|inner| inner.type_text(text))
1144 }
1145
1146 pub fn fill(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
1148 self.with_inner(|inner| inner.fill(selector, value))
1149 }
1150
1151 pub fn set_content(&self, html: &str) -> Result<(), BrowserError> {
1153 self.with_inner(|inner| inner.set_content(html))
1154 }
1155
1156 pub fn content(&self) -> Result<String, BrowserError> {
1158 self.with_inner(|inner| inner.content())
1159 }
1160
1161 pub fn set_viewport(&self, width: u32, height: u32) -> Result<(), BrowserError> {
1163 self.with_inner(|inner| {
1164 inner.set_viewport(width, height);
1165 Ok(())
1166 })
1167 }
1168
1169 pub fn cookies(&self, urls: &[String]) -> Result<Vec<cookie::Cookie<'static>>, BrowserError> {
1171 self.with_inner(|inner| inner.cookies(urls))
1172 }
1173
1174 pub fn set_cookie(
1176 &self,
1177 url: &str,
1178 cookie: cookie::Cookie<'static>,
1179 ) -> Result<(), BrowserError> {
1180 self.with_inner(|inner| inner.set_cookie(url, cookie))
1181 }
1182
1183 pub fn delete_cookie(&self, name: &str, url: Option<&str>) -> Result<(), BrowserError> {
1185 self.with_inner(|inner| inner.delete_cookie(name, url))
1186 }
1187
1188 pub fn select(&self, selector: &str, values: &[&str]) -> Result<(), BrowserError> {
1190 self.with_inner(|inner| inner.select(selector, values))
1191 }
1192
1193 pub fn press(&self, key: &str) -> Result<(), BrowserError> {
1195 self.with_inner(|inner| inner.press(key))
1196 }
1197
1198 pub fn hover(&self, selector: &str) -> Result<(), BrowserError> {
1200 self.with_inner(|inner| inner.hover(selector))
1201 }
1202
1203 pub fn focus_element(&self, selector: &str) -> Result<(), BrowserError> {
1205 self.with_inner(|inner| inner.focus_element(selector))
1206 }
1207
1208 pub fn reload(&self) -> Result<(), BrowserError> {
1210 self.with_inner(|inner| inner.reload())
1211 }
1212
1213 pub fn go_back(&self) -> Result<(), BrowserError> {
1215 self.with_inner(|inner| inner.go_back())
1216 }
1217
1218 pub fn go_forward(&self) -> Result<(), BrowserError> {
1220 self.with_inner(|inner| inner.go_forward())
1221 }
1222
1223 pub fn can_go_back(&self) -> bool {
1225 self.with_inner_opt(|inner| Some(inner.can_go_back()))
1226 .unwrap_or(false)
1227 }
1228
1229 pub fn can_go_forward(&self) -> bool {
1231 self.with_inner_opt(|inner| Some(inner.can_go_forward()))
1232 .unwrap_or(false)
1233 }
1234
1235 pub fn take_screenshot_advanced(
1237 &self,
1238 format: ScreenshotFormat,
1239 clip: Option<(f64, f64, f64, f64)>,
1240 full_page: bool,
1241 ) -> Result<Vec<u8>, BrowserError> {
1242 self.with_inner(|inner| inner.take_screenshot_advanced(format, clip, full_page))
1243 }
1244
1245 pub fn dispatch_mouse_event(
1247 &self,
1248 action: MouseButtonAction,
1249 button: MouseButton,
1250 x: f32,
1251 y: f32,
1252 ) -> Result<(), BrowserError> {
1253 self.with_inner(|inner| {
1254 inner.dispatch_mouse_event(action, button, x, y);
1255 Ok(())
1256 })
1257 }
1258
1259 pub fn dispatch_mouse_move(&self, x: f32, y: f32) -> Result<(), BrowserError> {
1261 self.with_inner(|inner| {
1262 inner.dispatch_mouse_move(x, y);
1263 Ok(())
1264 })
1265 }
1266
1267 pub fn dispatch_key_event(
1269 &self,
1270 state: KeyState,
1271 key: Key,
1272 code: Code,
1273 ) -> Result<(), BrowserError> {
1274 self.with_inner(|inner| {
1275 inner.dispatch_key_event(state, key, code);
1276 Ok(())
1277 })
1278 }
1279
1280 pub fn close(&self) -> Result<(), BrowserError> {
1281 let mut borrow = self.inner.borrow_mut();
1282 if let Some(inner) = borrow.take() {
1283 *inner.state.borrow_mut() = PageState::Closing;
1287 {
1299 let mut ws = inner.webview_state.borrow_mut();
1300 if ws.active_worker_count() > 0 {
1301 log::debug!(
1302 "[page] close: terminating {} active workers",
1303 ws.active_worker_count()
1304 );
1305 ws.terminate_all_workers();
1306 }
1307 }
1308 let pg = *inner.page_global.borrow();
1309 let ng = *inner.node_realm_global.borrow();
1310 if let Some(wid) = inner.webview_id_opt() {
1314 crate::runtime_bridge::remove_node_realm_by_id(wid);
1315 }
1316 if !pg.is_null() {
1317 bao_stealth::engine_props::remove_profile_for_global(pg as usize);
1321 }
1322 if !ng.is_null() {
1323 bao_stealth::engine_props::remove_profile_for_global(ng as usize);
1324 }
1325 *inner.state.borrow_mut() = PageState::Closed;
1328 drop(inner);
1329 }
1330 Ok(())
1331 }
1332
1333 fn with_inner<F, R>(&self, f: F) -> Result<R, BrowserError>
1334 where
1335 F: FnOnce(&PageInner) -> Result<R, BrowserError>,
1336 {
1337 let borrow = self.inner.borrow();
1338 match borrow.as_ref() {
1339 Some(inner) => f(inner),
1340 None => Err(BrowserError::Init("page is closed".into())),
1341 }
1342 }
1343
1344 pub fn set_page_global(
1347 &self,
1348 page_global: *mut mozjs::jsapi::JSObject,
1349 node_global: *mut mozjs::jsapi::JSObject,
1350 ) {
1351 let borrow = self.inner.borrow();
1352 if let Some(inner) = borrow.as_ref() {
1353 *inner.page_global.borrow_mut() = page_global;
1354 *inner.node_realm_global.borrow_mut() = node_global;
1355 }
1356 }
1357
1358 pub fn has_node_realm(&self) -> (bool, bool) {
1362 let borrow = self.inner.borrow();
1363 if let Some(inner) = borrow.as_ref() {
1364 let pg = *inner.page_global.borrow();
1365 let ng = *inner.node_realm_global.borrow();
1366 return (!pg.is_null(), !ng.is_null());
1367 }
1368 (false, false)
1369 }
1370
1371 fn with_inner_opt<F, R>(&self, f: F) -> Option<R>
1372 where
1373 F: FnOnce(&PageInner) -> Option<R>,
1374 {
1375 let borrow = self.inner.borrow();
1376 borrow.as_ref().and_then(f)
1377 }
1378}
1379
1380fn format_js_value(v: &servo::JSValue) -> String {
1381 match v {
1382 servo::JSValue::String(s) => s.clone(),
1383 servo::JSValue::Number(n) => n.to_string(),
1384 servo::JSValue::Boolean(b) => b.to_string(),
1385 servo::JSValue::Null => "null".into(),
1386 servo::JSValue::Undefined => "undefined".into(),
1387 servo::JSValue::Element(id) => format!("[Element: {id}]"),
1388 servo::JSValue::ShadowRoot(id) => format!("[ShadowRoot: {id}]"),
1389 servo::JSValue::Frame(id) => format!("[Frame: {id}]"),
1390 servo::JSValue::Window(id) => format!("[Window: {id}]"),
1391 servo::JSValue::Array(items) => {
1392 let formatted: Vec<String> = items.iter().map(format_js_value).collect();
1393 format!("[{}]", formatted.join(", "))
1394 }
1395 servo::JSValue::Object(map) => {
1396 let formatted: Vec<String> = map
1397 .iter()
1398 .map(|(k, val)| format!("{}: {}", k, format_js_value(val)))
1399 .collect();
1400 format!("{{{}}}", formatted.join(", "))
1401 }
1402 }
1403}
1404
1405fn key_code_for_char(ch: char) -> Code {
1407 match ch {
1408 'a' => Code::KeyA,
1409 'b' => Code::KeyB,
1410 'c' => Code::KeyC,
1411 'd' => Code::KeyD,
1412 'e' => Code::KeyE,
1413 'f' => Code::KeyF,
1414 'g' => Code::KeyG,
1415 'h' => Code::KeyH,
1416 'i' => Code::KeyI,
1417 'j' => Code::KeyJ,
1418 'k' => Code::KeyK,
1419 'l' => Code::KeyL,
1420 'm' => Code::KeyM,
1421 'n' => Code::KeyN,
1422 'o' => Code::KeyO,
1423 'p' => Code::KeyP,
1424 'q' => Code::KeyQ,
1425 'r' => Code::KeyR,
1426 's' => Code::KeyS,
1427 't' => Code::KeyT,
1428 'u' => Code::KeyU,
1429 'v' => Code::KeyV,
1430 'w' => Code::KeyW,
1431 'x' => Code::KeyX,
1432 'y' => Code::KeyY,
1433 'z' => Code::KeyZ,
1434 'A' => Code::KeyA,
1435 'B' => Code::KeyB,
1436 'C' => Code::KeyC,
1437 'D' => Code::KeyD,
1438 'E' => Code::KeyE,
1439 'F' => Code::KeyF,
1440 'G' => Code::KeyG,
1441 'H' => Code::KeyH,
1442 'I' => Code::KeyI,
1443 'J' => Code::KeyJ,
1444 'K' => Code::KeyK,
1445 'L' => Code::KeyL,
1446 'M' => Code::KeyM,
1447 'N' => Code::KeyN,
1448 'O' => Code::KeyO,
1449 'P' => Code::KeyP,
1450 'Q' => Code::KeyQ,
1451 'R' => Code::KeyR,
1452 'S' => Code::KeyS,
1453 'T' => Code::KeyT,
1454 'U' => Code::KeyU,
1455 'V' => Code::KeyV,
1456 'W' => Code::KeyW,
1457 'X' => Code::KeyX,
1458 'Y' => Code::KeyY,
1459 'Z' => Code::KeyZ,
1460 '0' => Code::Digit0,
1461 '1' => Code::Digit1,
1462 '2' => Code::Digit2,
1463 '3' => Code::Digit3,
1464 '4' => Code::Digit4,
1465 '5' => Code::Digit5,
1466 '6' => Code::Digit6,
1467 '7' => Code::Digit7,
1468 '8' => Code::Digit8,
1469 '9' => Code::Digit9,
1470 '\n' => Code::Enter,
1471 '\t' => Code::Tab,
1472 '\u{8}' => Code::Backspace,
1473 '\u{7f}' => Code::Delete,
1474 ' ' => Code::Space,
1475 ';' => Code::Semicolon,
1476 '=' => Code::Equal,
1477 ',' => Code::Comma,
1478 '-' => Code::Minus,
1479 '.' => Code::Period,
1480 '/' => Code::Slash,
1481 '`' => Code::Backquote,
1482 '[' => Code::BracketLeft,
1483 '\\' => Code::Backslash,
1484 ']' => Code::BracketRight,
1485 '\'' => Code::Quote,
1486 _ => Code::Unidentified,
1487 }
1488}
1489
1490fn parse_key_name(name: &str) -> (Key, Code) {
1492 match name {
1493 "Enter" => (Key::Named(NamedKey::Enter), Code::Enter),
1494 "Tab" => (Key::Named(NamedKey::Tab), Code::Tab),
1495 "Escape" | "Esc" => (Key::Named(NamedKey::Escape), Code::Escape),
1496 "Backspace" => (Key::Named(NamedKey::Backspace), Code::Backspace),
1497 "Delete" => (Key::Named(NamedKey::Delete), Code::Delete),
1498 "Space" => (Key::Character(" ".into()), Code::Space),
1499 "ArrowUp" => (Key::Named(NamedKey::ArrowUp), Code::ArrowUp),
1500 "ArrowDown" => (Key::Named(NamedKey::ArrowDown), Code::ArrowDown),
1501 "ArrowLeft" => (Key::Named(NamedKey::ArrowLeft), Code::ArrowLeft),
1502 "ArrowRight" => (Key::Named(NamedKey::ArrowRight), Code::ArrowRight),
1503 "Home" => (Key::Named(NamedKey::Home), Code::Home),
1504 "End" => (Key::Named(NamedKey::End), Code::End),
1505 "PageUp" => (Key::Named(NamedKey::PageUp), Code::PageUp),
1506 "PageDown" => (Key::Named(NamedKey::PageDown), Code::PageDown),
1507 "Insert" => (Key::Named(NamedKey::Insert), Code::Insert),
1508 "F1" => (Key::Named(NamedKey::F1), Code::F1),
1509 "F2" => (Key::Named(NamedKey::F2), Code::F2),
1510 "F3" => (Key::Named(NamedKey::F3), Code::F3),
1511 "F4" => (Key::Named(NamedKey::F4), Code::F4),
1512 "F5" => (Key::Named(NamedKey::F5), Code::F5),
1513 "F6" => (Key::Named(NamedKey::F6), Code::F6),
1514 "F7" => (Key::Named(NamedKey::F7), Code::F7),
1515 "F8" => (Key::Named(NamedKey::F8), Code::F8),
1516 "F9" => (Key::Named(NamedKey::F9), Code::F9),
1517 "F10" => (Key::Named(NamedKey::F10), Code::F10),
1518 "F11" => (Key::Named(NamedKey::F11), Code::F11),
1519 "F12" => (Key::Named(NamedKey::F12), Code::F12),
1520 "ControlLeft" | "Control" => (Key::Named(NamedKey::Control), Code::ControlLeft),
1521 "ControlRight" => (Key::Named(NamedKey::Control), Code::ControlRight),
1522 "ShiftLeft" | "Shift" => (Key::Named(NamedKey::Shift), Code::ShiftLeft),
1523 "ShiftRight" => (Key::Named(NamedKey::Shift), Code::ShiftRight),
1524 "AltLeft" | "Alt" => (Key::Named(NamedKey::Alt), Code::AltLeft),
1525 "AltRight" => (Key::Named(NamedKey::Alt), Code::AltRight),
1526 "MetaLeft" | "Meta" => (Key::Named(NamedKey::Meta), Code::MetaLeft),
1527 "MetaRight" => (Key::Named(NamedKey::Meta), Code::MetaRight),
1528 "CapsLock" => (Key::Named(NamedKey::CapsLock), Code::CapsLock),
1529 "NumLock" => (Key::Named(NamedKey::NumLock), Code::NumLock),
1530 "ScrollLock" => (Key::Named(NamedKey::ScrollLock), Code::ScrollLock),
1531 s if s.chars().count() == 1 => {
1533 let ch = s.chars().next().unwrap();
1534 let key = Key::Character(ch.to_string());
1535 let code = key_code_for_char(ch);
1536 (key, code)
1537 }
1538 s => (Key::Character(s.to_string()), Code::Unidentified),
1540 }
1541}
1542
1543#[cfg(test)]
1545mod tests {
1546 use super::*;
1547
1548 #[test]
1549 fn page_state_variants_equal_to_themselves() {
1550 assert_eq!(PageState::Created, PageState::Created);
1551 assert_eq!(PageState::Navigating, PageState::Navigating);
1552 assert_eq!(PageState::Interactive, PageState::Interactive);
1553 assert_eq!(PageState::Idle, PageState::Idle);
1554 assert_eq!(PageState::Closing, PageState::Closing);
1555 assert_eq!(PageState::Closed, PageState::Closed);
1556 }
1557
1558 #[test]
1559 fn page_state_clone_works() {
1560 let state = PageState::Navigating;
1561 let cloned = state.clone();
1562 assert_eq!(state, cloned);
1563 }
1564
1565 #[test]
1566 fn page_state_copy_works() {
1567 let state = PageState::Interactive;
1568 let copied: PageState = state;
1569 assert_eq!(state, copied);
1570 }
1571
1572 #[test]
1573 fn page_state_debug_format_includes_variant_name() {
1574 assert!(format!("{:?}", PageState::Created).contains("Created"));
1575 assert!(format!("{:?}", PageState::Navigating).contains("Navigating"));
1576 assert!(format!("{:?}", PageState::Interactive).contains("Interactive"));
1577 assert!(format!("{:?}", PageState::Idle).contains("Idle"));
1578 assert!(format!("{:?}", PageState::Closing).contains("Closing"));
1579 assert!(format!("{:?}", PageState::Closed).contains("Closed"));
1580 }
1581
1582 #[test]
1583 fn page_state_closing_distinct_from_neighbors() {
1584 assert_ne!(PageState::Closing, PageState::Idle);
1587 assert_ne!(PageState::Closing, PageState::Interactive);
1588 assert_ne!(PageState::Closing, PageState::Closed);
1589 }
1590
1591 #[test]
1592 fn page_state_created_not_equal_closed() {
1593 assert_ne!(PageState::Created, PageState::Closed);
1594 }
1595
1596 #[test]
1597 fn format_js_value_string() {
1598 let value = servo::JSValue::String("hello".into());
1599 assert_eq!(format_js_value(&value), "hello");
1600 }
1601
1602 #[test]
1603 fn format_js_value_number() {
1604 let value = servo::JSValue::Number(42.5);
1605 assert_eq!(format_js_value(&value), "42.5");
1606 }
1607
1608 #[test]
1609 fn format_js_value_boolean_true() {
1610 let value = servo::JSValue::Boolean(true);
1611 assert_eq!(format_js_value(&value), "true");
1612 }
1613
1614 #[test]
1615 fn format_js_value_null() {
1616 let value = servo::JSValue::Null;
1617 assert_eq!(format_js_value(&value), "null");
1618 }
1619
1620 #[test]
1621 fn format_js_value_undefined() {
1622 let value = servo::JSValue::Undefined;
1623 assert_eq!(format_js_value(&value), "undefined");
1624 }
1625
1626 #[test]
1627 fn format_js_value_array() {
1628 let value = servo::JSValue::Array(vec![
1629 servo::JSValue::Number(1.0),
1630 servo::JSValue::Number(2.0),
1631 servo::JSValue::Number(3.0),
1632 ]);
1633 assert_eq!(format_js_value(&value), "[1, 2, 3]");
1634 }
1635
1636 #[test]
1637 fn format_js_value_object() {
1638 let mut map = HashMap::new();
1639 map.insert("name".into(), servo::JSValue::String("test".into()));
1640 map.insert("count".into(), servo::JSValue::Number(5.0));
1641 let value = servo::JSValue::Object(map);
1642 let result = format_js_value(&value);
1643 assert!(result.starts_with('{') && result.ends_with('}'));
1644 assert!(result.contains("name: test"));
1645 assert!(result.contains("count: 5"));
1646 }
1647
1648 #[test]
1649 fn format_js_value_element() {
1650 let value = servo::JSValue::Element("div#main".into());
1651 assert_eq!(format_js_value(&value), "[Element: div#main]");
1652 }
1653
1654 #[test]
1655 fn format_js_value_shadow_root() {
1656 let value = servo::JSValue::ShadowRoot("host-element".into());
1657 assert_eq!(format_js_value(&value), "[ShadowRoot: host-element]");
1658 }
1659
1660 #[test]
1661 fn format_js_value_frame() {
1662 let value = servo::JSValue::Frame("iframe-123".into());
1663 assert_eq!(format_js_value(&value), "[Frame: iframe-123]");
1664 }
1665
1666 #[test]
1667 fn format_js_value_window() {
1668 let value = servo::JSValue::Window("window-456".into());
1669 assert_eq!(format_js_value(&value), "[Window: window-456]");
1670 }
1671
1672 #[test]
1681 fn evaluate_js_uses_node_realm_or_iife_fallback() {
1682 let source = include_str!("page.rs");
1683 let func_start = source
1684 .find("pub fn evaluate_js(&self, script: &str)")
1685 .expect("evaluate_js function not found");
1686 let func_body = &source[func_start..func_start + 2800.min(source.len() - func_start)];
1687 assert!(
1689 func_body.contains("get_node_realm_global"),
1690 "REQ-SEC-002 REGRESSION: evaluate_js must check Node Realm global"
1691 );
1692 assert!(
1694 func_body.contains("evaluate_js_via_node_realm"),
1695 "REQ-SEC-002 REGRESSION: evaluate_js must use Node Realm execution"
1696 );
1697 assert!(
1699 func_body.contains("eager init failed"),
1700 "REQ-SEC-002 REGRESSION: evaluate_js must detect uninitialized Node Realm"
1701 );
1702 }
1703
1704 #[test]
1707 fn evaluate_js_drains_callbacks_for_result() {
1708 let source = include_str!("page.rs");
1709 let func_start = source
1710 .find("pub fn evaluate_js(&self, script: &str)")
1711 .expect("evaluate_js function not found");
1712 let func_body = &source[func_start..func_start + 2800.min(source.len() - func_start)];
1713 assert!(
1714 func_body.contains("drain_callbacks"),
1715 "REQ-SEC-002 REGRESSION: evaluate_js must drain callbacks after Node Realm execution"
1716 );
1717 }
1718
1719 #[test]
1722 fn evaluate_js_reads_evaluate_result() {
1723 let source = include_str!("page.rs");
1724 let func_start = source
1725 .find("pub fn evaluate_js(&self, script: &str)")
1726 .expect("evaluate_js function not found");
1727 let func_body = &source[func_start..func_start + 2800.min(source.len() - func_start)];
1728 assert!(
1729 func_body.contains("eval_result"),
1730 "REQ-SEC-002 REGRESSION: evaluate_js must read EvaluateResult"
1731 );
1732 }
1733
1734 #[test]
1737 fn page_global_has_no_node_apis() {
1738 let source = include_str!("runtime_bridge.rs");
1739 let func_start = source
1740 .find("unsafe fn install_all_native")
1741 .expect("install_all_native function not found");
1742 let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
1743
1744 assert!(
1745 func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
1746 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
1747 );
1748 assert!(
1749 func_body.contains("bun_runtime::timers::install_timer_globals"),
1750 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
1751 );
1752 assert!(
1753 !func_body.contains("globals::install_all("),
1754 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
1755 );
1756 assert!(
1757 !func_body.contains("globals::install_node_apis("),
1758 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis() on page global"
1759 );
1760 }
1761
1762 #[test]
1765 fn node_realm_has_node_apis() {
1766 let source = include_str!("runtime_bridge.rs");
1767 let func_start = source
1768 .find("unsafe fn create_node_realm_native")
1769 .expect("create_node_realm_native function not found");
1770 let func_end = source[func_start..]
1771 .find("pub fn inject_node_apis")
1772 .or_else(|| source[func_start..].find("/// Inject Node.js APIs as native"))
1773 .expect("end boundary not found");
1774 let func_body = &source[func_start..func_start + func_end];
1775
1776 assert!(
1777 func_body.contains("bun_runtime::globals::install_node_apis"),
1778 "REQ-SEC-002 REGRESSION: create_node_realm_native must install Node APIs on Node Realm global"
1779 );
1780 assert!(
1781 func_body.contains("bun_runtime::globals::install_web_apis"),
1782 "REQ-SEC-002: Node Realm must also have Web APIs for trusted scripts"
1783 );
1784 }
1785
1786 #[test]
1789 fn node_realm_uses_new_compartment() {
1790 let source = include_str!("runtime_bridge.rs");
1791 let func_start = source
1792 .find("unsafe fn create_node_realm_native")
1793 .expect("create_node_realm_native function not found");
1794 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
1795 assert!(
1796 func_body.contains("NewCompartmentAndZone"),
1797 "REQ-SEC-002 REGRESSION: Node Realm must use NewCompartmentAndZone"
1798 );
1799 assert!(
1800 func_body.contains("SIMPLE_GLOBAL_CLASS"),
1801 "REQ-SEC-002 REGRESSION: Node Realm must use SIMPLE_GLOBAL_CLASS"
1802 );
1803 }
1804
1805 #[test]
1807 fn evaluate_in_node_realm_uses_auto_realm() {
1808 let source = include_str!("runtime_bridge.rs");
1809
1810 let func_start = source
1812 .find("pub unsafe fn evaluate_in_node_realm")
1813 .expect("evaluate_in_node_realm function not found");
1814 let func_body_start = source[func_start..]
1815 .find("{")
1816 .expect("function body start not found");
1817 let search_limit = source[func_start + func_body_start..]
1818 .find("unsafe fn create_node_realm_native")
1819 .unwrap_or(3000)
1820 .min(3000);
1821 let func_body =
1822 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
1823
1824 assert!(
1825 func_body.contains("AutoRealm::new"),
1826 "REQ-SEC-002 REGRESSION: evaluate_in_node_realm must use AutoRealm"
1827 );
1828 }
1829
1830 #[test]
1833 fn node_realm_global_stored_per_page() {
1834 let source = include_str!("runtime_bridge.rs");
1835 assert!(
1836 source.contains("NODE_REALM_BY_WEBVIEW"),
1837 "REQ-SEC-002 REGRESSION: must have NODE_REALM_BY_WEBVIEW per-page storage"
1838 );
1839 assert!(
1840 source.contains("store_node_realm"),
1841 "REQ-SEC-002 REGRESSION: must have store_node_realm accessor"
1842 );
1843 assert!(
1844 source.contains("get_node_realm_by_id"),
1845 "REQ-SEC-002 REGRESSION: must have get_node_realm_by_id accessor"
1846 );
1847 assert!(
1848 source.contains("get_node_realm_global"),
1849 "REQ-SEC-002 REGRESSION: must have get_node_realm_global accessor"
1850 );
1851 }
1852
1853 #[test]
1855 fn page_inner_has_node_realm_global_field() {
1856 let source = include_str!("page.rs");
1857 assert!(
1858 source.contains("node_realm_global: RefCell<*mut mozjs::jsapi::JSObject>"),
1859 "REQ-SEC-002 REGRESSION: PageInner must have node_realm_global field"
1860 );
1861 }
1862
1863 #[test]
1866 fn page_inner_has_drain_callbacks_method() {
1867 let source = include_str!("page.rs");
1868 assert!(
1869 source.contains("fn drain_callbacks(&self)"),
1870 "REQ-SEC-002 REGRESSION: PageInner must have drain_callbacks method"
1871 );
1872 assert!(
1873 source.contains("InternalError"),
1874 "REQ-SEC-002 REGRESSION: drain_callbacks must handle InternalError retry"
1875 );
1876 }
1877
1878 #[test]
1882 fn key_code_for_char_letters() {
1883 assert_eq!(super::key_code_for_char('a'), Code::KeyA);
1884 assert_eq!(super::key_code_for_char('Z'), Code::KeyZ);
1885 }
1886
1887 #[test]
1888 fn key_code_for_char_digits() {
1889 assert_eq!(super::key_code_for_char('0'), Code::Digit0);
1890 assert_eq!(super::key_code_for_char('9'), Code::Digit9);
1891 }
1892
1893 #[test]
1894 fn key_code_for_char_special() {
1895 assert_eq!(super::key_code_for_char('\n'), Code::Enter);
1896 assert_eq!(super::key_code_for_char('\t'), Code::Tab);
1897 assert_eq!(super::key_code_for_char(' '), Code::Space);
1898 }
1899
1900 #[test]
1901 fn parse_key_name_enter() {
1902 let (key, code) = super::parse_key_name("Enter");
1903 assert!(matches!(key, Key::Named(NamedKey::Enter)));
1904 assert_eq!(code, Code::Enter);
1905 }
1906
1907 #[test]
1908 fn parse_key_name_arrow_keys() {
1909 let (key, code) = super::parse_key_name("ArrowDown");
1910 assert!(matches!(key, Key::Named(NamedKey::ArrowDown)));
1911 assert_eq!(code, Code::ArrowDown);
1912
1913 let (key, code) = super::parse_key_name("ArrowUp");
1914 assert!(matches!(key, Key::Named(NamedKey::ArrowUp)));
1915 assert_eq!(code, Code::ArrowUp);
1916 }
1917
1918 #[test]
1919 fn parse_key_name_single_char() {
1920 let (key, code) = super::parse_key_name("a");
1921 assert!(matches!(key, Key::Character(s) if s == "a"));
1922 assert_eq!(code, Code::KeyA);
1923 }
1924
1925 #[test]
1926 fn parse_key_name_function_keys() {
1927 let (key, code) = super::parse_key_name("F1");
1928 assert!(matches!(key, Key::Named(NamedKey::F1)));
1929 assert_eq!(code, Code::F1);
1930 }
1931
1932 #[test]
1933 fn parse_key_name_escape_aliases() {
1934 let (key, code) = super::parse_key_name("Escape");
1935 assert!(matches!(key, Key::Named(NamedKey::Escape)));
1936 assert_eq!(code, Code::Escape);
1937
1938 let (key, code) = super::parse_key_name("Esc");
1939 assert!(matches!(key, Key::Named(NamedKey::Escape)));
1940 assert_eq!(code, Code::Escape);
1941 }
1942
1943 #[test]
1947 fn page_inner_has_wait_for_selector() {
1948 let source = include_str!("page.rs");
1949 assert!(
1950 source.contains("pub fn wait_for_selector("),
1951 "REQ-LIB-001: PageInner must have wait_for_selector method"
1952 );
1953 }
1954
1955 #[test]
1956 fn page_inner_has_wait_for_navigation() {
1957 let source = include_str!("page.rs");
1958 assert!(
1959 source.contains("pub fn wait_for_navigation("),
1960 "REQ-LIB-001: PageInner must have wait_for_navigation method"
1961 );
1962 }
1963
1964 #[test]
1965 fn page_inner_has_wait_for_function() {
1966 let source = include_str!("page.rs");
1967 assert!(
1968 source.contains("pub fn wait_for_function("),
1969 "REQ-LIB-001: PageInner must have wait_for_function method"
1970 );
1971 }
1972
1973 #[test]
1974 fn page_inner_has_click_element() {
1975 let source = include_str!("page.rs");
1976 assert!(
1977 source.contains("pub fn click_element("),
1978 "REQ-LIB-001: PageInner must have click_element method"
1979 );
1980 }
1981
1982 #[test]
1983 fn page_inner_has_type_text() {
1984 let source = include_str!("page.rs");
1985 assert!(
1986 source.contains("pub fn type_text("),
1987 "REQ-LIB-001: PageInner must have type_text method"
1988 );
1989 }
1990
1991 #[test]
1992 fn page_inner_has_fill() {
1993 let source = include_str!("page.rs");
1994 assert!(
1995 source.contains("pub fn fill("),
1996 "REQ-LIB-001: PageInner must have fill method"
1997 );
1998 }
1999
2000 #[test]
2001 fn page_inner_has_set_content() {
2002 let source = include_str!("page.rs");
2003 assert!(
2004 source.contains("pub fn set_content("),
2005 "REQ-LIB-001: PageInner must have set_content method"
2006 );
2007 }
2008
2009 #[test]
2010 fn page_inner_has_content() {
2011 let source = include_str!("page.rs");
2012 assert!(
2013 source.contains("pub fn content("),
2014 "REQ-LIB-001: PageInner must have content method"
2015 );
2016 }
2017
2018 #[test]
2019 fn page_inner_has_reload_go_back_go_forward() {
2020 let source = include_str!("page.rs");
2021 assert!(
2022 source.contains("pub fn reload(&self)"),
2023 "REQ-LIB-001: PageInner must have reload method"
2024 );
2025 assert!(
2026 source.contains("pub fn go_back(&self)"),
2027 "REQ-LIB-001: PageInner must have go_back method"
2028 );
2029 assert!(
2030 source.contains("pub fn go_forward(&self)"),
2031 "REQ-LIB-001: PageInner must have go_forward method"
2032 );
2033 }
2034
2035 #[test]
2036 fn page_inner_has_dispatch_mouse_event() {
2037 let source = include_str!("page.rs");
2038 assert!(
2039 source.contains("pub fn dispatch_mouse_event("),
2040 "REQ-LIB-001: PageInner must have dispatch_mouse_event method"
2041 );
2042 }
2043
2044 #[test]
2045 fn page_inner_has_dispatch_key_event() {
2046 let source = include_str!("page.rs");
2047 assert!(
2048 source.contains("pub fn dispatch_key_event("),
2049 "REQ-LIB-001: PageInner must have dispatch_key_event method"
2050 );
2051 }
2052
2053 #[test]
2054 fn page_handle_has_high_level_api() {
2055 let source = include_str!("page.rs");
2056 assert!(
2058 source.contains("pub fn click(&self"),
2059 "PageHandle must have click"
2060 );
2061 assert!(
2062 source.contains("pub fn type_text(&self"),
2063 "PageHandle must have type_text"
2064 );
2065 assert!(
2066 source.contains("pub fn fill(&self"),
2067 "PageHandle must have fill"
2068 );
2069 assert!(
2070 source.contains("pub fn set_content(&self"),
2071 "PageHandle must have set_content"
2072 );
2073 assert!(
2074 source.contains("pub fn content(&self"),
2075 "PageHandle must have content"
2076 );
2077 assert!(
2078 source.contains("pub fn press(&self"),
2079 "PageHandle must have press"
2080 );
2081 assert!(
2082 source.contains("pub fn hover(&self"),
2083 "PageHandle must have hover"
2084 );
2085 assert!(
2086 source.contains("pub fn focus_element(&self"),
2087 "PageHandle must have focus_element"
2088 );
2089 assert!(
2090 source.contains("pub fn reload(&self"),
2091 "PageHandle must have reload"
2092 );
2093 assert!(
2094 source.contains("pub fn go_back(&self"),
2095 "PageHandle must have go_back"
2096 );
2097 assert!(
2098 source.contains("pub fn go_forward(&self"),
2099 "PageHandle must have go_forward"
2100 );
2101 assert!(
2102 source.contains("pub fn select(&self"),
2103 "PageHandle must have select"
2104 );
2105 assert!(
2106 source.contains("pub fn set_viewport(&self"),
2107 "PageHandle must have set_viewport"
2108 );
2109 assert!(
2110 source.contains("pub fn cookies(&self"),
2111 "PageHandle must have cookies"
2112 );
2113 assert!(
2114 source.contains("pub fn set_cookie(&self"),
2115 "PageHandle must have set_cookie"
2116 );
2117 assert!(
2118 source.contains("pub fn delete_cookie(&self"),
2119 "PageHandle must have delete_cookie"
2120 );
2121 }
2122}