use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use dpi::PhysicalSize;
use servo::{
CSSPixel, Code, CookieSource, InputEvent, Key, KeyState, KeyboardEvent, Location, Modifiers,
MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, NamedKey, RenderingContext,
Servo, SoftwareRenderingContext, StorageType, WebView, WebViewBuilder, WebViewPoint,
};
use crate::config::PageConfig;
use crate::delegate::{BaoWebViewDelegate, BaoWebViewState};
use crate::error::BrowserError;
use crate::permission::PermissionGuard;
use crate::screenshot::{encode_image, ScreenshotFormat};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageState {
Created,
Navigating,
Interactive,
Idle,
Closing,
Closed,
}
pub struct PageInner {
pub id: usize,
pub webview: WebView,
pub servo: Rc<Servo>,
pub rendering_context: Rc<SoftwareRenderingContext>,
pub delegate: Rc<BaoWebViewDelegate>,
pub state: Rc<RefCell<PageState>>,
pub webview_state: Rc<RefCell<BaoWebViewState>>,
pub viewport: PhysicalSize<u32>,
pub stealth_profile: Option<bao_stealth::StealthProfile>,
pub permission: PermissionGuard,
pub user_content_manager: Option<Rc<servo::UserContentManager>>,
pub last_active_at: RefCell<Instant>,
pub created_at: Instant,
pub node_realm_global: RefCell<*mut mozjs::jsapi::JSObject>,
pub page_global: RefCell<*mut mozjs::jsapi::JSObject>,
}
impl PageInner {
pub fn touch(&self) {
*self.last_active_at.borrow_mut() = Instant::now();
}
pub fn webview_id_opt(&self) -> Option<servo::WebViewId> {
Some(self.webview.id())
}
pub fn navigate(&self, url: &str) -> Result<(), BrowserError> {
let parsed = url::Url::parse(url)
.map_err(|e| BrowserError::Navigation(format!("invalid URL: {e}")))?;
self.webview.load(parsed);
self.touch();
*self.state.borrow_mut() = PageState::Navigating;
Ok(())
}
pub fn drain_callbacks(&self) -> Result<String, BrowserError> {
let max_attempts = 100;
for attempt in 0..max_attempts {
match self.evaluate_js_web(";") {
Ok(result) => return Ok(result),
Err(BrowserError::JavaScript(msg)) if msg.contains("InternalError") => {
if attempt % 5 == 4 {
self.servo.spin_event_loop();
self.webview.paint();
}
continue;
}
Err(other) => return Err(other),
}
}
Err(BrowserError::Init(
"callback drain failed: pipeline not ready after timeout".into(),
))
}
pub fn evaluate_js(&self, script: &str) -> Result<String, BrowserError> {
let webview_id = self.webview.id();
if self.webview_state.borrow().dom_proxies_dirty {
let old_pg = *self.page_global.borrow();
crate::runtime_bridge::register_refresh_dom_proxies(webview_id, old_pg);
self.drain_callbacks()?;
let new_pg = crate::runtime_bridge::get_page_global(webview_id);
let new_node = crate::runtime_bridge::get_node_realm_global(webview_id);
*self.page_global.borrow_mut() = new_pg;
*self.node_realm_global.borrow_mut() = new_node;
self.webview_state.borrow_mut().dom_proxies_dirty = false;
}
let node_global = crate::runtime_bridge::get_node_realm_global(webview_id);
if node_global.is_null() {
return Err(BrowserError::JavaScript(
"Node Realm not initialized — this is a bug, eager init failed".into(),
));
}
let result = crate::runtime_bridge::evaluate_js_via_node_realm(webview_id, script);
self.drain_callbacks()?;
let eval_result = result.get().expect("evaluate result not set after drain");
match (&eval_result.value, &eval_result.error) {
(Some(val), _) => Ok(val.clone()),
(_, Some(err)) => Err(BrowserError::JavaScript(err.clone())),
(None, None) => Ok(String::new()),
}
}
pub fn evaluate_js_web(&self, script: &str) -> Result<String, BrowserError> {
let saved = Rc::new(RefCell::new(None));
let cb_saved = saved.clone();
self.webview
.evaluate_javascript(script.to_string(), move |result| {
*cb_saved.borrow_mut() = Some(result);
});
self.spin_servo(Duration::from_secs(15), || saved.borrow().is_none())?;
let result = saved
.borrow()
.clone()
.ok_or_else(|| BrowserError::JavaScript("no evaluation result".into()))?
.map_err(|e| BrowserError::JavaScript(format!("{e:?}")))?;
self.touch();
Ok(format_js_value(&result))
}
pub fn take_screenshot(&self, format: ScreenshotFormat) -> Result<Vec<u8>, BrowserError> {
self.webview.paint();
let saved = Rc::new(RefCell::new(None));
let cb_saved = saved.clone();
self.webview.take_screenshot(None, move |result| {
*cb_saved.borrow_mut() = Some(result);
});
self.spin_servo(Duration::from_secs(15), || saved.borrow().is_none())?;
let image = saved
.borrow()
.clone()
.ok_or_else(|| BrowserError::Rendering("no screenshot result".into()))?
.map_err(|e| BrowserError::Rendering(format!("{e:?}")))?;
self.touch();
encode_image(&image, format)
}
pub fn reload(&self) -> Result<(), BrowserError> {
self.webview.reload();
self.touch();
*self.state.borrow_mut() = PageState::Navigating;
Ok(())
}
pub fn go_back(&self) -> Result<(), BrowserError> {
self.webview.go_back(1);
self.touch();
*self.state.borrow_mut() = PageState::Navigating;
Ok(())
}
pub fn go_forward(&self) -> Result<(), BrowserError> {
self.webview.go_forward(1);
self.touch();
*self.state.borrow_mut() = PageState::Navigating;
Ok(())
}
pub fn can_go_back(&self) -> bool {
self.webview.can_go_back()
}
pub fn can_go_forward(&self) -> bool {
self.webview.can_go_forward()
}
pub fn set_viewport(&self, width: u32, height: u32) {
let new_size = PhysicalSize::new(width, height);
self.webview.resize(new_size);
self.touch();
}
pub fn focus(&self) {
self.webview.focus();
}
pub fn dispatch_mouse_event(
&self,
action: MouseButtonAction,
button: MouseButton,
x: f32,
y: f32,
) {
let point = WebViewPoint::Page(euclid::Point2D::<f32, CSSPixel>::new(x, y));
let event = InputEvent::MouseButton(MouseButtonEvent::new(action, button, point));
self.webview.notify_input_event(event);
self.touch();
}
pub fn dispatch_mouse_move(&self, x: f32, y: f32) {
let point = WebViewPoint::Page(euclid::Point2D::<f32, CSSPixel>::new(x, y));
let event = InputEvent::MouseMove(MouseMoveEvent::new(point));
self.webview.notify_input_event(event);
self.touch();
}
pub fn dispatch_key_event(&self, state: KeyState, key: Key, code: Code) {
let keyboard_event = KeyboardEvent::new_without_event(
state,
key,
code,
Location::Standard,
Modifiers::empty(),
false,
false,
);
let event = InputEvent::Keyboard(keyboard_event);
self.webview.notify_input_event(event);
self.touch();
}
pub fn dispatch_key_event_full(
&self,
state: KeyState,
key: Key,
code: Code,
location: Location,
modifiers: Modifiers,
repeat: bool,
) {
let keyboard_event =
KeyboardEvent::new_without_event(state, key, code, location, modifiers, repeat, false);
let event = InputEvent::Keyboard(keyboard_event);
self.webview.notify_input_event(event);
self.touch();
}
pub fn cookies(&self, urls: &[String]) -> Result<Vec<cookie::Cookie<'static>>, BrowserError> {
let sdm = self.servo.site_data_manager();
if urls.is_empty() {
let current_url = self.current_url().unwrap_or_default();
if current_url.is_empty() || current_url == "about:blank" {
return Ok(Vec::new());
}
match url::Url::parse(¤t_url) {
Ok(parsed) => Ok(sdm.cookies_for_url(parsed, CookieSource::HTTP)),
Err(_) => Ok(Vec::new()),
}
} else {
let mut seen = std::collections::HashSet::new();
let mut result = Vec::new();
for url_str in urls {
if let Ok(parsed) = url::Url::parse(url_str) {
for c in sdm.cookies_for_url(parsed, CookieSource::HTTP) {
let key = (
c.name().to_string(),
c.domain().unwrap_or("").to_string(),
c.path().unwrap_or("").to_string(),
);
if seen.insert(key) {
result.push(c);
}
}
}
}
Ok(result)
}
}
pub fn set_cookie(
&self,
url: &str,
cookie: cookie::Cookie<'static>,
) -> Result<(), BrowserError> {
let sdm = self.servo.site_data_manager();
let parsed = url::Url::parse(url)
.map_err(|e| BrowserError::Navigation(format!("invalid URL for setCookie: {e}")))?;
sdm.set_cookie_for_url(parsed, cookie, None);
self.touch();
Ok(())
}
pub fn delete_cookie(&self, name: &str, url: Option<&str>) -> Result<(), BrowserError> {
let sdm = self.servo.site_data_manager();
if let Some(url_str) = url {
let parsed = url::Url::parse(url_str).map_err(|e| {
BrowserError::Navigation(format!("invalid URL for deleteCookie: {e}"))
})?;
let current = sdm.cookies_for_url(parsed.clone(), CookieSource::HTTP);
let site = parsed.host_str().unwrap_or("");
sdm.clear_site_data(&[site], StorageType::Cookies);
for c in current {
if c.name() != name {
sdm.set_cookie_for_url(parsed.clone(), c, None);
}
}
} else {
let site_data = sdm.site_data(StorageType::Cookies);
for sd in site_data {
let site_name = sd.name();
let url_str =
if site_name.starts_with("http://") || site_name.starts_with("https://") {
site_name.clone()
} else {
format!("https://{site_name}")
};
if let Ok(parsed) = url::Url::parse(&url_str) {
let current = sdm.cookies_for_url(parsed.clone(), CookieSource::HTTP);
let has_match = current.iter().any(|c| c.name() == name);
if has_match {
sdm.clear_site_data(&[&site_name], StorageType::Cookies);
for c in current {
if c.name() != name {
sdm.set_cookie_for_url(parsed.clone(), c, None);
}
}
}
}
}
}
self.touch();
Ok(())
}
pub fn wait_for_selector(&self, selector: &str, timeout: Duration) -> Result<(), BrowserError> {
let js = format!(
"(function() {{ return document.querySelector({}) !== null; }})()",
serde_json::to_string(selector).unwrap_or_default()
);
let start = Instant::now();
while start.elapsed() < timeout {
match self.evaluate_js_web(&js) {
Ok(ref result) if result == "true" => {
self.touch();
return Ok(());
}
Ok(_) => {}
Err(BrowserError::JavaScript(ref msg)) if msg.contains("InternalError") => {
self.servo.spin_event_loop();
self.webview.paint();
continue;
}
Err(e) => return Err(e),
}
self.servo.spin_event_loop();
self.webview.paint();
std::thread::yield_now();
}
Err(BrowserError::Init(format!(
"waitForSelector timed out after {}ms for: {selector}",
timeout.as_millis()
)))
}
pub fn wait_for_function(
&self,
fn_expression: &str,
timeout: Duration,
) -> Result<(), BrowserError> {
let js = format!("(function() {{ return !!({fn_expression}); }})()");
let start = Instant::now();
while start.elapsed() < timeout {
match self.evaluate_js_web(&js) {
Ok(ref result) if result == "true" => {
self.touch();
return Ok(());
}
Ok(_) => {}
Err(BrowserError::JavaScript(ref msg)) if msg.contains("InternalError") => {
self.servo.spin_event_loop();
self.webview.paint();
continue;
}
Err(e) => return Err(e),
}
self.servo.spin_event_loop();
self.webview.paint();
std::thread::yield_now();
}
Err(BrowserError::Init(format!(
"waitForFunction timed out after {}ms",
timeout.as_millis()
)))
}
pub fn wait_for_navigation(&self, timeout: Duration) -> Result<(), BrowserError> {
let start = Instant::now();
let initial_status = self.webview_state.borrow().load_status;
let mut saw_new_navigation = initial_status != servo::LoadStatus::Started;
while start.elapsed() < timeout {
let current_status = self.webview_state.borrow().load_status;
if current_status == servo::LoadStatus::Started {
saw_new_navigation = true;
}
if saw_new_navigation && current_status == servo::LoadStatus::Complete {
self.touch();
return Ok(());
}
self.servo.spin_event_loop();
self.webview.paint();
std::thread::yield_now();
}
Err(BrowserError::Init(format!(
"waitForNavigation timed out after {}ms",
timeout.as_millis()
)))
}
pub fn click_element(&self, selector: &str) -> Result<(), BrowserError> {
let js = format!(
"(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}}); }})()",
serde_json::to_string(selector).unwrap_or_default()
);
let pos_str = self.evaluate_js_web(&js)?;
if pos_str == "null" || pos_str.is_empty() {
return Err(BrowserError::JavaScript(format!(
"element not found for click: {selector}"
)));
}
let pos: serde_json::Value = serde_json::from_str(&pos_str)
.map_err(|e| BrowserError::JavaScript(format!("invalid position JSON: {e}")))?;
let x = pos["x"].as_f64().unwrap_or(0.0) as f32;
let y = pos["y"].as_f64().unwrap_or(0.0) as f32;
self.dispatch_mouse_event(MouseButtonAction::Down, MouseButton::Left, x, y);
self.servo.spin_event_loop();
self.webview.paint();
self.dispatch_mouse_event(MouseButtonAction::Up, MouseButton::Left, x, y);
Ok(())
}
pub fn type_text(&self, text: &str) -> Result<(), BrowserError> {
for ch in text.chars() {
let key = match ch {
'\n' => Key::Named(NamedKey::Enter),
'\t' => Key::Named(NamedKey::Tab),
'\u{8}' => Key::Named(NamedKey::Backspace),
'\u{7f}' => Key::Named(NamedKey::Delete),
' ' => Key::Character(" ".into()),
c => Key::Character(c.to_string()),
};
let code = key_code_for_char(ch);
self.dispatch_key_event_full(
KeyState::Down,
key.clone(),
code.clone(),
Location::Standard,
Modifiers::empty(),
false,
);
self.servo.spin_event_loop();
self.webview.paint();
self.dispatch_key_event_full(
KeyState::Up,
key,
code,
Location::Standard,
Modifiers::empty(),
false,
);
}
Ok(())
}
pub fn fill(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
let js = format!(
"(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; }})()",
serde_json::to_string(selector).unwrap_or_default(),
serde_json::to_string(value).unwrap_or_default(),
);
let result = self.evaluate_js_web(&js)?;
if result == "false" {
return Err(BrowserError::JavaScript(format!(
"element not found for fill: {selector}"
)));
}
Ok(())
}
pub fn set_content(&self, html: &str) -> Result<(), BrowserError> {
let js = format!(
"(function() {{ document.open(); document.write({}); document.close(); }})()",
serde_json::to_string(html).unwrap_or_default(),
);
self.evaluate_js_web(&js)?;
Ok(())
}
pub fn content(&self) -> Result<String, BrowserError> {
self.evaluate_js_web("document.documentElement.outerHTML")
}
pub fn select(&self, selector: &str, values: &[&str]) -> Result<(), BrowserError> {
let values_json = serde_json::to_string(&values).unwrap_or_default();
let js = format!(
"(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; }})()",
serde_json::to_string(selector).unwrap_or_default(),
);
let result = self.evaluate_js_web(&js)?;
if result == "false" {
return Err(BrowserError::JavaScript(format!(
"element not found for select: {selector}"
)));
}
Ok(())
}
pub fn press(&self, key: &str) -> Result<(), BrowserError> {
let (key_val, code_val) = parse_key_name(key);
self.dispatch_key_event_full(
KeyState::Down,
key_val.clone(),
code_val.clone(),
Location::Standard,
Modifiers::empty(),
false,
);
self.servo.spin_event_loop();
self.webview.paint();
self.dispatch_key_event_full(
KeyState::Up,
key_val,
code_val,
Location::Standard,
Modifiers::empty(),
false,
);
Ok(())
}
pub fn hover(&self, selector: &str) -> Result<(), BrowserError> {
let js = format!(
"(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}}); }})()",
serde_json::to_string(selector).unwrap_or_default()
);
let pos_str = self.evaluate_js_web(&js)?;
if pos_str == "null" || pos_str.is_empty() {
return Err(BrowserError::JavaScript(format!(
"element not found for hover: {selector}"
)));
}
let pos: serde_json::Value = serde_json::from_str(&pos_str)
.map_err(|e| BrowserError::JavaScript(format!("invalid position JSON: {e}")))?;
let x = pos["x"].as_f64().unwrap_or(0.0) as f32;
let y = pos["y"].as_f64().unwrap_or(0.0) as f32;
self.dispatch_mouse_move(x, y);
Ok(())
}
pub fn focus_element(&self, selector: &str) -> Result<(), BrowserError> {
let js = format!(
"(function() {{ var e = document.querySelector({}); if (!e) return false; e.focus(); return true; }})()",
serde_json::to_string(selector).unwrap_or_default()
);
let result = self.evaluate_js_web(&js)?;
if result == "false" {
return Err(BrowserError::JavaScript(format!(
"element not found for focus: {selector}"
)));
}
Ok(())
}
pub fn take_screenshot_advanced(
&self,
format: ScreenshotFormat,
clip: Option<(f64, f64, f64, f64)>,
full_page: bool,
) -> Result<Vec<u8>, BrowserError> {
let original_viewport = self.viewport;
if full_page {
let height_js = "document.documentElement.scrollHeight";
let height_str = self.evaluate_js_web(height_js).unwrap_or_default();
let full_height: u32 = height_str
.trim()
.parse()
.unwrap_or(original_viewport.height);
let capped_height = full_height.max(original_viewport.height);
if capped_height != original_viewport.height {
self.set_viewport(original_viewport.width, capped_height);
self.servo.spin_event_loop();
self.webview.paint();
}
}
let result = self.take_screenshot(format);
if full_page && original_viewport != self.viewport {
self.set_viewport(original_viewport.width, original_viewport.height);
}
let image_bytes = result?;
if let Some((x, y, w, h)) = clip {
let mut img = image::load_from_memory(&image_bytes).map_err(|e| {
BrowserError::Rendering(format!("failed to decode screenshot for clip: {e}"))
})?;
let crop_x = x.max(0.0) as u32;
let crop_y = y.max(0.0) as u32;
let crop_w = (w as u32).min(img.width().saturating_sub(crop_x));
let crop_h = (h as u32).min(img.height().saturating_sub(crop_y));
if crop_w == 0 || crop_h == 0 {
return Err(BrowserError::Rendering(
"clip region has zero dimensions".into(),
));
}
let cropped = img.crop(crop_x, crop_y, crop_w, crop_h);
let rgba = cropped.to_rgba8();
let fmt = if image_bytes.starts_with(&[0x89, 0x50, 0x4E, 0x47]) {
ScreenshotFormat::Png
} else {
ScreenshotFormat::Jpeg
};
return encode_image(&rgba, fmt);
}
Ok(image_bytes)
}
pub fn page_title(&self) -> Option<String> {
self.webview_state.borrow().title.clone()
}
pub fn current_url(&self) -> Option<String> {
self.webview_state
.borrow()
.url
.as_ref()
.map(|u| u.to_string())
}
pub fn get_state(&self) -> PageState {
*self.state.borrow()
}
fn spin_servo(
&self,
timeout: Duration,
callback: impl Fn() -> bool,
) -> Result<(), BrowserError> {
let start = Instant::now();
while callback() {
self.servo.spin_event_loop();
self.webview.paint();
if start.elapsed() > timeout {
return Err(BrowserError::Init("operation timed out".into()));
}
std::thread::yield_now();
}
Ok(())
}
}
#[derive(Clone)]
pub struct PageHandle {
inner: Rc<RefCell<Option<PageInner>>>,
id: usize,
servo: Rc<Servo>,
delegate: Rc<crate::delegate::BaoServoDelegate>,
}
impl PageHandle {
pub(crate) fn new(
servo: Rc<Servo>,
servo_delegate: Rc<crate::delegate::BaoServoDelegate>,
config: &PageConfig,
default_viewport: PhysicalSize<u32>,
id: usize,
) -> Result<Self, BrowserError> {
let viewport = PhysicalSize::new(
config.viewport_width.unwrap_or(default_viewport.width),
config.viewport_height.unwrap_or(default_viewport.height),
);
let rendering_context = Rc::new(
SoftwareRenderingContext::new(viewport)
.map_err(|e| BrowserError::Init(format!("rendering context failed: {e:?}")))?,
);
let webview_state = Rc::new(RefCell::new(BaoWebViewState::default()));
if let Some(tx) = servo_delegate.console_log_tx() {
webview_state.borrow_mut().console_log_tx = Some(tx);
}
if let Some(ref profile) = config.stealth_profile {
webview_state.borrow_mut().set_worker_scope_config(
crate::delegate::WorkerScopeConfig::from(profile as &bao_stealth::StealthProfile),
);
}
let webview_delegate =
Rc::new(BaoWebViewDelegate::new(Rc::clone(&webview_state), viewport));
let state = Rc::new(RefCell::new(PageState::Created));
let mut builder = WebViewBuilder::new(
&servo,
rendering_context.clone() as Rc<dyn RenderingContext>,
)
.delegate(Rc::clone(&webview_delegate) as Rc<dyn servo::WebViewDelegate>);
let user_content_manager = Rc::new(servo::UserContentManager::new(&servo));
builder = builder.user_content_manager(Rc::clone(&user_content_manager));
if let Some(ref url_str) = config.url {
let url = url::Url::parse(url_str)
.map_err(|e| BrowserError::Init(format!("invalid URL: {e}")))?;
builder = builder.url(url);
}
let webview = builder.build();
let inner = PageInner {
id,
webview,
servo: Rc::clone(&servo),
rendering_context,
delegate: webview_delegate,
state,
webview_state,
viewport,
stealth_profile: config.stealth_profile.clone(),
permission: match &config.permission {
Some(perm) => PermissionGuard::new(perm.clone()),
None => PermissionGuard::none(),
},
user_content_manager: Some(user_content_manager),
last_active_at: RefCell::new(Instant::now()),
created_at: Instant::now(),
node_realm_global: RefCell::new(std::ptr::null_mut()),
page_global: RefCell::new(std::ptr::null_mut()),
};
Ok(PageHandle {
inner: Rc::new(RefCell::new(Some(inner))),
id,
servo,
delegate: servo_delegate,
})
}
pub fn id(&self) -> usize {
self.id
}
pub fn navigate(&self, url: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.navigate(url))
}
pub fn wait_for_pipeline_ready(&self, timeout: Duration) -> Result<(), BrowserError> {
let start = Instant::now();
while start.elapsed() < timeout {
let ready = self
.with_inner_opt(|inner| Some(inner.webview_state.borrow().frame_ready))
.unwrap_or(false);
if ready {
return self.drain_callbacks().map(|_| ());
}
self.with_inner(|inner| {
inner.servo.spin_event_loop();
Ok(())
})?;
std::thread::yield_now();
}
Err(BrowserError::Init(
"pipeline not ready after timeout".into(),
))
}
pub fn drain_callbacks(&self) -> Result<String, BrowserError> {
self.with_inner(|inner| inner.drain_callbacks())
}
pub fn evaluate_js(&self, script: &str) -> Result<String, BrowserError> {
self.with_inner(|inner| inner.evaluate_js(script))
}
pub fn evaluate_js_web(&self, script: &str) -> Result<String, BrowserError> {
self.with_inner(|inner| inner.evaluate_js_web(script))
}
pub fn add_script_to_evaluate_on_new_document(&self, source: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| {
let ucm = inner
.user_content_manager
.clone()
.ok_or_else(|| BrowserError::Init("page has no UserContentManager".into()))?;
ucm.add_script(Rc::new(servo::UserScript::new(source.to_string(), None)));
Ok(())
})
}
pub fn take_screenshot(&self, format: ScreenshotFormat) -> Result<Vec<u8>, BrowserError> {
self.with_inner(|inner| inner.take_screenshot(format))
}
pub fn page_title(&self) -> Option<String> {
self.with_inner_opt(|inner| inner.page_title())
}
pub fn current_url(&self) -> Option<String> {
self.with_inner_opt(|inner| inner.current_url())
}
pub fn servo(&self) -> &Rc<Servo> {
&self.servo
}
pub fn get_state(&self) -> PageState {
self.inner
.borrow()
.as_ref()
.map_or(PageState::Closed, |inner| inner.get_state())
}
pub fn is_alive(&self) -> bool {
self.inner.borrow().is_some()
}
pub(crate) fn webview_id(&self) -> Option<servo::WebViewId> {
self.inner.borrow().as_ref().map(|inner| inner.webview.id())
}
pub fn permission(&self) -> PermissionGuard {
let borrow = self.inner.borrow();
match borrow.as_ref() {
Some(inner) => inner.permission.clone(),
None => PermissionGuard::none(),
}
}
pub fn stealth_profile(&self) -> Option<bao_stealth::StealthProfile> {
self.inner
.borrow()
.as_ref()
.and_then(|inner| inner.stealth_profile.clone())
}
pub fn webview_state(&self) -> Rc<RefCell<BaoWebViewState>> {
self.inner
.borrow()
.as_ref()
.map(|inner| inner.webview_state.clone())
.unwrap_or_else(|| Rc::new(RefCell::new(BaoWebViewState::default())))
}
pub fn wait_for_selector(&self, selector: &str, timeout: Duration) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.wait_for_selector(selector, timeout))
}
pub fn wait_for_function(
&self,
fn_expression: &str,
timeout: Duration,
) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.wait_for_function(fn_expression, timeout))
}
pub fn wait_for_navigation(&self, timeout: Duration) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.wait_for_navigation(timeout))
}
pub fn click(&self, selector: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.click_element(selector))
}
pub fn type_text(&self, text: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.type_text(text))
}
pub fn fill(&self, selector: &str, value: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.fill(selector, value))
}
pub fn set_content(&self, html: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.set_content(html))
}
pub fn content(&self) -> Result<String, BrowserError> {
self.with_inner(|inner| inner.content())
}
pub fn set_viewport(&self, width: u32, height: u32) -> Result<(), BrowserError> {
self.with_inner(|inner| {
inner.set_viewport(width, height);
Ok(())
})
}
pub fn cookies(&self, urls: &[String]) -> Result<Vec<cookie::Cookie<'static>>, BrowserError> {
self.with_inner(|inner| inner.cookies(urls))
}
pub fn set_cookie(
&self,
url: &str,
cookie: cookie::Cookie<'static>,
) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.set_cookie(url, cookie))
}
pub fn delete_cookie(&self, name: &str, url: Option<&str>) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.delete_cookie(name, url))
}
pub fn select(&self, selector: &str, values: &[&str]) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.select(selector, values))
}
pub fn press(&self, key: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.press(key))
}
pub fn hover(&self, selector: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.hover(selector))
}
pub fn focus_element(&self, selector: &str) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.focus_element(selector))
}
pub fn reload(&self) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.reload())
}
pub fn go_back(&self) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.go_back())
}
pub fn go_forward(&self) -> Result<(), BrowserError> {
self.with_inner(|inner| inner.go_forward())
}
pub fn can_go_back(&self) -> bool {
self.with_inner_opt(|inner| Some(inner.can_go_back()))
.unwrap_or(false)
}
pub fn can_go_forward(&self) -> bool {
self.with_inner_opt(|inner| Some(inner.can_go_forward()))
.unwrap_or(false)
}
pub fn take_screenshot_advanced(
&self,
format: ScreenshotFormat,
clip: Option<(f64, f64, f64, f64)>,
full_page: bool,
) -> Result<Vec<u8>, BrowserError> {
self.with_inner(|inner| inner.take_screenshot_advanced(format, clip, full_page))
}
pub fn dispatch_mouse_event(
&self,
action: MouseButtonAction,
button: MouseButton,
x: f32,
y: f32,
) -> Result<(), BrowserError> {
self.with_inner(|inner| {
inner.dispatch_mouse_event(action, button, x, y);
Ok(())
})
}
pub fn dispatch_mouse_move(&self, x: f32, y: f32) -> Result<(), BrowserError> {
self.with_inner(|inner| {
inner.dispatch_mouse_move(x, y);
Ok(())
})
}
pub fn dispatch_key_event(
&self,
state: KeyState,
key: Key,
code: Code,
) -> Result<(), BrowserError> {
self.with_inner(|inner| {
inner.dispatch_key_event(state, key, code);
Ok(())
})
}
pub fn close(&self) -> Result<(), BrowserError> {
let mut borrow = self.inner.borrow_mut();
if let Some(inner) = borrow.take() {
*inner.state.borrow_mut() = PageState::Closing;
{
let mut ws = inner.webview_state.borrow_mut();
if ws.active_worker_count() > 0 {
log::debug!(
"[page] close: terminating {} active workers",
ws.active_worker_count()
);
ws.terminate_all_workers();
}
}
let pg = *inner.page_global.borrow();
let ng = *inner.node_realm_global.borrow();
if let Some(wid) = inner.webview_id_opt() {
crate::runtime_bridge::remove_node_realm_by_id(wid);
}
if !pg.is_null() {
bao_stealth::engine_props::remove_profile_for_global(pg as usize);
}
if !ng.is_null() {
bao_stealth::engine_props::remove_profile_for_global(ng as usize);
}
*inner.state.borrow_mut() = PageState::Closed;
drop(inner);
}
Ok(())
}
fn with_inner<F, R>(&self, f: F) -> Result<R, BrowserError>
where
F: FnOnce(&PageInner) -> Result<R, BrowserError>,
{
let borrow = self.inner.borrow();
match borrow.as_ref() {
Some(inner) => f(inner),
None => Err(BrowserError::Init("page is closed".into())),
}
}
pub fn set_page_global(
&self,
page_global: *mut mozjs::jsapi::JSObject,
node_global: *mut mozjs::jsapi::JSObject,
) {
let borrow = self.inner.borrow();
if let Some(inner) = borrow.as_ref() {
*inner.page_global.borrow_mut() = page_global;
*inner.node_realm_global.borrow_mut() = node_global;
}
}
pub fn has_node_realm(&self) -> (bool, bool) {
let borrow = self.inner.borrow();
if let Some(inner) = borrow.as_ref() {
let pg = *inner.page_global.borrow();
let ng = *inner.node_realm_global.borrow();
return (!pg.is_null(), !ng.is_null());
}
(false, false)
}
fn with_inner_opt<F, R>(&self, f: F) -> Option<R>
where
F: FnOnce(&PageInner) -> Option<R>,
{
let borrow = self.inner.borrow();
borrow.as_ref().and_then(f)
}
}
fn format_js_value(v: &servo::JSValue) -> String {
match v {
servo::JSValue::String(s) => s.clone(),
servo::JSValue::Number(n) => n.to_string(),
servo::JSValue::Boolean(b) => b.to_string(),
servo::JSValue::Null => "null".into(),
servo::JSValue::Undefined => "undefined".into(),
servo::JSValue::Element(id) => format!("[Element: {id}]"),
servo::JSValue::ShadowRoot(id) => format!("[ShadowRoot: {id}]"),
servo::JSValue::Frame(id) => format!("[Frame: {id}]"),
servo::JSValue::Window(id) => format!("[Window: {id}]"),
servo::JSValue::Array(items) => {
let formatted: Vec<String> = items.iter().map(format_js_value).collect();
format!("[{}]", formatted.join(", "))
}
servo::JSValue::Object(map) => {
let formatted: Vec<String> = map
.iter()
.map(|(k, val)| format!("{}: {}", k, format_js_value(val)))
.collect();
format!("{{{}}}", formatted.join(", "))
}
}
}
fn key_code_for_char(ch: char) -> Code {
match ch {
'a' => Code::KeyA,
'b' => Code::KeyB,
'c' => Code::KeyC,
'd' => Code::KeyD,
'e' => Code::KeyE,
'f' => Code::KeyF,
'g' => Code::KeyG,
'h' => Code::KeyH,
'i' => Code::KeyI,
'j' => Code::KeyJ,
'k' => Code::KeyK,
'l' => Code::KeyL,
'm' => Code::KeyM,
'n' => Code::KeyN,
'o' => Code::KeyO,
'p' => Code::KeyP,
'q' => Code::KeyQ,
'r' => Code::KeyR,
's' => Code::KeyS,
't' => Code::KeyT,
'u' => Code::KeyU,
'v' => Code::KeyV,
'w' => Code::KeyW,
'x' => Code::KeyX,
'y' => Code::KeyY,
'z' => Code::KeyZ,
'A' => Code::KeyA,
'B' => Code::KeyB,
'C' => Code::KeyC,
'D' => Code::KeyD,
'E' => Code::KeyE,
'F' => Code::KeyF,
'G' => Code::KeyG,
'H' => Code::KeyH,
'I' => Code::KeyI,
'J' => Code::KeyJ,
'K' => Code::KeyK,
'L' => Code::KeyL,
'M' => Code::KeyM,
'N' => Code::KeyN,
'O' => Code::KeyO,
'P' => Code::KeyP,
'Q' => Code::KeyQ,
'R' => Code::KeyR,
'S' => Code::KeyS,
'T' => Code::KeyT,
'U' => Code::KeyU,
'V' => Code::KeyV,
'W' => Code::KeyW,
'X' => Code::KeyX,
'Y' => Code::KeyY,
'Z' => Code::KeyZ,
'0' => Code::Digit0,
'1' => Code::Digit1,
'2' => Code::Digit2,
'3' => Code::Digit3,
'4' => Code::Digit4,
'5' => Code::Digit5,
'6' => Code::Digit6,
'7' => Code::Digit7,
'8' => Code::Digit8,
'9' => Code::Digit9,
'\n' => Code::Enter,
'\t' => Code::Tab,
'\u{8}' => Code::Backspace,
'\u{7f}' => Code::Delete,
' ' => Code::Space,
';' => Code::Semicolon,
'=' => Code::Equal,
',' => Code::Comma,
'-' => Code::Minus,
'.' => Code::Period,
'/' => Code::Slash,
'`' => Code::Backquote,
'[' => Code::BracketLeft,
'\\' => Code::Backslash,
']' => Code::BracketRight,
'\'' => Code::Quote,
_ => Code::Unidentified,
}
}
fn parse_key_name(name: &str) -> (Key, Code) {
match name {
"Enter" => (Key::Named(NamedKey::Enter), Code::Enter),
"Tab" => (Key::Named(NamedKey::Tab), Code::Tab),
"Escape" | "Esc" => (Key::Named(NamedKey::Escape), Code::Escape),
"Backspace" => (Key::Named(NamedKey::Backspace), Code::Backspace),
"Delete" => (Key::Named(NamedKey::Delete), Code::Delete),
"Space" => (Key::Character(" ".into()), Code::Space),
"ArrowUp" => (Key::Named(NamedKey::ArrowUp), Code::ArrowUp),
"ArrowDown" => (Key::Named(NamedKey::ArrowDown), Code::ArrowDown),
"ArrowLeft" => (Key::Named(NamedKey::ArrowLeft), Code::ArrowLeft),
"ArrowRight" => (Key::Named(NamedKey::ArrowRight), Code::ArrowRight),
"Home" => (Key::Named(NamedKey::Home), Code::Home),
"End" => (Key::Named(NamedKey::End), Code::End),
"PageUp" => (Key::Named(NamedKey::PageUp), Code::PageUp),
"PageDown" => (Key::Named(NamedKey::PageDown), Code::PageDown),
"Insert" => (Key::Named(NamedKey::Insert), Code::Insert),
"F1" => (Key::Named(NamedKey::F1), Code::F1),
"F2" => (Key::Named(NamedKey::F2), Code::F2),
"F3" => (Key::Named(NamedKey::F3), Code::F3),
"F4" => (Key::Named(NamedKey::F4), Code::F4),
"F5" => (Key::Named(NamedKey::F5), Code::F5),
"F6" => (Key::Named(NamedKey::F6), Code::F6),
"F7" => (Key::Named(NamedKey::F7), Code::F7),
"F8" => (Key::Named(NamedKey::F8), Code::F8),
"F9" => (Key::Named(NamedKey::F9), Code::F9),
"F10" => (Key::Named(NamedKey::F10), Code::F10),
"F11" => (Key::Named(NamedKey::F11), Code::F11),
"F12" => (Key::Named(NamedKey::F12), Code::F12),
"ControlLeft" | "Control" => (Key::Named(NamedKey::Control), Code::ControlLeft),
"ControlRight" => (Key::Named(NamedKey::Control), Code::ControlRight),
"ShiftLeft" | "Shift" => (Key::Named(NamedKey::Shift), Code::ShiftLeft),
"ShiftRight" => (Key::Named(NamedKey::Shift), Code::ShiftRight),
"AltLeft" | "Alt" => (Key::Named(NamedKey::Alt), Code::AltLeft),
"AltRight" => (Key::Named(NamedKey::Alt), Code::AltRight),
"MetaLeft" | "Meta" => (Key::Named(NamedKey::Meta), Code::MetaLeft),
"MetaRight" => (Key::Named(NamedKey::Meta), Code::MetaRight),
"CapsLock" => (Key::Named(NamedKey::CapsLock), Code::CapsLock),
"NumLock" => (Key::Named(NamedKey::NumLock), Code::NumLock),
"ScrollLock" => (Key::Named(NamedKey::ScrollLock), Code::ScrollLock),
s if s.chars().count() == 1 => {
let ch = s.chars().next().unwrap();
let key = Key::Character(ch.to_string());
let code = key_code_for_char(ch);
(key, code)
}
s => (Key::Character(s.to_string()), Code::Unidentified),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn page_state_variants_equal_to_themselves() {
assert_eq!(PageState::Created, PageState::Created);
assert_eq!(PageState::Navigating, PageState::Navigating);
assert_eq!(PageState::Interactive, PageState::Interactive);
assert_eq!(PageState::Idle, PageState::Idle);
assert_eq!(PageState::Closing, PageState::Closing);
assert_eq!(PageState::Closed, PageState::Closed);
}
#[test]
fn page_state_clone_works() {
let state = PageState::Navigating;
let cloned = state.clone();
assert_eq!(state, cloned);
}
#[test]
fn page_state_copy_works() {
let state = PageState::Interactive;
let copied: PageState = state;
assert_eq!(state, copied);
}
#[test]
fn page_state_debug_format_includes_variant_name() {
assert!(format!("{:?}", PageState::Created).contains("Created"));
assert!(format!("{:?}", PageState::Navigating).contains("Navigating"));
assert!(format!("{:?}", PageState::Interactive).contains("Interactive"));
assert!(format!("{:?}", PageState::Idle).contains("Idle"));
assert!(format!("{:?}", PageState::Closing).contains("Closing"));
assert!(format!("{:?}", PageState::Closed).contains("Closed"));
}
#[test]
fn page_state_closing_distinct_from_neighbors() {
assert_ne!(PageState::Closing, PageState::Idle);
assert_ne!(PageState::Closing, PageState::Interactive);
assert_ne!(PageState::Closing, PageState::Closed);
}
#[test]
fn page_state_created_not_equal_closed() {
assert_ne!(PageState::Created, PageState::Closed);
}
#[test]
fn format_js_value_string() {
let value = servo::JSValue::String("hello".into());
assert_eq!(format_js_value(&value), "hello");
}
#[test]
fn format_js_value_number() {
let value = servo::JSValue::Number(42.5);
assert_eq!(format_js_value(&value), "42.5");
}
#[test]
fn format_js_value_boolean_true() {
let value = servo::JSValue::Boolean(true);
assert_eq!(format_js_value(&value), "true");
}
#[test]
fn format_js_value_null() {
let value = servo::JSValue::Null;
assert_eq!(format_js_value(&value), "null");
}
#[test]
fn format_js_value_undefined() {
let value = servo::JSValue::Undefined;
assert_eq!(format_js_value(&value), "undefined");
}
#[test]
fn format_js_value_array() {
let value = servo::JSValue::Array(vec![
servo::JSValue::Number(1.0),
servo::JSValue::Number(2.0),
servo::JSValue::Number(3.0),
]);
assert_eq!(format_js_value(&value), "[1, 2, 3]");
}
#[test]
fn format_js_value_object() {
let mut map = HashMap::new();
map.insert("name".into(), servo::JSValue::String("test".into()));
map.insert("count".into(), servo::JSValue::Number(5.0));
let value = servo::JSValue::Object(map);
let result = format_js_value(&value);
assert!(result.starts_with('{') && result.ends_with('}'));
assert!(result.contains("name: test"));
assert!(result.contains("count: 5"));
}
#[test]
fn format_js_value_element() {
let value = servo::JSValue::Element("div#main".into());
assert_eq!(format_js_value(&value), "[Element: div#main]");
}
#[test]
fn format_js_value_shadow_root() {
let value = servo::JSValue::ShadowRoot("host-element".into());
assert_eq!(format_js_value(&value), "[ShadowRoot: host-element]");
}
#[test]
fn format_js_value_frame() {
let value = servo::JSValue::Frame("iframe-123".into());
assert_eq!(format_js_value(&value), "[Frame: iframe-123]");
}
#[test]
fn format_js_value_window() {
let value = servo::JSValue::Window("window-456".into());
assert_eq!(format_js_value(&value), "[Window: window-456]");
}
#[test]
fn evaluate_js_uses_node_realm_or_iife_fallback() {
let source = include_str!("page.rs");
let func_start = source
.find("pub fn evaluate_js(&self, script: &str)")
.expect("evaluate_js function not found");
let func_body = &source[func_start..func_start + 2800.min(source.len() - func_start)];
assert!(
func_body.contains("get_node_realm_global"),
"REQ-SEC-002 REGRESSION: evaluate_js must check Node Realm global"
);
assert!(
func_body.contains("evaluate_js_via_node_realm"),
"REQ-SEC-002 REGRESSION: evaluate_js must use Node Realm execution"
);
assert!(
func_body.contains("eager init failed"),
"REQ-SEC-002 REGRESSION: evaluate_js must detect uninitialized Node Realm"
);
}
#[test]
fn evaluate_js_drains_callbacks_for_result() {
let source = include_str!("page.rs");
let func_start = source
.find("pub fn evaluate_js(&self, script: &str)")
.expect("evaluate_js function not found");
let func_body = &source[func_start..func_start + 2800.min(source.len() - func_start)];
assert!(
func_body.contains("drain_callbacks"),
"REQ-SEC-002 REGRESSION: evaluate_js must drain callbacks after Node Realm execution"
);
}
#[test]
fn evaluate_js_reads_evaluate_result() {
let source = include_str!("page.rs");
let func_start = source
.find("pub fn evaluate_js(&self, script: &str)")
.expect("evaluate_js function not found");
let func_body = &source[func_start..func_start + 2800.min(source.len() - func_start)];
assert!(
func_body.contains("eval_result"),
"REQ-SEC-002 REGRESSION: evaluate_js must read EvaluateResult"
);
}
#[test]
fn page_global_has_no_node_apis() {
let source = include_str!("runtime_bridge.rs");
let func_start = source
.find("unsafe fn install_all_native")
.expect("install_all_native function not found");
let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
assert!(
func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
"REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
);
assert!(
func_body.contains("bun_runtime::timers::install_timer_globals"),
"REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
);
assert!(
!func_body.contains("globals::install_all("),
"REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
);
assert!(
!func_body.contains("globals::install_node_apis("),
"REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis() on page global"
);
}
#[test]
fn node_realm_has_node_apis() {
let source = include_str!("runtime_bridge.rs");
let func_start = source
.find("unsafe fn create_node_realm_native")
.expect("create_node_realm_native function not found");
let func_end = source[func_start..]
.find("pub fn inject_node_apis")
.or_else(|| source[func_start..].find("/// Inject Node.js APIs as native"))
.expect("end boundary not found");
let func_body = &source[func_start..func_start + func_end];
assert!(
func_body.contains("bun_runtime::globals::install_node_apis"),
"REQ-SEC-002 REGRESSION: create_node_realm_native must install Node APIs on Node Realm global"
);
assert!(
func_body.contains("bun_runtime::globals::install_web_apis"),
"REQ-SEC-002: Node Realm must also have Web APIs for trusted scripts"
);
}
#[test]
fn node_realm_uses_new_compartment() {
let source = include_str!("runtime_bridge.rs");
let func_start = source
.find("unsafe fn create_node_realm_native")
.expect("create_node_realm_native function not found");
let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
assert!(
func_body.contains("NewCompartmentAndZone"),
"REQ-SEC-002 REGRESSION: Node Realm must use NewCompartmentAndZone"
);
assert!(
func_body.contains("SIMPLE_GLOBAL_CLASS"),
"REQ-SEC-002 REGRESSION: Node Realm must use SIMPLE_GLOBAL_CLASS"
);
}
#[test]
fn evaluate_in_node_realm_uses_auto_realm() {
let source = include_str!("runtime_bridge.rs");
let func_start = source
.find("pub unsafe fn evaluate_in_node_realm")
.expect("evaluate_in_node_realm function not found");
let func_body_start = source[func_start..]
.find("{")
.expect("function body start not found");
let search_limit = source[func_start + func_body_start..]
.find("unsafe fn create_node_realm_native")
.unwrap_or(3000)
.min(3000);
let func_body =
&source[func_start + func_body_start..func_start + func_body_start + search_limit];
assert!(
func_body.contains("AutoRealm::new"),
"REQ-SEC-002 REGRESSION: evaluate_in_node_realm must use AutoRealm"
);
}
#[test]
fn node_realm_global_stored_per_page() {
let source = include_str!("runtime_bridge.rs");
assert!(
source.contains("NODE_REALM_BY_WEBVIEW"),
"REQ-SEC-002 REGRESSION: must have NODE_REALM_BY_WEBVIEW per-page storage"
);
assert!(
source.contains("store_node_realm"),
"REQ-SEC-002 REGRESSION: must have store_node_realm accessor"
);
assert!(
source.contains("get_node_realm_by_id"),
"REQ-SEC-002 REGRESSION: must have get_node_realm_by_id accessor"
);
assert!(
source.contains("get_node_realm_global"),
"REQ-SEC-002 REGRESSION: must have get_node_realm_global accessor"
);
}
#[test]
fn page_inner_has_node_realm_global_field() {
let source = include_str!("page.rs");
assert!(
source.contains("node_realm_global: RefCell<*mut mozjs::jsapi::JSObject>"),
"REQ-SEC-002 REGRESSION: PageInner must have node_realm_global field"
);
}
#[test]
fn page_inner_has_drain_callbacks_method() {
let source = include_str!("page.rs");
assert!(
source.contains("fn drain_callbacks(&self)"),
"REQ-SEC-002 REGRESSION: PageInner must have drain_callbacks method"
);
assert!(
source.contains("InternalError"),
"REQ-SEC-002 REGRESSION: drain_callbacks must handle InternalError retry"
);
}
#[test]
fn key_code_for_char_letters() {
assert_eq!(super::key_code_for_char('a'), Code::KeyA);
assert_eq!(super::key_code_for_char('Z'), Code::KeyZ);
}
#[test]
fn key_code_for_char_digits() {
assert_eq!(super::key_code_for_char('0'), Code::Digit0);
assert_eq!(super::key_code_for_char('9'), Code::Digit9);
}
#[test]
fn key_code_for_char_special() {
assert_eq!(super::key_code_for_char('\n'), Code::Enter);
assert_eq!(super::key_code_for_char('\t'), Code::Tab);
assert_eq!(super::key_code_for_char(' '), Code::Space);
}
#[test]
fn parse_key_name_enter() {
let (key, code) = super::parse_key_name("Enter");
assert!(matches!(key, Key::Named(NamedKey::Enter)));
assert_eq!(code, Code::Enter);
}
#[test]
fn parse_key_name_arrow_keys() {
let (key, code) = super::parse_key_name("ArrowDown");
assert!(matches!(key, Key::Named(NamedKey::ArrowDown)));
assert_eq!(code, Code::ArrowDown);
let (key, code) = super::parse_key_name("ArrowUp");
assert!(matches!(key, Key::Named(NamedKey::ArrowUp)));
assert_eq!(code, Code::ArrowUp);
}
#[test]
fn parse_key_name_single_char() {
let (key, code) = super::parse_key_name("a");
assert!(matches!(key, Key::Character(s) if s == "a"));
assert_eq!(code, Code::KeyA);
}
#[test]
fn parse_key_name_function_keys() {
let (key, code) = super::parse_key_name("F1");
assert!(matches!(key, Key::Named(NamedKey::F1)));
assert_eq!(code, Code::F1);
}
#[test]
fn parse_key_name_escape_aliases() {
let (key, code) = super::parse_key_name("Escape");
assert!(matches!(key, Key::Named(NamedKey::Escape)));
assert_eq!(code, Code::Escape);
let (key, code) = super::parse_key_name("Esc");
assert!(matches!(key, Key::Named(NamedKey::Escape)));
assert_eq!(code, Code::Escape);
}
#[test]
fn page_inner_has_wait_for_selector() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn wait_for_selector("),
"REQ-LIB-001: PageInner must have wait_for_selector method"
);
}
#[test]
fn page_inner_has_wait_for_navigation() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn wait_for_navigation("),
"REQ-LIB-001: PageInner must have wait_for_navigation method"
);
}
#[test]
fn page_inner_has_wait_for_function() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn wait_for_function("),
"REQ-LIB-001: PageInner must have wait_for_function method"
);
}
#[test]
fn page_inner_has_click_element() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn click_element("),
"REQ-LIB-001: PageInner must have click_element method"
);
}
#[test]
fn page_inner_has_type_text() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn type_text("),
"REQ-LIB-001: PageInner must have type_text method"
);
}
#[test]
fn page_inner_has_fill() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn fill("),
"REQ-LIB-001: PageInner must have fill method"
);
}
#[test]
fn page_inner_has_set_content() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn set_content("),
"REQ-LIB-001: PageInner must have set_content method"
);
}
#[test]
fn page_inner_has_content() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn content("),
"REQ-LIB-001: PageInner must have content method"
);
}
#[test]
fn page_inner_has_reload_go_back_go_forward() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn reload(&self)"),
"REQ-LIB-001: PageInner must have reload method"
);
assert!(
source.contains("pub fn go_back(&self)"),
"REQ-LIB-001: PageInner must have go_back method"
);
assert!(
source.contains("pub fn go_forward(&self)"),
"REQ-LIB-001: PageInner must have go_forward method"
);
}
#[test]
fn page_inner_has_dispatch_mouse_event() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn dispatch_mouse_event("),
"REQ-LIB-001: PageInner must have dispatch_mouse_event method"
);
}
#[test]
fn page_inner_has_dispatch_key_event() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn dispatch_key_event("),
"REQ-LIB-001: PageInner must have dispatch_key_event method"
);
}
#[test]
fn page_handle_has_high_level_api() {
let source = include_str!("page.rs");
assert!(
source.contains("pub fn click(&self"),
"PageHandle must have click"
);
assert!(
source.contains("pub fn type_text(&self"),
"PageHandle must have type_text"
);
assert!(
source.contains("pub fn fill(&self"),
"PageHandle must have fill"
);
assert!(
source.contains("pub fn set_content(&self"),
"PageHandle must have set_content"
);
assert!(
source.contains("pub fn content(&self"),
"PageHandle must have content"
);
assert!(
source.contains("pub fn press(&self"),
"PageHandle must have press"
);
assert!(
source.contains("pub fn hover(&self"),
"PageHandle must have hover"
);
assert!(
source.contains("pub fn focus_element(&self"),
"PageHandle must have focus_element"
);
assert!(
source.contains("pub fn reload(&self"),
"PageHandle must have reload"
);
assert!(
source.contains("pub fn go_back(&self"),
"PageHandle must have go_back"
);
assert!(
source.contains("pub fn go_forward(&self"),
"PageHandle must have go_forward"
);
assert!(
source.contains("pub fn select(&self"),
"PageHandle must have select"
);
assert!(
source.contains("pub fn set_viewport(&self"),
"PageHandle must have set_viewport"
);
assert!(
source.contains("pub fn cookies(&self"),
"PageHandle must have cookies"
);
assert!(
source.contains("pub fn set_cookie(&self"),
"PageHandle must have set_cookie"
);
assert!(
source.contains("pub fn delete_cookie(&self"),
"PageHandle must have delete_cookie"
);
}
}