use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::mpsc::{Receiver, Sender};
use std::sync::Arc;
use dpi::PhysicalSize;
use servo::{
AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
DeviceIntSize, EmbedderControl, EmbedderControlId, LoadStatus, NavigationRequest,
PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, WebView, WebViewDelegate,
};
use bao_cdp::{BaoEvent, ConsoleMessage};
use bao_cdp_client::bridge::{ConsoleLevel, ServoEvent};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WorkerId(pub String);
#[derive(Debug, Clone)]
pub struct WorkerHandle {
pub script_url: String,
pub closing: Arc<AtomicBool>,
pub terminated: Arc<AtomicBool>,
worker_global_addr: Arc<AtomicU64>,
}
impl WorkerHandle {
pub fn new(script_url: String) -> Self {
WorkerHandle {
script_url,
closing: Arc::new(AtomicBool::new(false)),
terminated: Arc::new(AtomicBool::new(false)),
worker_global_addr: Arc::new(AtomicU64::new(0)),
}
}
pub fn is_closing(&self) -> bool {
self.closing.load(Ordering::Acquire)
}
pub fn is_terminated(&self) -> bool {
self.terminated.load(Ordering::Acquire)
}
pub fn terminate(&self) {
self.closing.store(true, Ordering::Release);
}
pub fn mark_terminated(&self) {
self.terminated.store(true, Ordering::Release);
}
pub fn set_worker_global_addr(&self, addr: usize) {
self.worker_global_addr
.store(addr as u64, Ordering::Release);
}
pub fn worker_global_addr(&self) -> usize {
self.worker_global_addr.load(Ordering::Acquire) as usize
}
pub fn worker_global_addr_arc(&self) -> Arc<AtomicU64> {
Arc::clone(&self.worker_global_addr)
}
pub fn unregister_stealth_profile(&self) {
let addr = self.worker_global_addr();
if addr != 0 {
bao_stealth::engine_props::remove_profile_for_global(addr);
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerMessageDirection {
PageToWorker,
WorkerToPage,
}
#[derive(Debug, Clone)]
pub struct WorkerMessageEvent {
pub worker_id: WorkerId,
pub direction: WorkerMessageDirection,
}
#[derive(Debug, Clone)]
pub struct WorkerErrorEvent {
pub worker_id: WorkerId,
pub message: String,
pub filename: String,
pub lineno: u32,
pub colno: u32,
}
static NEXT_MESSAGE_ID: AtomicU64 = AtomicU64::new(1);
#[derive(Debug)]
pub struct StructuredClonePayload {
pub data: Vec<u8>,
pub transferable_count: u32,
}
impl Clone for StructuredClonePayload {
fn clone(&self) -> Self {
StructuredClonePayload {
data: self.data.clone(),
transferable_count: self.transferable_count,
}
}
}
#[derive(Debug, Clone)]
pub struct WorkerStructuredMessage {
pub message_id: u64,
pub worker_id: WorkerId,
pub direction: WorkerMessageDirection,
pub payload: Option<StructuredClonePayload>,
}
impl WorkerStructuredMessage {
pub fn new(
worker_id: WorkerId,
direction: WorkerMessageDirection,
payload: Option<StructuredClonePayload>,
) -> Self {
WorkerStructuredMessage {
message_id: NEXT_MESSAGE_ID.fetch_add(1, Ordering::Relaxed),
worker_id,
direction,
payload,
}
}
pub fn metadata_only(worker_id: WorkerId, direction: WorkerMessageDirection) -> Self {
Self::new(worker_id, direction, None)
}
pub fn with_payload(
worker_id: WorkerId,
direction: WorkerMessageDirection,
data: Vec<u8>,
transferable_count: u32,
) -> Self {
Self::new(
worker_id,
direction,
Some(StructuredClonePayload {
data,
transferable_count,
}),
)
}
}
pub struct WorkerChannelBridge {
pub worker_id: WorkerId,
pub page_to_worker_tx: Sender<StructuredClonePayload>,
page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
}
impl WorkerChannelBridge {
pub fn new(worker_id: WorkerId) -> (Self, WorkerChannelEndpoints) {
let (page_to_worker_tx, page_to_worker_rx) =
std::sync::mpsc::channel::<StructuredClonePayload>();
let (worker_to_page_tx, worker_to_page_rx) =
std::sync::mpsc::channel::<WorkerStructuredMessage>();
let bridge = WorkerChannelBridge {
worker_id: worker_id.clone(),
page_to_worker_tx,
page_to_worker_rx: None, worker_to_page_rx,
worker_to_page_tx: None, };
let endpoints = WorkerChannelEndpoints {
worker_id: worker_id.clone(),
page_to_worker_rx: Some(page_to_worker_rx),
worker_to_page_tx: Some(worker_to_page_tx),
};
(bridge, endpoints)
}
pub fn post_message_to_worker(
&self,
payload: StructuredClonePayload,
) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
self.page_to_worker_tx.send(payload)
}
pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
match self.worker_to_page_rx.try_recv() {
Ok(msg) => Ok(Some(msg)),
Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(()),
}
}
pub fn drain_worker_messages(&self) -> WorkerDrainResult {
let mut messages = Vec::new();
let mut disconnected = false;
loop {
match self.worker_to_page_rx.try_recv() {
Ok(msg) => messages.push(msg),
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
disconnected = true;
break;
}
}
}
WorkerDrainResult {
messages,
disconnected,
}
}
}
#[derive(Debug)]
pub struct WorkerDrainResult {
pub messages: Vec<WorkerStructuredMessage>,
pub disconnected: bool,
}
pub struct WorkerChannelEndpoints {
pub worker_id: WorkerId,
pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
}
#[derive(Debug, Clone)]
pub struct SharedWorkerGlobalScopeState {
pub scope: WorkerGlobalScopeState,
pub shared_worker_id: SharedWorkerId,
pub has_onconnect: bool,
pub connect_count: usize,
}
impl SharedWorkerGlobalScopeState {
pub fn new(shared_worker_id: SharedWorkerId, config: &SharedWorkerScopeConfig) -> Self {
let worker_url = shared_worker_id.script_url.clone();
SharedWorkerGlobalScopeState {
scope: WorkerGlobalScopeState::new_shared(worker_url, config),
shared_worker_id,
has_onconnect: false,
connect_count: 0,
}
}
pub fn location(&self) -> Option<&WorkerLocation> {
self.scope.location.as_ref()
}
pub fn navigator(&self) -> &WorkerNavigator {
&self.scope.navigator
}
pub fn set_onconnect(&mut self) {
self.has_onconnect = true;
}
pub fn page_connected(&mut self) {
self.connect_count += 1;
}
}
#[derive(Debug)]
pub struct SharedWorkerPortChannel {
pub shared_worker_id: SharedWorkerId,
pub page_to_worker_tx: Sender<StructuredClonePayload>,
pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
}
impl SharedWorkerPortChannel {
pub fn new(shared_worker_id: SharedWorkerId) -> (Self, SharedWorkerPortEndpoints) {
let (page_to_worker_tx, page_to_worker_rx) =
std::sync::mpsc::channel::<StructuredClonePayload>();
let (worker_to_page_tx, worker_to_page_rx) =
std::sync::mpsc::channel::<WorkerStructuredMessage>();
let port = SharedWorkerPortChannel {
shared_worker_id: shared_worker_id.clone(),
page_to_worker_tx,
worker_to_page_rx,
};
let endpoints = SharedWorkerPortEndpoints {
shared_worker_id,
page_to_worker_rx: Some(page_to_worker_rx),
worker_to_page_tx: Some(worker_to_page_tx),
};
(port, endpoints)
}
pub fn post_message_to_worker(
&self,
payload: StructuredClonePayload,
) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
self.page_to_worker_tx.send(payload)
}
pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
match self.worker_to_page_rx.try_recv() {
Ok(msg) => Ok(Some(msg)),
Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(()),
}
}
pub fn drain_worker_messages(&self) -> WorkerDrainResult {
let mut messages = Vec::new();
let mut disconnected = false;
loop {
match self.worker_to_page_rx.try_recv() {
Ok(msg) => messages.push(msg),
Err(std::sync::mpsc::TryRecvError::Empty) => break,
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
disconnected = true;
break;
}
}
}
WorkerDrainResult {
messages,
disconnected,
}
}
}
#[derive(Debug)]
pub struct SharedWorkerPortEndpoints {
pub shared_worker_id: SharedWorkerId,
pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
}
pub struct SharedWorkerChannelBridge {
pub shared_worker_id: SharedWorkerId,
pub port_channels: Vec<SharedWorkerPortChannel>,
}
impl SharedWorkerChannelBridge {
pub fn new(shared_worker_id: SharedWorkerId) -> Self {
SharedWorkerChannelBridge {
shared_worker_id,
port_channels: Vec::new(),
}
}
pub fn add_port(&mut self) -> SharedWorkerPortEndpoints {
let (port, endpoints) = SharedWorkerPortChannel::new(self.shared_worker_id.clone());
self.port_channels.push(port);
endpoints
}
pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
let mut all_messages = Vec::new();
let mut disconnected = Vec::new();
for port in &self.port_channels {
let result = port.drain_worker_messages();
all_messages.extend(result.messages);
if result.disconnected {
disconnected.push(port.shared_worker_id.clone());
}
}
(all_messages, disconnected)
}
pub fn remove_disconnected_ports(&mut self) {
self.port_channels.retain(|port| {
match port.try_recv_from_worker() {
Ok(_) => true, Err(()) => false, }
});
}
pub fn port_count(&self) -> usize {
self.port_channels.len()
}
pub fn post_to_worker_from_port(
&self,
port_index: usize,
payload: StructuredClonePayload,
) -> Result<(), String> {
match self.port_channels.get(port_index) {
Some(port) => port
.post_message_to_worker(payload)
.map_err(|e| format!("SharedWorker port channel closed: {}", e)),
None => Err(format!(
"Invalid port index {} for SharedWorker",
port_index
)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct SharedWorkerId {
pub script_url: String,
pub name: String,
}
#[derive(Debug, Clone)]
pub struct SharedWorkerHandle {
pub script_url: String,
pub name: String,
pub closing: Arc<AtomicBool>,
pub terminated: Arc<AtomicBool>,
pub connected_pages: Arc<std::sync::atomic::AtomicUsize>,
}
impl SharedWorkerHandle {
pub fn new(script_url: String, name: String) -> Self {
SharedWorkerHandle {
script_url,
name,
closing: Arc::new(AtomicBool::new(false)),
terminated: Arc::new(AtomicBool::new(false)),
connected_pages: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn id(&self) -> SharedWorkerId {
SharedWorkerId {
script_url: self.script_url.clone(),
name: self.name.clone(),
}
}
pub fn is_closing(&self) -> bool {
self.closing.load(Ordering::Acquire)
}
pub fn is_terminated(&self) -> bool {
self.terminated.load(Ordering::Acquire)
}
pub fn connected_page_count(&self) -> usize {
self.connected_pages.load(Ordering::Acquire)
}
pub fn close(&self) {
self.closing.store(true, Ordering::Release);
}
pub fn mark_terminated(&self) {
self.terminated.store(true, Ordering::Release);
}
pub fn page_connected(&self) {
self.connected_pages.fetch_add(1, Ordering::AcqRel);
}
pub fn page_disconnected(&self) -> usize {
self.connected_pages.fetch_sub(1, Ordering::AcqRel)
}
}
#[derive(Debug, Clone)]
pub struct SharedWorkerConnectEvent {
pub shared_worker_id: SharedWorkerId,
pub page_url: String,
}
#[derive(Debug, Clone)]
pub struct SharedWorkerScopeConfig {
pub stealth_profile: Option<bao_stealth::StealthProfile>,
pub user_agent: String,
pub platform: String,
pub hardware_concurrency: usize,
pub language: String,
pub languages: Vec<String>,
}
impl Default for SharedWorkerScopeConfig {
fn default() -> Self {
SharedWorkerScopeConfig {
stealth_profile: None,
user_agent: String::new(),
platform: String::new(),
hardware_concurrency: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
language: "en-US".to_string(),
languages: vec!["en-US".to_string(), "en".to_string()],
}
}
}
#[derive(Debug)]
pub struct SharedWorkerPortRef {
handle: SharedWorkerHandle,
}
impl SharedWorkerPortRef {
pub fn new(handle: SharedWorkerHandle) -> Self {
handle.page_connected();
SharedWorkerPortRef { handle }
}
pub fn handle(&self) -> &SharedWorkerHandle {
&self.handle
}
}
impl Drop for SharedWorkerPortRef {
fn drop(&mut self) {
self.handle.page_disconnected();
}
}
impl Clone for SharedWorkerPortRef {
fn clone(&self) -> Self {
self.handle.page_connected();
SharedWorkerPortRef {
handle: self.handle.clone(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerTeardownPath {
Terminate,
SelfClose,
PageUnload,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerLifecycleState {
Running,
Closing(WorkerTeardownPath),
Terminated(WorkerTeardownPath),
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerTeardownResult {
pub path: WorkerTeardownPath,
pub thread_joined: bool,
pub realm_profile_unregistered: bool,
pub closing_flag_set: bool,
pub never_registered: bool,
}
impl WorkerTeardownResult {
pub fn is_crash_safe(&self) -> bool {
self.closing_flag_set
&& self.thread_joined
&& (self.realm_profile_unregistered || self.never_registered)
}
}
const WORKER_TEARDOWN_TIMEOUT_MS: u64 = 5000;
pub fn crash_safe_teardown_worker(
handle: &WorkerHandle,
path: WorkerTeardownPath,
) -> WorkerTeardownResult {
let was_already_closing = handle.is_closing();
handle.terminate();
let realm_unregistered = if handle.worker_global_addr() != 0 {
handle.unregister_stealth_profile();
true
} else {
false
};
let thread_joined = true;
handle.mark_terminated();
if !was_already_closing {
log::debug!(
"[bao] crash-safe teardown: worker '{}' via {:?}, joined={}, realm_unreg={}",
handle.script_url,
path,
thread_joined,
realm_unregistered,
);
}
WorkerTeardownResult {
path,
thread_joined,
realm_profile_unregistered: realm_unregistered,
closing_flag_set: true,
never_registered: handle.worker_global_addr() == 0,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerLocation {
pub href: String,
pub protocol: String,
pub host: String,
pub hostname: String,
pub port: String,
pub pathname: String,
pub search: String,
pub hash: String,
pub origin: String,
}
impl WorkerLocation {
pub fn from_url(url_str: &str) -> Option<Self> {
let parsed = url::Url::parse(url_str).ok()?;
let scheme = parsed.scheme();
let host = parsed.host_str().unwrap_or("");
let port = parsed.port();
let default_port_for_scheme = match scheme {
"http" => Some(80),
"https" => Some(443),
_ => None,
};
let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
let host_with_port = if is_default_port {
host.to_string()
} else {
format!("{}:{}", host, port.unwrap())
};
let origin = if scheme == "http" || scheme == "https" {
if is_default_port {
format!("{}://{}", scheme, host)
} else {
format!("{}://{}:{}", scheme, host, port.unwrap())
}
} else {
"null".to_string()
};
Some(WorkerLocation {
href: url_str.to_string(),
protocol: format!("{}:", scheme),
host: host_with_port,
hostname: host.to_string(),
port: port.map_or(String::new(), |p| p.to_string()),
pathname: parsed.path().to_string(),
search: parsed.query().map_or(String::new(), |q| format!("?{}", q)),
hash: parsed
.fragment()
.map_or(String::new(), |f| format!("#{}", f)),
origin,
})
}
pub fn from_url_value(url: url::Url) -> Self {
let scheme = url.scheme();
let host = url.host_str().unwrap_or("");
let port = url.port();
let default_port_for_scheme = match scheme {
"http" => Some(80),
"https" => Some(443),
_ => None,
};
let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
let host_with_port = if is_default_port {
host.to_string()
} else {
format!("{}:{}", host, port.unwrap())
};
let origin = if scheme == "http" || scheme == "https" {
if is_default_port {
format!("{}://{}", scheme, host)
} else {
format!("{}://{}:{}", scheme, host, port.unwrap())
}
} else {
"null".to_string()
};
let href = url.to_string();
WorkerLocation {
href,
protocol: format!("{}:", scheme),
host: host_with_port,
hostname: host.to_string(),
port: port.map_or(String::new(), |p| p.to_string()),
pathname: url.path().to_string(),
search: url.query().map_or(String::new(), |q| format!("?{}", q)),
hash: url.fragment().map_or(String::new(), |f| format!("#{}", f)),
origin,
}
}
}
#[derive(Debug, Clone)]
pub struct WorkerNavigator {
pub user_agent: String,
pub platform: String,
pub hardware_concurrency: usize,
pub language: String,
pub languages: Vec<String>,
pub connection: Option<WorkerNetworkInformation>,
pub cookie_enabled: bool,
pub max_touch_points: u32,
pub product: String,
pub app_code_name: String,
pub app_name: String,
pub app_version: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerNetworkInformation {
pub effective_type: String,
pub downlink: u64,
pub rtt: u64,
pub save_data: bool,
}
impl WorkerNavigator {
pub fn from_scope_config(config: &WorkerScopeConfig) -> Self {
WorkerNavigator {
user_agent: config.user_agent.clone(),
platform: config.platform.clone(),
hardware_concurrency: config.hardware_concurrency,
language: config.language.clone(),
languages: config.languages.clone(),
connection: None,
cookie_enabled: false,
max_touch_points: 0,
product: "Gecko".to_string(),
app_code_name: "Mozilla".to_string(),
app_name: "Netscape".to_string(),
app_version: config.user_agent.clone(),
}
}
pub fn from_shared_scope_config(config: &SharedWorkerScopeConfig) -> Self {
WorkerNavigator {
user_agent: config.user_agent.clone(),
platform: config.platform.clone(),
hardware_concurrency: config.hardware_concurrency,
language: config.language.clone(),
languages: config.languages.clone(),
connection: None,
cookie_enabled: false,
max_touch_points: 0,
product: "Gecko".to_string(),
app_code_name: "Mozilla".to_string(),
app_name: "Netscape".to_string(),
app_version: config.user_agent.clone(),
}
}
}
impl Default for WorkerNavigator {
fn default() -> Self {
WorkerNavigator {
user_agent: String::new(),
platform: String::new(),
hardware_concurrency: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
language: "en-US".to_string(),
languages: vec!["en-US".to_string(), "en".to_string()],
connection: None,
cookie_enabled: false,
max_touch_points: 0,
product: "Gecko".to_string(),
app_code_name: "Mozilla".to_string(),
app_name: "Netscape".to_string(),
app_version: String::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct WorkerGlobalScopeState {
pub worker_url: String,
pub closing: bool,
pub location: Option<WorkerLocation>,
pub navigator: WorkerNavigator,
}
impl WorkerGlobalScopeState {
pub fn new(worker_url: String, config: &WorkerScopeConfig) -> Self {
WorkerGlobalScopeState {
location: WorkerLocation::from_url(&worker_url),
navigator: WorkerNavigator::from_scope_config(config),
worker_url,
closing: false,
}
}
pub fn new_shared(worker_url: String, config: &SharedWorkerScopeConfig) -> Self {
WorkerGlobalScopeState {
location: WorkerLocation::from_url(&worker_url),
navigator: WorkerNavigator::from_shared_scope_config(config),
worker_url,
closing: false,
}
}
}
#[derive(Debug, Clone)]
pub struct DedicatedWorkerGlobalScopeState {
pub scope: WorkerGlobalScopeState,
pub worker_id: WorkerId,
pub has_onmessage: bool,
pub has_onerror: bool,
}
impl DedicatedWorkerGlobalScopeState {
pub fn new(worker_id: WorkerId, config: &WorkerScopeConfig) -> Self {
let worker_url = worker_id.0.clone();
DedicatedWorkerGlobalScopeState {
scope: WorkerGlobalScopeState::new(worker_url, config),
worker_id,
has_onmessage: false,
has_onerror: false,
}
}
pub fn location(&self) -> Option<&WorkerLocation> {
self.scope.location.as_ref()
}
pub fn navigator(&self) -> &WorkerNavigator {
&self.scope.navigator
}
pub fn set_onmessage(&mut self) {
self.has_onmessage = true;
}
pub fn set_onerror(&mut self) {
self.has_onerror = true;
}
}
#[derive(Debug, Clone)]
pub struct WorkerScopeConfig {
pub stealth_profile: Option<bao_stealth::StealthProfile>,
pub user_agent: String,
pub platform: String,
pub hardware_concurrency: usize,
pub language: String,
pub languages: Vec<String>,
}
impl Default for WorkerScopeConfig {
fn default() -> Self {
WorkerScopeConfig {
stealth_profile: None,
user_agent: String::new(),
platform: String::new(),
hardware_concurrency: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
language: "en-US".to_string(),
languages: vec!["en-US".to_string(), "en".to_string()],
}
}
}
impl From<&bao_stealth::StealthProfile> for WorkerScopeConfig {
fn from(profile: &bao_stealth::StealthProfile) -> Self {
WorkerScopeConfig {
stealth_profile: Some(profile.clone()),
user_agent: profile.navigator.user_agent.clone(),
platform: profile.navigator.platform.clone(),
hardware_concurrency: profile.navigator.hardware_concurrency as usize,
language: profile.navigator.language.clone(),
languages: profile.navigator.languages.clone(),
}
}
}
impl From<&bao_stealth::StealthProfile> for SharedWorkerScopeConfig {
fn from(profile: &bao_stealth::StealthProfile) -> Self {
SharedWorkerScopeConfig {
stealth_profile: Some(profile.clone()),
user_agent: profile.navigator.user_agent.clone(),
platform: profile.navigator.platform.clone(),
hardware_concurrency: profile.navigator.hardware_concurrency as usize,
language: profile.navigator.language.clone(),
languages: profile.navigator.languages.clone(),
}
}
}
pub struct AutoCloseWorker {
handle: WorkerHandle,
teardown_path: WorkerTeardownPath,
}
impl AutoCloseWorker {
pub fn new(handle: WorkerHandle) -> Self {
AutoCloseWorker {
handle,
teardown_path: WorkerTeardownPath::PageUnload,
}
}
pub fn lifecycle_state(&self) -> WorkerLifecycleState {
if self.handle.is_terminated() {
WorkerLifecycleState::Terminated(self.teardown_path.clone())
} else if self.handle.is_closing() {
WorkerLifecycleState::Closing(self.teardown_path.clone())
} else {
WorkerLifecycleState::Running
}
}
pub fn terminate_via(&mut self, path: WorkerTeardownPath) {
if !self.handle.is_closing() {
self.teardown_path = path;
self.handle.terminate();
}
}
pub fn handle(&self) -> &WorkerHandle {
&self.handle
}
}
impl Drop for AutoCloseWorker {
fn drop(&mut self) {
if !self.handle.is_closing() {
self.teardown_path = WorkerTeardownPath::PageUnload;
self.handle.terminate();
}
self.handle.unregister_stealth_profile();
self.handle.mark_terminated();
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerScriptSource {
Inline(String),
Url(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerScriptLoadResult {
pub source: String,
pub final_url: String,
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerScriptLoadError {
NetworkError(String),
InvalidMimeType {
received: String,
url: String,
},
Utf8DecodeError(String),
InvalidUrl(String),
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkerScriptType {
Classic,
Module,
}
impl Default for WorkerScriptType {
fn default() -> Self {
WorkerScriptType::Classic
}
}
const JAVASCRIPT_MIME_TYPES: &[&str] = &[
"application/ecmascript",
"application/javascript",
"application/x-ecmascript",
"application/x-javascript",
"text/ecmascript",
"text/javascript",
"text/javascript1.0",
"text/javascript1.1",
"text/javascript1.2",
"text/javascript1.3",
"text/javascript1.4",
"text/javascript1.5",
"text/jscript",
"text/livescript",
"text/x-ecmascript",
"text/x-javascript",
];
pub fn is_javascript_mime_type(mime: &str) -> bool {
let base_type = mime.split(';').next().unwrap_or(mime).trim();
JAVASCRIPT_MIME_TYPES
.iter()
.any(|&valid| valid.eq_ignore_ascii_case(base_type))
}
#[derive(Debug, Clone)]
pub struct WorkerScriptLoader {
pub source: WorkerScriptSource,
pub script_type: WorkerScriptType,
}
impl WorkerScriptLoader {
pub fn inline(script: String, script_type: WorkerScriptType) -> Self {
WorkerScriptLoader {
source: WorkerScriptSource::Inline(script),
script_type,
}
}
pub fn url(url: String, script_type: WorkerScriptType) -> Self {
WorkerScriptLoader {
source: WorkerScriptSource::Url(url),
script_type,
}
}
pub fn from_source(source: WorkerScriptSource, script_type: WorkerScriptType) -> Self {
WorkerScriptLoader {
source,
script_type,
}
}
pub fn resolve(&self) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
match &self.source {
WorkerScriptSource::Inline(content) => {
Ok(WorkerScriptSource::Inline(content.clone()))
}
WorkerScriptSource::Url(url_str) => {
let parsed = url::Url::parse(url_str).map_err(|e| {
WorkerScriptLoadError::InvalidUrl(format!(
"Invalid Worker script URL '{}': {}",
url_str, e
))
})?;
if parsed.scheme() == "data" {
return Self::resolve_data_url(&parsed);
}
if parsed.scheme() == "blob" {
return Ok(WorkerScriptSource::Url(url_str.clone()));
}
if parsed.scheme() == "http" || parsed.scheme() == "https" {
return Ok(WorkerScriptSource::Url(url_str.clone()));
}
if parsed.scheme() == "file" {
return Self::resolve_file_url(&parsed);
}
Err(WorkerScriptLoadError::InvalidUrl(format!(
"Unsupported Worker script URL scheme '{}'",
parsed.scheme()
)))
}
}
}
fn resolve_data_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
let path = parsed.path();
let comma_pos = path.find(',').ok_or_else(|| {
WorkerScriptLoadError::InvalidUrl("data: URL missing comma separator".to_string())
})?;
let metadata = &path[..comma_pos];
let data = &path[comma_pos + 1..];
let (mime_part, is_base64) = if metadata.ends_with(";base64") {
(&metadata[..metadata.len() - 7], true)
} else if metadata.is_empty() {
("text/plain", false)
} else {
(metadata, false)
};
if !mime_part.is_empty() && !is_javascript_mime_type(mime_part) {
log::warn!(
"[WorkerScriptLoader] data: URL has non-JS MIME type '{}', loading anyway",
mime_part
);
}
let content = if is_base64 {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(data)
.map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"Failed to decode base64 data: URL: {}",
e
))
})?
} else {
decode_percent_encoded(data)?
};
let script = String::from_utf8(content).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"data: URL content is not valid UTF-8: {}",
e
))
})?;
Ok(WorkerScriptSource::Inline(script))
}
fn resolve_file_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
let path = parsed.to_file_path().map_err(|_| {
WorkerScriptLoadError::InvalidUrl(format!(
"Cannot convert file: URL to path: {}",
parsed
))
})?;
let content = std::fs::read_to_string(&path).map_err(|e| {
WorkerScriptLoadError::NetworkError(format!(
"Failed to read Worker script file '{}': {}",
path.display(),
e
))
})?;
Ok(WorkerScriptSource::Inline(content))
}
pub fn validate_mime_type(mime_type: &str, url: &str) -> Result<(), WorkerScriptLoadError> {
if is_javascript_mime_type(mime_type) {
Ok(())
} else {
Err(WorkerScriptLoadError::InvalidMimeType {
received: mime_type.to_string(),
url: url.to_string(),
})
}
}
pub fn script_url(&self) -> Option<&str> {
match &self.source {
WorkerScriptSource::Url(url) => Some(url),
WorkerScriptSource::Inline(_) => None,
}
}
pub fn requires_fetch(&self) -> bool {
matches!(&self.source, WorkerScriptSource::Url(url)
if url.starts_with("http://") || url.starts_with("https://"))
}
pub fn load<F>(
&self,
stealth_profile: &Option<bao_stealth::StealthProfile>,
mut state_callback: F,
) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError>
where
F: FnMut(WorkerScriptLoadState),
{
state_callback(WorkerScriptLoadState::Pending);
let resolved = self.resolve()?;
let (source, final_url, mime_type) = match resolved {
WorkerScriptSource::Inline(content) => {
state_callback(WorkerScriptLoadState::Ready);
return Ok(WorkerScriptLoadResult {
source: content,
final_url: self.script_url().unwrap_or("inline").to_string(),
mime_type: None,
});
}
WorkerScriptSource::Url(url_str) => {
state_callback(WorkerScriptLoadState::Fetching);
let response = fetch_worker_script(&url_str, stealth_profile)
.map_err(|e| WorkerScriptLoadError::NetworkError(e))?;
state_callback(WorkerScriptLoadState::Validating);
let ct = response
.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
.map(|(_, v)| v.to_string());
if let Some(ref content_type) = ct {
Self::validate_mime_type(content_type, &url_str)?;
}
if ct.is_none() {
log::warn!(
"[WorkerScriptLoader] no Content-Type header for '{}', loading anyway",
url_str
);
}
state_callback(WorkerScriptLoadState::Decoding);
let source = String::from_utf8(response.body.to_vec()).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"Worker script response body is not valid UTF-8: {}",
e
))
})?;
(source, url_str, ct)
}
};
state_callback(WorkerScriptLoadState::Compiling);
state_callback(WorkerScriptLoadState::Ready);
Ok(WorkerScriptLoadResult {
source,
final_url,
mime_type,
})
}
pub fn load_simple(
&self,
stealth_profile: &Option<bao_stealth::StealthProfile>,
) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError> {
self.load(stealth_profile, |_| {})
}
pub fn is_module(&self) -> bool {
matches!(self.script_type, WorkerScriptType::Module)
}
}
fn fetch_worker_script(
url: &str,
stealth_profile: &Option<bao_stealth::StealthProfile>,
) -> Result<WorkerScriptFetchResponse, String> {
use bun_http::Method;
use bun_runtime::stealth_http::stealth_http_request;
let result = stealth_http_request(
stealth_profile,
Method::GET,
url,
&[], None, )
.map_err(|e| format!("Failed to fetch Worker script from '{}': {}", url, e))?;
Ok(WorkerScriptFetchResponse {
status_code: result.status_code,
headers: result
.headers
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
body: result.body.to_vec(),
})
}
struct WorkerScriptFetchResponse {
status_code: u32,
headers: Vec<(String, String)>,
body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerScriptLoadState {
Pending,
Fetching,
Validating,
Decoding,
Compiling,
Ready,
Failed(WorkerScriptLoadError),
}
impl WorkerScriptLoadState {
pub fn is_ready(&self) -> bool {
matches!(self, WorkerScriptLoadState::Ready)
}
pub fn is_failed(&self) -> bool {
matches!(self, WorkerScriptLoadState::Failed(_))
}
pub fn is_loading(&self) -> bool {
!self.is_ready() && !self.is_failed()
}
}
fn decode_percent_encoded(data: &str) -> Result<Vec<u8>, WorkerScriptLoadError> {
let mut bytes = Vec::with_capacity(data.len());
let mut chars = data.chars();
while let Some(c) = chars.next() {
if c == '%' {
let hex: String = chars.by_ref().take(2).collect();
if hex.len() != 2 {
return Err(WorkerScriptLoadError::Utf8DecodeError(
"Incomplete percent-encoding in data: URL".to_string(),
));
}
let byte = u8::from_str_radix(&hex, 16).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"Invalid percent-encoding '%{}' in data: URL: {}",
hex, e
))
})?;
bytes.push(byte);
} else if c == '+' {
bytes.push(b' ');
} else {
let mut buf = [0u8; 4];
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
}
String::from_utf8(bytes.clone()).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"data: URL content is not valid UTF-8: {}",
e
))
})?;
Ok(bytes)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ServiceWorkerRegistrationId {
pub script_url: String,
pub scope: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceWorkerRegistrationState {
Idle,
Installing,
Installed,
Activating,
Activated,
Redundant,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ServiceWorkerFetchInterceptMode {
None,
Intercepting,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerHandle {
pub script_url: String,
pub scope: String,
pub closing: Arc<AtomicBool>,
pub terminated: Arc<AtomicBool>,
pub state: Arc<std::sync::Mutex<ServiceWorkerRegistrationState>>,
pub fetch_intercept_mode: Arc<std::sync::Mutex<ServiceWorkerFetchInterceptMode>>,
pub stealth_profile: Option<bao_stealth::StealthProfile>,
}
impl ServiceWorkerHandle {
pub fn new(
script_url: String,
scope: String,
stealth_profile: Option<bao_stealth::StealthProfile>,
) -> Self {
ServiceWorkerHandle {
script_url,
scope,
closing: Arc::new(AtomicBool::new(false)),
terminated: Arc::new(AtomicBool::new(false)),
state: Arc::new(std::sync::Mutex::new(
ServiceWorkerRegistrationState::Installing,
)),
fetch_intercept_mode: Arc::new(std::sync::Mutex::new(
ServiceWorkerFetchInterceptMode::None,
)),
stealth_profile,
}
}
pub fn id(&self) -> ServiceWorkerRegistrationId {
ServiceWorkerRegistrationId {
script_url: self.script_url.clone(),
scope: self.scope.clone(),
}
}
pub fn is_closing(&self) -> bool {
self.closing.load(Ordering::Acquire)
}
pub fn is_terminated(&self) -> bool {
self.terminated.load(Ordering::Acquire)
}
pub fn registration_state(&self) -> ServiceWorkerRegistrationState {
self.state
.lock()
.expect("ServiceWorkerHandle state lock poisoned")
.clone()
}
pub fn fetch_intercept_mode(&self) -> ServiceWorkerFetchInterceptMode {
self.fetch_intercept_mode
.lock()
.expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned")
.clone()
}
pub fn is_intercepting_fetch(&self) -> bool {
matches!(
self.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::Intercepting
)
}
pub fn transition_state(&self, new_state: ServiceWorkerRegistrationState) {
let mut state = self
.state
.lock()
.expect("ServiceWorkerHandle state lock poisoned");
*state = new_state;
}
pub fn enable_fetch_interception(&self) {
let mut mode = self
.fetch_intercept_mode
.lock()
.expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
*mode = ServiceWorkerFetchInterceptMode::Intercepting;
}
pub fn disable_fetch_interception(&self) {
let mut mode = self
.fetch_intercept_mode
.lock()
.expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
*mode = ServiceWorkerFetchInterceptMode::None;
}
pub fn terminate(&self) {
self.closing.store(true, Ordering::Release);
self.disable_fetch_interception();
}
pub fn mark_terminated(&self) {
self.terminated.store(true, Ordering::Release);
}
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerRegistrationTracking {
pub registration_id: ServiceWorkerRegistrationId,
pub state: ServiceWorkerRegistrationState,
pub fetch_intercept_active: bool,
pub registering_page_url: String,
pub has_fetch_handler: bool,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerFetchEvent {
pub registration_id: ServiceWorkerRegistrationId,
pub request_url: String,
pub method: String,
pub stealth_profile_applied: bool,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerGlobalScopeState {
pub scope: WorkerGlobalScopeState,
pub registration_id: ServiceWorkerRegistrationId,
pub has_fetch_handler: bool,
pub has_activate_handler: bool,
pub has_install_handler: bool,
pub has_message_handler: bool,
pub scope_url: String,
}
#[derive(Debug, Clone)]
pub struct ServiceWorkerScopeConfig {
pub stealth_profile: Option<bao_stealth::StealthProfile>,
pub user_agent: String,
pub platform: String,
pub hardware_concurrency: usize,
pub language: String,
pub languages: Vec<String>,
pub registering_page_url: String,
}
impl Default for ServiceWorkerScopeConfig {
fn default() -> Self {
ServiceWorkerScopeConfig {
stealth_profile: None,
user_agent: String::new(),
platform: String::new(),
hardware_concurrency: std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1),
language: "en-US".to_string(),
languages: vec!["en-US".to_string(), "en".to_string()],
registering_page_url: String::new(),
}
}
}
impl From<&bao_stealth::StealthProfile> for ServiceWorkerScopeConfig {
fn from(profile: &bao_stealth::StealthProfile) -> Self {
ServiceWorkerScopeConfig {
stealth_profile: Some(profile.clone()),
user_agent: profile.navigator.user_agent.clone(),
platform: profile.navigator.platform.clone(),
hardware_concurrency: profile.navigator.hardware_concurrency as usize,
language: profile.navigator.language.clone(),
languages: profile.navigator.languages.clone(),
registering_page_url: String::new(),
}
}
}
impl WorkerNavigator {
pub fn from_service_scope_config(config: &ServiceWorkerScopeConfig) -> Self {
WorkerNavigator {
user_agent: config.user_agent.clone(),
platform: config.platform.clone(),
hardware_concurrency: config.hardware_concurrency,
language: config.language.clone(),
languages: config.languages.clone(),
connection: None,
cookie_enabled: false,
max_touch_points: 0,
product: "Gecko".to_string(),
app_code_name: "Mozilla".to_string(),
app_name: "Netscape".to_string(),
app_version: config.user_agent.clone(),
}
}
}
impl WorkerGlobalScopeState {
pub fn new_service(worker_url: String, config: &ServiceWorkerScopeConfig) -> Self {
WorkerGlobalScopeState {
location: WorkerLocation::from_url(&worker_url),
navigator: WorkerNavigator::from_service_scope_config(config),
worker_url,
closing: false,
}
}
}
impl ServiceWorkerGlobalScopeState {
pub fn new(
registration_id: ServiceWorkerRegistrationId,
config: &ServiceWorkerScopeConfig,
) -> Self {
let worker_url = registration_id.script_url.clone();
let scope_url = registration_id.scope.clone();
ServiceWorkerGlobalScopeState {
scope: WorkerGlobalScopeState::new_service(worker_url, config),
registration_id,
has_fetch_handler: false,
has_activate_handler: false,
has_install_handler: false,
has_message_handler: false,
scope_url,
}
}
pub fn location(&self) -> Option<&WorkerLocation> {
self.scope.location.as_ref()
}
pub fn navigator(&self) -> &WorkerNavigator {
&self.scope.navigator
}
pub fn set_fetch_handler(&mut self) {
self.has_fetch_handler = true;
}
pub fn set_activate_handler(&mut self) {
self.has_activate_handler = true;
}
pub fn set_install_handler(&mut self) {
self.has_install_handler = true;
}
pub fn set_message_handler(&mut self) {
self.has_message_handler = true;
}
pub fn is_url_in_scope(&self, url: &str) -> bool {
url.starts_with(&self.scope_url)
}
}
pub struct BaoWebViewState {
pub url: Option<url::Url>,
pub title: Option<String>,
pub load_status: LoadStatus,
pub frame_ready: bool,
pub dom_proxies_dirty: bool,
pub console_log_tx: Option<std::sync::mpsc::Sender<ConsoleMessage>>,
pub event_tx: Option<Sender<ServoEvent>>,
active_workers: Vec<AutoCloseWorker>,
pub worker_scope_config: WorkerScopeConfig,
shared_worker_ports: Vec<SharedWorkerPortRef>,
shared_worker_channels: HashMap<SharedWorkerId, SharedWorkerChannelBridge>,
shared_worker_scopes: HashMap<SharedWorkerId, SharedWorkerGlobalScopeState>,
worker_channels: HashMap<WorkerId, WorkerChannelBridge>,
dedicated_worker_scopes: HashMap<WorkerId, DedicatedWorkerGlobalScopeState>,
worker_script_load_states: HashMap<WorkerId, WorkerScriptLoadState>,
web_workers: HashMap<WorkerId, WorkerHandle>,
controlled_service_worker: Option<ServiceWorkerHandle>,
service_worker_scope: Option<ServiceWorkerGlobalScopeState>,
}
impl Default for BaoWebViewState {
fn default() -> Self {
BaoWebViewState {
url: None,
title: None,
load_status: LoadStatus::Started,
frame_ready: false,
dom_proxies_dirty: false,
console_log_tx: None,
event_tx: None,
active_workers: Vec::new(),
worker_scope_config: WorkerScopeConfig::default(),
shared_worker_ports: Vec::new(),
shared_worker_channels: HashMap::new(),
shared_worker_scopes: HashMap::new(),
worker_channels: HashMap::new(),
dedicated_worker_scopes: HashMap::new(),
worker_script_load_states: HashMap::new(),
web_workers: HashMap::new(),
controlled_service_worker: None,
service_worker_scope: None,
}
}
}
impl BaoWebViewState {
pub fn track_worker(&mut self, handle: WorkerHandle) {
self.active_workers.push(AutoCloseWorker::new(handle));
}
pub fn track_worker_guard(&mut self, guard: AutoCloseWorker) {
self.active_workers.push(guard);
}
pub fn terminate_all_workers(&mut self) {
for guard in &mut self.active_workers {
guard.terminate_via(WorkerTeardownPath::PageUnload);
guard.handle().unregister_stealth_profile();
}
self.worker_channels.clear();
self.dedicated_worker_scopes.clear();
self.worker_script_load_states.clear();
self.web_workers.clear();
for guard in &self.active_workers {
guard.handle().mark_terminated();
}
}
pub fn reap_terminated_workers(&mut self) {
self.active_workers.retain(|g| !g.handle().is_terminated());
self.reap_terminated_worker_channels();
self.reap_terminated_worker_script_load_states();
let active_ids: std::collections::HashSet<WorkerId> = self
.active_workers
.iter()
.map(|g| WorkerId(g.handle().script_url.clone()))
.collect();
self.dedicated_worker_scopes
.retain(|id, _| active_ids.contains(id));
self.web_workers.retain(|id, _| active_ids.contains(id));
}
pub fn active_worker_count(&self) -> usize {
self.active_workers
.iter()
.filter(|g| !g.handle().is_terminated())
.count()
}
pub fn terminate_worker_via_path(
&mut self,
worker_id: &WorkerId,
path: WorkerTeardownPath,
) -> Option<WorkerTeardownResult> {
let guard_idx = self
.active_workers
.iter()
.position(|g| &WorkerId(g.handle().script_url.clone()) == worker_id)?;
let guard = &mut self.active_workers[guard_idx];
guard.terminate_via(path.clone());
let realm_unregistered = if guard.handle().worker_global_addr() != 0 {
guard.handle().unregister_stealth_profile();
true
} else {
false
};
guard.handle().mark_terminated();
let thread_joined = if self.web_workers.contains_key(worker_id) {
self.web_workers.remove(worker_id);
true
} else {
true
};
let never_registered = guard.handle().worker_global_addr() == 0;
self.worker_channels.remove(worker_id);
self.dedicated_worker_scopes.remove(worker_id);
self.worker_script_load_states.remove(worker_id);
Some(WorkerTeardownResult {
path,
thread_joined,
realm_profile_unregistered: realm_unregistered,
closing_flag_set: true,
never_registered,
})
}
pub fn register_dedicated_worker_scope(
&mut self,
worker_id: WorkerId,
scope: DedicatedWorkerGlobalScopeState,
) {
self.dedicated_worker_scopes.insert(worker_id, scope);
}
pub fn register_web_worker(&mut self, worker_id: WorkerId, handle: WorkerHandle) {
self.web_workers.insert(worker_id, handle);
}
pub fn web_worker(&self, worker_id: &WorkerId) -> Option<&WorkerHandle> {
self.web_workers.get(worker_id)
}
pub fn dedicated_worker_scope(
&self,
worker_id: &WorkerId,
) -> Option<&DedicatedWorkerGlobalScopeState> {
self.dedicated_worker_scopes.get(worker_id)
}
pub fn dedicated_worker_scope_mut(
&mut self,
worker_id: &WorkerId,
) -> Option<&mut DedicatedWorkerGlobalScopeState> {
self.dedicated_worker_scopes.get_mut(worker_id)
}
pub fn remove_dedicated_worker_scope(
&mut self,
worker_id: &WorkerId,
) -> Option<DedicatedWorkerGlobalScopeState> {
self.dedicated_worker_scopes.remove(worker_id)
}
pub fn dedicated_worker_scope_count(&self) -> usize {
self.dedicated_worker_scopes.len()
}
pub fn dedicated_worker_scopes(&self) -> Vec<&DedicatedWorkerGlobalScopeState> {
self.dedicated_worker_scopes.values().collect()
}
pub fn dedicated_worker_scope_by_url(
&self,
url: &str,
) -> Option<&DedicatedWorkerGlobalScopeState> {
self.dedicated_worker_scopes
.values()
.find(|scope| scope.worker_id.0 == url)
}
pub fn register_worker_script_load_state(
&mut self,
worker_id: WorkerId,
state: WorkerScriptLoadState,
) {
self.worker_script_load_states.insert(worker_id, state);
}
pub fn update_worker_script_load_state(
&mut self,
worker_id: &WorkerId,
state: WorkerScriptLoadState,
) {
if let Some(current) = self.worker_script_load_states.get_mut(worker_id) {
*current = state;
}
}
pub fn worker_script_load_state(&self, worker_id: &WorkerId) -> Option<&WorkerScriptLoadState> {
self.worker_script_load_states.get(worker_id)
}
pub fn remove_worker_script_load_state(
&mut self,
worker_id: &WorkerId,
) -> Option<WorkerScriptLoadState> {
self.worker_script_load_states.remove(worker_id)
}
pub fn worker_script_load_state_count(&self) -> usize {
self.worker_script_load_states.len()
}
fn reap_terminated_worker_script_load_states(&mut self) {
let active_ids: std::collections::HashSet<WorkerId> = self
.active_workers
.iter()
.map(|g| WorkerId(g.handle().script_url.clone()))
.collect();
self.worker_script_load_states
.retain(|id, _| active_ids.contains(id));
}
pub fn worker_lifecycle_states(&self) -> Vec<(WorkerId, WorkerLifecycleState)> {
self.active_workers
.iter()
.map(|g| {
let id = WorkerId(g.handle().script_url.clone());
(id, g.lifecycle_state())
})
.collect()
}
pub fn set_worker_scope_config(&mut self, config: WorkerScopeConfig) {
self.worker_scope_config = config;
}
pub fn forward_worker_message_event(&self, event: WorkerMessageEvent) {
if let Some(ref tx) = self.event_tx {
let direction = match event.direction {
WorkerMessageDirection::PageToWorker => "page→worker",
WorkerMessageDirection::WorkerToPage => "worker→page",
};
let _ = tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: ConsoleLevel::Debug,
text: format!("[Worker] postMessage {}: {}", direction, event.worker_id.0),
url: None,
line: None,
column: None,
});
}
}
pub fn forward_worker_structured_message(&self, msg: &WorkerStructuredMessage) {
if let Some(ref tx) = self.event_tx {
let direction = match msg.direction {
WorkerMessageDirection::PageToWorker => "page→worker",
WorkerMessageDirection::WorkerToPage => "worker→page",
};
let payload_info = match &msg.payload {
Some(p) => format!(
"{} bytes, {} transferable(s)",
p.data.len(),
p.transferable_count
),
None => "metadata-only (servo handles clone)".to_string(),
};
let _ = tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: ConsoleLevel::Debug,
text: format!(
"[Worker] postMessage #{} {}: {} [{}]",
msg.message_id, direction, msg.worker_id.0, payload_info
),
url: None,
line: None,
column: None,
});
}
}
pub fn forward_worker_error_event(&self, event: WorkerErrorEvent) {
if let Some(ref tx) = self.event_tx {
let _ = tx.send(ServoEvent::PageError {
target_id: "0".to_string(),
text: format!("[Worker] {}: {}", event.worker_id.0, event.message),
url: Some(event.filename.clone()),
line: Some(event.lineno),
column: Some(event.colno),
stack: None,
});
}
}
pub fn register_worker_channel(&mut self, bridge: WorkerChannelBridge) {
let id = bridge.worker_id.clone();
self.worker_channels.insert(id, bridge);
}
pub fn create_worker_channel(&mut self, worker_id: WorkerId) -> WorkerChannelEndpoints {
let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
self.worker_channels
.insert(bridge.worker_id.clone(), bridge);
endpoints
}
pub fn remove_worker_channel(&mut self, worker_id: &WorkerId) -> Option<WorkerChannelBridge> {
self.worker_channels.remove(worker_id)
}
pub fn worker_channel(&self, worker_id: &WorkerId) -> Option<&WorkerChannelBridge> {
self.worker_channels.get(worker_id)
}
pub fn post_to_worker(
&self,
worker_id: &WorkerId,
payload: StructuredClonePayload,
) -> Result<(), String> {
match self.worker_channels.get(worker_id) {
Some(bridge) => bridge
.post_message_to_worker(payload)
.map_err(|e| format!("Worker channel closed: {}", e)),
None => Err(format!("No channel bridge for worker: {}", worker_id.0)),
}
}
pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<WorkerId>) {
let mut all_messages = Vec::new();
let mut disconnected_workers = Vec::new();
for (id, bridge) in &self.worker_channels {
let result = bridge.drain_worker_messages();
all_messages.extend(result.messages);
if result.disconnected {
disconnected_workers.push(id.clone());
}
}
(all_messages, disconnected_workers)
}
pub fn drain_and_forward_worker_messages(&self) -> Vec<WorkerId> {
let (messages, disconnected) = self.drain_all_worker_messages();
for msg in &messages {
self.forward_worker_structured_message(msg);
}
disconnected
}
pub fn reap_terminated_worker_channels(&mut self) {
let active_ids: std::collections::HashSet<WorkerId> = self
.active_workers
.iter()
.map(|g| WorkerId(g.handle().script_url.clone()))
.collect();
self.worker_channels.retain(|id, _| active_ids.contains(id));
}
pub fn worker_channel_count(&self) -> usize {
self.worker_channels.len()
}
pub fn track_shared_worker_port(&mut self, port_ref: SharedWorkerPortRef) {
self.shared_worker_ports.push(port_ref);
}
pub fn disconnect_shared_worker_ports(&mut self) {
if !self.shared_worker_ports.is_empty() {
log::debug!(
"[delegate] page navigation: disconnecting {} shared worker ports",
self.shared_worker_ports.len()
);
}
self.shared_worker_ports.clear();
self.shared_worker_channels.clear();
self.shared_worker_scopes.clear();
}
pub fn shared_worker_port_count(&self) -> usize {
self.shared_worker_ports.len()
}
pub fn forward_shared_worker_connect_event(&self, event: SharedWorkerConnectEvent) {
if let Some(ref tx) = self.event_tx {
let _ = tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: ConsoleLevel::Debug,
text: format!(
"[SharedWorker] connect: {} (name={}) from {}",
event.shared_worker_id.script_url,
if event.shared_worker_id.name.is_empty() {
"<default>"
} else {
&event.shared_worker_id.name
},
event.page_url
),
url: None,
line: None,
column: None,
});
}
}
pub fn register_shared_worker_channel(&mut self, bridge: SharedWorkerChannelBridge) {
let id = bridge.shared_worker_id.clone();
self.shared_worker_channels.insert(id, bridge);
}
pub fn create_shared_worker_channel(&mut self, shared_worker_id: SharedWorkerId) {
let bridge = SharedWorkerChannelBridge::new(shared_worker_id.clone());
self.shared_worker_channels.insert(shared_worker_id, bridge);
}
pub fn add_shared_worker_port(
&mut self,
shared_worker_id: SharedWorkerId,
) -> SharedWorkerPortEndpoints {
if !self.shared_worker_channels.contains_key(&shared_worker_id) {
self.create_shared_worker_channel(shared_worker_id.clone());
}
self.shared_worker_channels
.get_mut(&shared_worker_id)
.expect("just created")
.add_port()
}
pub fn shared_worker_channel(&self, id: &SharedWorkerId) -> Option<&SharedWorkerChannelBridge> {
self.shared_worker_channels.get(id)
}
pub fn remove_shared_worker_channel(
&mut self,
id: &SharedWorkerId,
) -> Option<SharedWorkerChannelBridge> {
self.shared_worker_channels.remove(id)
}
pub fn drain_all_shared_worker_messages(
&self,
) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
let mut all_messages = Vec::new();
let mut all_disconnected = Vec::new();
for (_, bridge) in &self.shared_worker_channels {
let (messages, disconnected) = bridge.drain_all_worker_messages();
all_messages.extend(messages);
all_disconnected.extend(disconnected);
}
(all_messages, all_disconnected)
}
pub fn drain_and_forward_shared_worker_messages(&self) -> Vec<SharedWorkerId> {
let (messages, disconnected) = self.drain_all_shared_worker_messages();
for msg in &messages {
self.forward_worker_structured_message(&msg);
}
disconnected
}
pub fn post_to_worker_via_shared_port(
&self,
id: &SharedWorkerId,
port_index: usize,
payload: StructuredClonePayload,
) -> Result<(), String> {
match self.shared_worker_channels.get(id) {
Some(bridge) => bridge.post_to_worker_from_port(port_index, payload),
None => Err(format!(
"No channel bridge for SharedWorker: {}:{}",
id.script_url, id.name
)),
}
}
pub fn reap_disconnected_shared_worker_ports(&mut self) {
for (_, bridge) in &mut self.shared_worker_channels {
bridge.remove_disconnected_ports();
}
self.shared_worker_channels
.retain(|_, bridge| bridge.port_count() > 0);
}
pub fn shared_worker_channel_count(&self) -> usize {
self.shared_worker_channels
.values()
.map(|b| b.port_count())
.sum()
}
pub fn register_shared_worker_scope(
&mut self,
id: SharedWorkerId,
scope: SharedWorkerGlobalScopeState,
) {
self.shared_worker_scopes.insert(id, scope);
}
pub fn shared_worker_scope(
&self,
id: &SharedWorkerId,
) -> Option<&SharedWorkerGlobalScopeState> {
self.shared_worker_scopes.get(id)
}
pub fn shared_worker_scope_mut(
&mut self,
id: &SharedWorkerId,
) -> Option<&mut SharedWorkerGlobalScopeState> {
self.shared_worker_scopes.get_mut(id)
}
pub fn remove_shared_worker_scope(
&mut self,
id: &SharedWorkerId,
) -> Option<SharedWorkerGlobalScopeState> {
self.shared_worker_scopes.remove(id)
}
pub fn shared_worker_scope_count(&self) -> usize {
self.shared_worker_scopes.len()
}
pub fn shared_worker_scopes(&self) -> Vec<&SharedWorkerGlobalScopeState> {
self.shared_worker_scopes.values().collect()
}
pub fn shared_worker_scope_by_script_url(
&self,
script_url: &str,
) -> Option<&SharedWorkerGlobalScopeState> {
self.shared_worker_scopes
.values()
.find(|scope| scope.shared_worker_id.script_url == script_url)
}
pub fn set_shared_worker_scope_config(
&mut self,
shared_worker_id: &SharedWorkerId,
config: &SharedWorkerScopeConfig,
) {
if let Some(scope) = self.shared_worker_scopes.get_mut(shared_worker_id) {
scope.scope.navigator = WorkerNavigator::from_shared_scope_config(config);
}
}
pub fn set_controlling_service_worker(&mut self, handle: ServiceWorkerHandle) {
self.controlled_service_worker = Some(handle);
}
pub fn clear_controlling_service_worker(&mut self) {
self.controlled_service_worker = None;
self.service_worker_scope = None;
}
pub fn controlling_service_worker(&self) -> Option<&ServiceWorkerHandle> {
self.controlled_service_worker.as_ref()
}
pub fn is_controlled_by_service_worker(&self) -> bool {
self.controlled_service_worker.is_some()
}
pub fn is_url_in_service_worker_scope(&self, url: &str) -> bool {
self.service_worker_scope
.as_ref()
.map(|scope| scope.is_url_in_scope(url))
.unwrap_or(false)
}
pub fn register_service_worker_scope(&mut self, scope: ServiceWorkerGlobalScopeState) {
self.service_worker_scope = Some(scope);
}
pub fn service_worker_scope(&self) -> Option<&ServiceWorkerGlobalScopeState> {
self.service_worker_scope.as_ref()
}
pub fn service_worker_scope_mut(&mut self) -> Option<&mut ServiceWorkerGlobalScopeState> {
self.service_worker_scope.as_mut()
}
pub fn remove_service_worker_scope(&mut self) -> Option<ServiceWorkerGlobalScopeState> {
self.service_worker_scope.take()
}
pub fn forward_service_worker_fetch_event(&self, event: ServiceWorkerFetchEvent) {
if let Some(ref tx) = self.event_tx {
let stealth_status = if event.stealth_profile_applied {
"stealth profile applied"
} else {
"⚠️ STEALTH BOUNDARY VIOLATION"
};
let _ = tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: if event.stealth_profile_applied {
ConsoleLevel::Debug
} else {
ConsoleLevel::Warning
},
text: format!(
"[ServiceWorker] fetch {} {} -> {} ({})",
event.method,
event.request_url,
event.registration_id.script_url,
stealth_status
),
url: None,
line: None,
column: None,
});
}
}
pub fn set_service_worker_scope_config(&mut self, config: &ServiceWorkerScopeConfig) {
if let Some(scope) = &mut self.service_worker_scope {
scope.scope.navigator = WorkerNavigator::from_service_scope_config(config);
}
}
}
pub struct BaoServoDelegate {
last_error: RefCell<Option<String>>,
console_log_tx: RefCell<Option<std::sync::mpsc::Sender<ConsoleMessage>>>,
event_tx: RefCell<Option<Sender<ServoEvent>>>,
shared_workers: RefCell<Vec<SharedWorkerHandle>>,
service_workers: RefCell<Vec<ServiceWorkerHandle>>,
}
impl Default for BaoServoDelegate {
fn default() -> Self {
BaoServoDelegate {
last_error: RefCell::new(None),
console_log_tx: RefCell::new(None),
event_tx: RefCell::new(None),
shared_workers: RefCell::new(Vec::new()),
service_workers: RefCell::new(Vec::new()),
}
}
}
impl BaoServoDelegate {
pub fn new() -> Self {
Self::default()
}
pub fn last_error(&self) -> Option<String> {
self.last_error.borrow().clone()
}
pub fn set_console_log_tx(&self, tx: std::sync::mpsc::Sender<ConsoleMessage>) {
*self.console_log_tx.borrow_mut() = Some(tx);
}
pub fn console_log_tx(&self) -> Option<std::sync::mpsc::Sender<ConsoleMessage>> {
self.console_log_tx.borrow().clone()
}
pub fn set_event_tx(&self, tx: Sender<ServoEvent>) {
*self.event_tx.borrow_mut() = Some(tx);
}
pub fn event_tx(&self) -> Option<Sender<ServoEvent>> {
self.event_tx.borrow().clone()
}
pub fn register_shared_worker(&self, handle: SharedWorkerHandle) -> SharedWorkerHandle {
let id = handle.id();
let mut shared_workers = self.shared_workers.borrow_mut();
if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
existing.clone()
} else {
shared_workers.push(handle.clone());
handle
}
}
pub fn find_shared_worker(&self, script_url: &str, name: &str) -> Option<SharedWorkerHandle> {
self.shared_workers
.borrow()
.iter()
.find(|h| h.script_url == script_url && h.name == name)
.cloned()
}
pub fn reap_terminated_shared_workers(&self) {
self.shared_workers
.borrow_mut()
.retain(|h| !h.is_terminated() || h.connected_page_count() > 0);
}
pub fn shared_worker_count(&self) -> usize {
self.shared_workers.borrow().len()
}
pub fn route_shared_worker(&self, handle: SharedWorkerHandle) -> (SharedWorkerHandle, bool) {
let id = handle.id();
let mut shared_workers = self.shared_workers.borrow_mut();
if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
(existing.clone(), false)
} else {
shared_workers.push(handle.clone());
(handle, true)
}
}
pub fn get_or_create_shared_worker(
&self,
script_url: &str,
name: &str,
) -> (SharedWorkerHandle, bool) {
if let Some(existing) = self.find_shared_worker(script_url, name) {
(existing, false)
} else {
let handle = SharedWorkerHandle::new(script_url.to_string(), name.to_string());
let returned = self.register_shared_worker(handle);
(returned, true)
}
}
pub fn unregister_shared_worker(&self, id: &SharedWorkerId) -> bool {
let mut shared_workers = self.shared_workers.borrow_mut();
let before = shared_workers.len();
shared_workers.retain(|h| &h.id() != id);
shared_workers.len() < before
}
pub fn all_shared_workers(&self) -> Vec<SharedWorkerHandle> {
self.shared_workers.borrow().iter().cloned().collect()
}
pub fn register_service_worker(&self, handle: ServiceWorkerHandle) -> ServiceWorkerHandle {
let id = handle.id();
let mut service_workers = self.service_workers.borrow_mut();
if let Some(existing) = service_workers.iter().find(|h| h.id() == id) {
existing.clone()
} else {
service_workers.push(handle.clone());
handle
}
}
pub fn find_service_worker(
&self,
script_url: &str,
scope: &str,
) -> Option<ServiceWorkerHandle> {
self.service_workers
.borrow()
.iter()
.find(|h| h.script_url == script_url && h.scope == scope)
.cloned()
}
pub fn find_service_worker_for_url(&self, url: &str) -> Option<ServiceWorkerHandle> {
self.service_workers
.borrow()
.iter()
.filter(|h| h.is_intercepting_fetch())
.find(|h| url.starts_with(&h.scope))
.cloned()
}
pub fn reap_terminated_service_workers(&self) {
self.service_workers
.borrow_mut()
.retain(|h| !h.is_terminated());
}
pub fn service_worker_count(&self) -> usize {
self.service_workers.borrow().len()
}
pub fn unregister_service_worker(&self, id: &ServiceWorkerRegistrationId) -> bool {
let mut service_workers = self.service_workers.borrow_mut();
let before = service_workers.len();
service_workers.retain(|h| &h.id() != id);
service_workers.len() < before
}
pub fn get_or_create_service_worker(
&self,
script_url: &str,
scope: &str,
stealth_profile: Option<bao_stealth::StealthProfile>,
) -> (ServiceWorkerHandle, bool) {
if let Some(existing) = self.find_service_worker(script_url, scope) {
(existing, false)
} else {
let handle = ServiceWorkerHandle::new(
script_url.to_string(),
scope.to_string(),
stealth_profile,
);
let returned = self.register_service_worker(handle);
(returned, true)
}
}
pub fn all_service_workers(&self) -> Vec<ServiceWorkerHandle> {
self.service_workers.borrow().iter().cloned().collect()
}
pub fn verify_service_worker_stealth_consistency(
&self,
page_stealth_profile: &bao_stealth::StealthProfile,
) -> Vec<ServiceWorkerRegistrationId> {
self.service_workers
.borrow()
.iter()
.filter(|h| h.is_intercepting_fetch())
.filter(|h| {
match &h.stealth_profile {
Some(sw_profile) => {
sw_profile.navigator.user_agent != page_stealth_profile.navigator.user_agent
|| sw_profile.navigator.platform
!= page_stealth_profile.navigator.platform
}
None => {
true
}
}
})
.map(|h| h.id())
.collect()
}
}
impl ServoDelegate for BaoServoDelegate {
fn notify_error(&self, error: ServoError) {
let error_str = format!("{error:?}");
*self.last_error.borrow_mut() = Some(error_str.clone());
if error_str.to_lowercase().contains("certificate")
|| error_str.to_lowercase().contains("tls")
{
if let Some(ref tx) = *self.console_log_tx.borrow() {
let _ = tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
event_id: 0,
error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
url: String::new(),
}));
}
}
}
fn show_console_message(&self, level: ConsoleLogLevel, message: String) {
let level_str = match level {
ConsoleLogLevel::Debug => "debug",
ConsoleLogLevel::Log => "info",
ConsoleLogLevel::Info => "info",
ConsoleLogLevel::Warn => "warning",
ConsoleLogLevel::Error => "error",
ConsoleLogLevel::Trace => "verbose",
ConsoleLogLevel::Dir => "info",
};
log::trace!("[servo] {message}");
let event_tx = self.event_tx.borrow();
if let Some(ref tx) = *event_tx {
let servo_level = match level {
ConsoleLogLevel::Debug => ConsoleLevel::Debug,
ConsoleLogLevel::Log => ConsoleLevel::Info,
ConsoleLogLevel::Info => ConsoleLevel::Info,
ConsoleLogLevel::Warn => ConsoleLevel::Warning,
ConsoleLogLevel::Error => ConsoleLevel::Error,
ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
ConsoleLogLevel::Dir => ConsoleLevel::Info,
};
let _ = tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: servo_level,
text: message,
url: None,
line: None,
column: None,
});
} else if let Some(ref tx) = *self.console_log_tx.borrow() {
let msg = match BaoEvent::from_console_text(&message) {
Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
_ => ConsoleMessage::Log {
level: level_str.to_string(),
text: message,
},
};
let _ = tx.send(msg);
}
}
fn request_devtools_connection(&self, request: AllowOrDenyRequest) {
request.allow();
}
}
pub struct BaoWebViewDelegate {
state: Rc<RefCell<BaoWebViewState>>,
viewport: PhysicalSize<u32>,
}
impl BaoWebViewDelegate {
pub fn new(state: Rc<RefCell<BaoWebViewState>>, viewport: PhysicalSize<u32>) -> Self {
BaoWebViewDelegate { state, viewport }
}
pub fn state(&self) -> &Rc<RefCell<BaoWebViewState>> {
&self.state
}
}
impl WebViewDelegate for BaoWebViewDelegate {
fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
let screen_size =
DeviceIntSize::new(self.viewport.width as i32, self.viewport.height as i32);
Some(ScreenGeometry {
size: screen_size,
available_size: screen_size,
window_rect: DeviceIntRect::from_origin_and_size(DeviceIntPoint::zero(), screen_size),
})
}
fn notify_url_changed(&self, _webview: WebView, url: url::Url) {
let url_str = url.to_string();
self.state.borrow_mut().url = Some(url);
let event_tx = self.state.borrow().event_tx.clone();
if let Some(ref tx) = event_tx {
let _ = tx.send(ServoEvent::FrameNavigated {
target_id: "0".to_string(),
frame_id: "0".to_string(),
url: url_str,
name: None,
});
} else if let Some(ref tx) = self.state.borrow().console_log_tx {
let loader_id = format!("{:016x}", url_str.len() as u64);
let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
frame_id: "0".to_string(),
url: url_str,
loader_id,
}));
}
}
fn notify_page_title_changed(&self, _webview: WebView, title: Option<String>) {
self.state.borrow_mut().title = title;
}
fn notify_load_status_changed(&self, _webview: WebView, status: LoadStatus) {
self.state.borrow_mut().load_status = status;
match status {
LoadStatus::Started => {
{
let mut state = self.state.borrow_mut();
if !state.active_workers.is_empty() {
log::debug!(
"[delegate] page navigation: terminating {} active workers",
state.active_worker_count()
);
state.terminate_all_workers();
}
state.disconnect_shared_worker_ports();
state.clear_controlling_service_worker();
}
let event_tx = self.state.borrow().event_tx.clone();
if let Some(ref tx) = event_tx {
let _ = tx.send(ServoEvent::FrameStartedLoading {
target_id: "0".to_string(),
frame_id: "0".to_string(),
});
}
}
LoadStatus::Complete => {
self.state.borrow_mut().dom_proxies_dirty = true;
self.state.borrow_mut().reap_terminated_workers();
let event_tx = self.state.borrow().event_tx.clone();
if let Some(ref tx) = event_tx {
let _ = tx.send(ServoEvent::FrameStoppedLoading {
target_id: "0".to_string(),
frame_id: "0".to_string(),
});
} else if let Some(ref tx) = self.state.borrow().console_log_tx {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs_f64();
let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageLoadEventFired {
timestamp,
}));
}
}
LoadStatus::HeadParsed => {}
}
}
fn notify_new_frame_ready(&self, _webview: WebView) {
self.state.borrow_mut().frame_ready = true;
}
fn request_navigation(&self, _webview: WebView, request: NavigationRequest) {
request.allow();
}
fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
request.allow();
}
fn request_create_new(&self, _parent_webview: WebView, _request: CreateNewWebViewRequest) {}
fn show_console_message(&self, _webview: WebView, level: ConsoleLogLevel, message: String) {
let level_str = match level {
ConsoleLogLevel::Debug => "debug",
ConsoleLogLevel::Log => "info",
ConsoleLogLevel::Info => "info",
ConsoleLogLevel::Warn => "warning",
ConsoleLogLevel::Error => "error",
ConsoleLogLevel::Trace => "verbose",
ConsoleLogLevel::Dir => "info",
};
log::trace!("[webview] {message}");
let event_tx = self.state.borrow().event_tx.clone();
if let Some(ref tx) = event_tx {
let servo_level = match level {
ConsoleLogLevel::Debug => ConsoleLevel::Debug,
ConsoleLogLevel::Log => ConsoleLevel::Info,
ConsoleLogLevel::Info => ConsoleLevel::Info,
ConsoleLogLevel::Warn => ConsoleLevel::Warning,
ConsoleLogLevel::Error => ConsoleLevel::Error,
ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
ConsoleLogLevel::Dir => ConsoleLevel::Info,
};
let _ = tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: servo_level,
text: message,
url: None,
line: None,
column: None,
});
} else if let Some(ref tx) = self.state.borrow().console_log_tx {
let msg = match BaoEvent::from_console_text(&message) {
Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
_ => ConsoleMessage::Log {
level: level_str.to_string(),
text: message,
},
};
let _ = tx.send(msg);
}
}
fn show_embedder_control(&self, _webview: WebView, _control: EmbedderControl) {}
fn hide_embedder_control(&self, _webview: WebView, _id: EmbedderControlId) {}
fn notify_crashed(&self, _webview: WebView, reason: String, _backtrace: Option<String>) {
log::error!("[webview] crashed: {reason}");
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_webview_state_default() {
let state = BaoWebViewState::default();
assert!(state.url.is_none());
assert!(state.title.is_none());
assert!(matches!(state.load_status, LoadStatus::Started));
assert!(!state.frame_ready);
assert!(!state.dom_proxies_dirty);
}
#[test]
fn test_webview_state_url_mutate() {
let mut state = BaoWebViewState::default();
state.url = Some(url::Url::parse("https://example.com").unwrap());
assert!(state.url.is_some());
assert_eq!(state.url.unwrap().as_str(), "https://example.com/");
}
#[test]
fn test_webview_state_title_mutate() {
let mut state = BaoWebViewState::default();
state.title = Some("Test Page".to_string());
assert_eq!(state.title.as_deref(), Some("Test Page"));
}
#[test]
fn test_webview_state_frame_ready_toggle() {
let mut state = BaoWebViewState::default();
assert!(!state.frame_ready);
state.frame_ready = true;
assert!(state.frame_ready);
}
#[test]
fn test_servo_delegate_new_no_error() {
let delegate = BaoServoDelegate::new();
assert!(delegate.last_error().is_none());
}
#[test]
fn test_servo_delegate_default_no_error() {
let delegate = BaoServoDelegate::default();
assert!(delegate.last_error().is_none());
}
#[test]
fn test_webview_delegate_new_with_state() {
let state = Rc::new(RefCell::new(BaoWebViewState::default()));
let viewport = PhysicalSize::new(1024, 768);
let delegate = BaoWebViewDelegate::new(state, viewport);
assert!(delegate.state().borrow().url.is_none());
}
#[test]
fn test_webview_delegate_state_rc_shared() {
let state = Rc::new(RefCell::new(BaoWebViewState::default()));
let viewport = PhysicalSize::new(800, 600);
let delegate = BaoWebViewDelegate::new(Rc::clone(&state), viewport);
state.borrow_mut().title = Some("External".to_string());
assert_eq!(delegate.state().borrow().title.as_deref(), Some("External"));
}
#[test]
fn test_webview_delegate_viewport_size() {
let state = Rc::new(RefCell::new(BaoWebViewState::default()));
let viewport = PhysicalSize::new(1440, 900);
let delegate = BaoWebViewDelegate::new(state, viewport);
assert!(delegate.state().borrow().url.is_none());
}
#[test]
fn test_pool_stats_fields() {
let stats = crate::page_pool::PoolStats {
active: 3,
idle: 1,
total_created: 5,
total_destroyed: 2,
};
assert_eq!(stats.active, 3);
assert_eq!(stats.idle, 1);
assert_eq!(stats.total_created, 5);
assert_eq!(stats.total_destroyed, 2);
}
#[test]
fn test_dom_proxies_dirty_default_false() {
let state = BaoWebViewState::default();
assert!(!state.dom_proxies_dirty);
}
#[test]
fn test_dom_proxies_dirty_set_on_complete() {
let mut state = BaoWebViewState::default();
state.load_status = LoadStatus::Complete;
state.dom_proxies_dirty = true;
assert!(state.dom_proxies_dirty);
}
#[test]
fn test_dom_proxies_dirty_clear_after_refresh() {
let mut state = BaoWebViewState::default();
state.dom_proxies_dirty = true;
state.dom_proxies_dirty = false;
assert!(!state.dom_proxies_dirty);
}
#[test]
fn test_servo_delegate_console_log_channel_set_and_get() {
let delegate = BaoServoDelegate::new();
assert!(delegate.console_log_tx().is_none());
let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
delegate.set_console_log_tx(tx);
assert!(delegate.console_log_tx().is_some());
}
#[test]
fn test_servo_delegate_console_log_tx_clones() {
let delegate = BaoServoDelegate::new();
let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
delegate.set_console_log_tx(tx);
let cloned = delegate.console_log_tx().unwrap();
cloned
.send(ConsoleMessage::Log {
level: "info".into(),
text: "hello".into(),
})
.unwrap();
let msg = rx.try_recv().unwrap();
match msg {
ConsoleMessage::Log { level, text } => {
assert_eq!(level, "info");
assert_eq!(text, "hello");
}
ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
}
}
#[test]
fn test_webview_state_console_log_tx_propagation() {
let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
let mut state = BaoWebViewState::default();
state.console_log_tx = Some(tx);
if let Some(ref tx) = state.console_log_tx {
tx.send(ConsoleMessage::Log {
level: "warning".into(),
text: "test message".into(),
})
.unwrap();
}
let msg = rx.try_recv().unwrap();
match msg {
ConsoleMessage::Log { level, text } => {
assert_eq!(level, "warning");
assert_eq!(text, "test message");
}
ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
}
}
#[test]
fn test_webview_state_console_log_tx_default_none() {
let state = BaoWebViewState::default();
assert!(state.console_log_tx.is_none());
}
#[test]
fn test_console_log_all_level_mappings() {
let delegate = BaoServoDelegate::new();
let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
delegate.set_console_log_tx(tx);
let cases: Vec<(ConsoleLogLevel, &str)> = vec![
(ConsoleLogLevel::Debug, "debug"),
(ConsoleLogLevel::Log, "info"),
(ConsoleLogLevel::Info, "info"),
(ConsoleLogLevel::Warn, "warning"),
(ConsoleLogLevel::Error, "error"),
(ConsoleLogLevel::Trace, "verbose"),
(ConsoleLogLevel::Dir, "info"),
];
for (level, expected_str) in cases {
let mapped = match level {
ConsoleLogLevel::Debug => "debug",
ConsoleLogLevel::Log => "info",
ConsoleLogLevel::Info => "info",
ConsoleLogLevel::Warn => "warning",
ConsoleLogLevel::Error => "error",
ConsoleLogLevel::Trace => "verbose",
ConsoleLogLevel::Dir => "info",
};
assert_eq!(
mapped, expected_str,
"level {:?} should map to {}",
level, expected_str
);
}
}
#[test]
fn test_webview_delegate_console_log_forwarding() {
let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
let state = Rc::new(RefCell::new(BaoWebViewState {
console_log_tx: Some(tx),
..Default::default()
}));
let viewport = PhysicalSize::new(800, 600);
let _delegate = BaoWebViewDelegate::new(state, viewport);
if let Some(ref tx) = _delegate.state().borrow().console_log_tx {
tx.send(ConsoleMessage::Log {
level: "error".into(),
text: "crash!".into(),
})
.unwrap();
}
let msg = rx.try_recv().unwrap();
match msg {
ConsoleMessage::Log { level, text } => {
assert_eq!(level, "error");
assert_eq!(text, "crash!");
}
ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
}
}
#[test]
fn test_notify_url_changed_emits_frame_navigated() {
let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
let state = Rc::new(RefCell::new(BaoWebViewState {
console_log_tx: Some(tx),
..Default::default()
}));
let viewport = PhysicalSize::new(800, 600);
let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
let url = url::Url::parse("https://example.com").unwrap();
let url_str = url.to_string();
let loader_id = format!("{:016x}", url_str.len() as u64);
if let Some(ref tx) = state.borrow().console_log_tx {
tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
frame_id: "0".to_string(),
url: url_str.clone(),
loader_id: loader_id.clone(),
}))
.unwrap();
}
let msg = rx.try_recv().unwrap();
match msg {
ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
frame_id,
url,
loader_id: lid,
}) => {
assert_eq!(frame_id, "0");
assert!(url.starts_with("https://example.com"));
assert_eq!(lid, loader_id);
}
other => panic!("expected PageFrameNavigated, got {:?}", other),
}
}
#[test]
fn test_notify_error_certificate_error_emits_security_event() {
let delegate = BaoServoDelegate::new();
let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
delegate.set_console_log_tx(tx);
if let Some(ref tx) = *delegate.console_log_tx.borrow() {
tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
event_id: 0,
error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
url: String::new(),
}))
.unwrap();
}
let msg = rx.try_recv().unwrap();
match msg {
ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
event_id,
error_type,
url,
}) => {
assert_eq!(event_id, 0);
assert_eq!(error_type, "net::ERR_CERT_AUTHORITY_INVALID");
assert_eq!(url, "");
}
other => panic!("expected SecurityCertificateError, got {:?}", other),
}
}
#[test]
fn test_servo_delegate_event_tx_set_and_get() {
let delegate = BaoServoDelegate::new();
assert!(delegate.event_tx().is_none());
let (tx, _rx) = std::sync::mpsc::channel::<ServoEvent>();
delegate.set_event_tx(tx);
assert!(delegate.event_tx().is_some());
}
#[test]
fn test_servo_delegate_event_tx_sends_console_event() {
let delegate = BaoServoDelegate::new();
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
delegate.set_event_tx(tx);
if let Some(ref tx) = delegate.event_tx() {
tx.send(ServoEvent::Console {
target_id: "0".to_string(),
level: ConsoleLevel::Info,
text: "hello".to_string(),
url: None,
line: None,
column: None,
})
.unwrap();
}
let event = rx.try_recv().unwrap();
match event {
ServoEvent::Console { level, text, .. } => {
assert_eq!(level, ConsoleLevel::Info);
assert_eq!(text, "hello");
}
_ => panic!("expected Console event"),
}
}
#[test]
fn test_webview_state_event_tx_default_none() {
let state = BaoWebViewState::default();
assert!(state.event_tx.is_none());
}
#[test]
fn test_webview_state_event_tx_propagation() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let mut state = BaoWebViewState::default();
state.event_tx = Some(tx);
if let Some(ref tx) = state.event_tx {
tx.send(ServoEvent::FrameNavigated {
target_id: "0".to_string(),
frame_id: "0".to_string(),
url: "https://example.com/".to_string(),
name: None,
})
.unwrap();
}
let event = rx.try_recv().unwrap();
match event {
ServoEvent::FrameNavigated { url, .. } => {
assert_eq!(url, "https://example.com/");
}
_ => panic!("expected FrameNavigated event"),
}
}
#[test]
fn test_event_tx_console_level_mapping() {
let cases: Vec<(ConsoleLogLevel, ConsoleLevel)> = vec![
(ConsoleLogLevel::Debug, ConsoleLevel::Debug),
(ConsoleLogLevel::Log, ConsoleLevel::Info),
(ConsoleLogLevel::Info, ConsoleLevel::Info),
(ConsoleLogLevel::Warn, ConsoleLevel::Warning),
(ConsoleLogLevel::Error, ConsoleLevel::Error),
(ConsoleLogLevel::Trace, ConsoleLevel::Verbose),
];
for (servo_level, expected) in cases {
let mapped = match servo_level {
ConsoleLogLevel::Debug => ConsoleLevel::Debug,
ConsoleLogLevel::Log => ConsoleLevel::Info,
ConsoleLogLevel::Info => ConsoleLevel::Info,
ConsoleLogLevel::Warn => ConsoleLevel::Warning,
ConsoleLogLevel::Error => ConsoleLevel::Error,
ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
ConsoleLogLevel::Dir => ConsoleLevel::Info,
};
assert_eq!(
mapped, expected,
"servo {:?} should map to {:?}",
servo_level, expected
);
}
}
#[test]
fn test_notify_load_started_emits_frame_started_loading() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let state = Rc::new(RefCell::new(BaoWebViewState {
event_tx: Some(tx),
..Default::default()
}));
let viewport = PhysicalSize::new(800, 600);
let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
if let Some(ref tx) = state.borrow().event_tx {
tx.send(ServoEvent::FrameStartedLoading {
target_id: "0".to_string(),
frame_id: "0".to_string(),
})
.unwrap();
}
let event = rx.try_recv().unwrap();
match event {
ServoEvent::FrameStartedLoading {
target_id,
frame_id,
} => {
assert_eq!(target_id, "0");
assert_eq!(frame_id, "0");
}
_ => panic!("expected FrameStartedLoading event"),
}
}
#[test]
fn test_worker_handle_new_is_running() {
let handle = WorkerHandle::new("https://example.com/worker.js".to_string());
assert_eq!(handle.script_url, "https://example.com/worker.js");
assert!(!handle.is_closing());
assert!(!handle.is_terminated());
}
#[test]
fn test_worker_handle_terminate_sets_closing() {
let handle = WorkerHandle::new("worker.js".to_string());
assert!(!handle.is_closing());
handle.terminate();
assert!(handle.is_closing());
handle.terminate();
assert!(handle.is_closing());
}
#[test]
fn test_worker_handle_mark_terminated() {
let handle = WorkerHandle::new("worker.js".to_string());
assert!(!handle.is_terminated());
handle.mark_terminated();
assert!(handle.is_terminated());
}
#[test]
fn test_worker_handle_terminate_then_terminated() {
let handle = WorkerHandle::new("worker.js".to_string());
handle.terminate();
assert!(handle.is_closing());
assert!(!handle.is_terminated());
handle.mark_terminated();
assert!(handle.is_terminated());
}
#[test]
fn test_worker_handle_clone_shares_state() {
let handle = WorkerHandle::new("worker.js".to_string());
let clone = handle.clone();
handle.terminate();
assert!(
clone.is_closing(),
"clone should see closing flag from original"
);
clone.mark_terminated();
assert!(
handle.is_terminated(),
"original should see terminated flag from clone"
);
}
#[test]
fn test_webview_state_active_workers_default_empty() {
let state = BaoWebViewState::default();
assert!(state.active_workers.is_empty());
assert_eq!(state.active_worker_count(), 0);
}
#[test]
fn test_webview_state_track_worker() {
let mut state = BaoWebViewState::default();
let handle = WorkerHandle::new("worker1.js".to_string());
state.track_worker(handle);
assert_eq!(state.active_worker_count(), 1);
assert_eq!(state.active_workers.len(), 1);
assert_eq!(state.active_workers[0].handle().script_url, "worker1.js");
}
#[test]
fn test_webview_state_track_multiple_workers() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
state.track_worker(WorkerHandle::new("worker3.js".to_string()));
assert_eq!(state.active_worker_count(), 3);
}
#[test]
fn test_webview_state_terminate_all_workers() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
assert!(!state.active_workers[0].handle().is_closing());
assert!(!state.active_workers[1].handle().is_closing());
state.terminate_all_workers();
assert!(state.active_workers[0].handle().is_closing());
assert!(state.active_workers[1].handle().is_closing());
}
#[test]
fn test_webview_state_reap_terminated_workers() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
state.active_workers[0].handle().terminate();
state.active_workers[0].handle().mark_terminated();
assert_eq!(state.active_worker_count(), 1);
state.reap_terminated_workers();
assert_eq!(state.active_workers.len(), 1);
assert_eq!(state.active_workers[0].handle().script_url, "worker2.js");
}
#[test]
fn test_webview_state_reap_all_terminated() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.terminate_all_workers();
state.reap_terminated_workers();
assert!(state.active_workers.is_empty());
assert_eq!(state.active_worker_count(), 0);
}
#[test]
fn test_worker_id_equality() {
let id1 = WorkerId("worker1.js".to_string());
let id2 = WorkerId("worker1.js".to_string());
let id3 = WorkerId("worker2.js".to_string());
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_worker_message_direction() {
assert_eq!(
WorkerMessageDirection::PageToWorker,
WorkerMessageDirection::PageToWorker
);
assert_ne!(
WorkerMessageDirection::PageToWorker,
WorkerMessageDirection::WorkerToPage
);
}
#[test]
fn test_worker_message_event_creation() {
let event = WorkerMessageEvent {
worker_id: WorkerId("worker1.js".to_string()),
direction: WorkerMessageDirection::PageToWorker,
};
assert_eq!(event.worker_id.0, "worker1.js");
assert_eq!(event.direction, WorkerMessageDirection::PageToWorker);
}
#[test]
fn test_webview_state_forward_worker_message_to_event_tx() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let msg = WorkerMessageEvent {
worker_id: WorkerId("worker1.js".to_string()),
direction: WorkerMessageDirection::WorkerToPage,
};
state.forward_worker_message_event(msg);
let event = rx.try_recv().unwrap();
match event {
ServoEvent::Console { level, text, .. } => {
assert_eq!(level, ConsoleLevel::Debug);
assert!(text.contains("worker→page"));
assert!(text.contains("worker1.js"));
}
_ => panic!("expected Console event for worker message"),
}
}
#[test]
fn test_webview_state_forward_worker_message_no_event_tx() {
let state = BaoWebViewState::default();
let msg = WorkerMessageEvent {
worker_id: WorkerId("worker1.js".to_string()),
direction: WorkerMessageDirection::PageToWorker,
};
state.forward_worker_message_event(msg);
}
#[test]
fn test_terminate_on_navigation_then_reap() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
assert_eq!(state.active_worker_count(), 2);
state.terminate_all_workers();
assert!(state.active_workers[0].handle().is_closing());
assert!(state.active_workers[1].handle().is_closing());
assert_eq!(state.active_worker_count(), 0);
state.reap_terminated_workers();
assert!(state.active_workers.is_empty());
}
#[test]
fn test_worker_error_event_creation() {
let event = WorkerErrorEvent {
worker_id: WorkerId("worker1.js".to_string()),
message: "Uncaught TypeError: x is not a function".to_string(),
filename: "worker1.js".to_string(),
lineno: 42,
colno: 5,
};
assert_eq!(event.worker_id.0, "worker1.js");
assert_eq!(event.message, "Uncaught TypeError: x is not a function");
assert_eq!(event.filename, "worker1.js");
assert_eq!(event.lineno, 42);
assert_eq!(event.colno, 5);
}
#[test]
fn test_webview_state_forward_worker_error_to_event_tx() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let error = WorkerErrorEvent {
worker_id: WorkerId("worker1.js".to_string()),
message: "Uncaught Error: boom".to_string(),
filename: "worker1.js".to_string(),
lineno: 10,
colno: 3,
};
state.forward_worker_error_event(error);
let event = rx.try_recv().unwrap();
match event {
ServoEvent::PageError {
text,
url,
line,
column,
..
} => {
assert!(text.contains("worker1.js"));
assert!(text.contains("Uncaught Error: boom"));
assert_eq!(url.as_deref(), Some("worker1.js"));
assert_eq!(line, Some(10));
assert_eq!(column, Some(3));
}
_ => panic!("expected PageError event for worker error"),
}
}
#[test]
fn test_webview_state_forward_worker_error_no_event_tx() {
let state = BaoWebViewState::default();
let error = WorkerErrorEvent {
worker_id: WorkerId("worker1.js".to_string()),
message: "error".to_string(),
filename: "worker1.js".to_string(),
lineno: 1,
colno: 1,
};
state.forward_worker_error_event(error);
}
#[test]
fn test_worker_teardown_path_equality() {
assert_eq!(WorkerTeardownPath::Terminate, WorkerTeardownPath::Terminate);
assert_eq!(WorkerTeardownPath::SelfClose, WorkerTeardownPath::SelfClose);
assert_eq!(
WorkerTeardownPath::PageUnload,
WorkerTeardownPath::PageUnload
);
assert_ne!(WorkerTeardownPath::Terminate, WorkerTeardownPath::SelfClose);
}
#[test]
fn test_worker_lifecycle_state_running() {
let handle = WorkerHandle::new("worker.js".to_string());
let guard = AutoCloseWorker::new(handle);
assert_eq!(guard.lifecycle_state(), WorkerLifecycleState::Running);
}
#[test]
fn test_worker_lifecycle_state_closing() {
let handle = WorkerHandle::new("worker.js".to_string());
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::Terminate);
assert_eq!(
guard.lifecycle_state(),
WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
);
}
#[test]
fn test_worker_lifecycle_state_terminated() {
let handle = WorkerHandle::new("worker.js".to_string());
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::SelfClose);
guard.handle().mark_terminated();
assert_eq!(
guard.lifecycle_state(),
WorkerLifecycleState::Terminated(WorkerTeardownPath::SelfClose)
);
}
#[test]
fn test_worker_lifecycle_states_snapshot() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
let snapshot = state.worker_lifecycle_states();
assert_eq!(snapshot.len(), 2);
assert_eq!(snapshot[0].0, WorkerId("worker1.js".to_string()));
assert_eq!(snapshot[0].1, WorkerLifecycleState::Running);
assert_eq!(snapshot[1].0, WorkerId("worker2.js".to_string()));
assert_eq!(snapshot[1].1, WorkerLifecycleState::Running);
}
#[test]
fn test_auto_close_worker_new_is_running() {
let handle = WorkerHandle::new("worker.js".to_string());
let guard = AutoCloseWorker::new(handle);
assert!(!guard.handle().is_closing());
assert!(!guard.handle().is_terminated());
}
#[test]
fn test_auto_close_worker_terminate_via() {
let handle = WorkerHandle::new("worker.js".to_string());
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::Terminate);
assert!(guard.handle().is_closing());
assert_eq!(
guard.lifecycle_state(),
WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
);
}
#[test]
fn test_auto_close_worker_terminate_via_idempotent() {
let handle = WorkerHandle::new("worker.js".to_string());
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::Terminate);
guard.terminate_via(WorkerTeardownPath::SelfClose);
assert_eq!(
guard.lifecycle_state(),
WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
);
}
#[test]
fn test_auto_close_worker_drop_terminates() {
let handle = WorkerHandle::new("worker.js".to_string());
let handle_clone = handle.clone();
let guard = AutoCloseWorker::new(handle);
assert!(!handle_clone.is_closing());
drop(guard);
assert!(handle_clone.is_closing());
}
#[test]
fn test_auto_close_worker_drop_already_closing() {
let handle = WorkerHandle::new("worker.js".to_string());
let handle_clone = handle.clone();
let mut guard = AutoCloseWorker::new(handle);
guard.terminate_via(WorkerTeardownPath::Terminate);
drop(guard);
assert!(handle_clone.is_closing());
assert!(handle_clone.is_terminated());
}
#[test]
fn test_worker_handle_global_addr_default_zero() {
let handle = WorkerHandle::new("worker.js".to_string());
assert_eq!(handle.worker_global_addr(), 0);
}
#[test]
fn test_worker_handle_global_addr_set_and_get() {
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(0xDEADBEEF);
assert_eq!(handle.worker_global_addr(), 0xDEADBEEF);
}
#[test]
fn test_worker_handle_global_addr_arc_shared() {
let handle = WorkerHandle::new("worker.js".to_string());
let arc = handle.worker_global_addr_arc();
arc.store(0xCAFEBABE_usize as u64, Ordering::Release);
assert_eq!(handle.worker_global_addr(), 0xCAFEBABE);
}
#[test]
fn test_worker_handle_unregister_stealth_profile_no_addr() {
let handle = WorkerHandle::new("worker.js".to_string());
handle.unregister_stealth_profile();
}
#[test]
fn test_worker_handle_unregister_stealth_profile_with_addr() {
let fake_addr = 0x12345678_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
handle.unregister_stealth_profile();
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
bao_stealth::engine_props::clear_all_realm_profiles();
}
#[test]
fn test_teardown_result_crash_safe() {
let result = WorkerTeardownResult {
path: WorkerTeardownPath::Terminate,
thread_joined: true,
realm_profile_unregistered: true,
closing_flag_set: true,
never_registered: false,
};
assert!(result.is_crash_safe());
}
#[test]
fn test_teardown_result_not_crash_safe_no_join() {
let result = WorkerTeardownResult {
path: WorkerTeardownPath::PageUnload,
thread_joined: false,
realm_profile_unregistered: true,
closing_flag_set: true,
never_registered: false,
};
assert!(!result.is_crash_safe());
}
#[test]
fn test_teardown_result_not_crash_safe_no_closing() {
let result = WorkerTeardownResult {
path: WorkerTeardownPath::SelfClose,
thread_joined: true,
realm_profile_unregistered: true,
closing_flag_set: false,
never_registered: false,
};
assert!(!result.is_crash_safe());
}
#[test]
fn test_crash_safe_teardown_no_web_worker() {
let handle = WorkerHandle::new("worker.js".to_string());
let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::Terminate);
assert!(result.closing_flag_set);
assert!(result.thread_joined); assert!(!result.realm_profile_unregistered); assert!(handle.is_closing());
assert!(handle.is_terminated());
}
#[test]
fn test_crash_safe_teardown_with_stealth_profile() {
let fake_addr = 0xABCD0000_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::SelfClose);
assert!(result.closing_flag_set);
assert!(result.thread_joined);
assert!(result.realm_profile_unregistered);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
assert!(handle.is_closing());
assert!(handle.is_terminated());
}
#[test]
fn test_auto_close_worker_drop_unregisters_stealth_profile() {
let fake_addr = 0xBEEF0000_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
let guard = AutoCloseWorker::new(handle);
drop(guard);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
}
#[test]
fn test_terminate_all_workers_unregisters_stealth_profiles() {
let fake_addr1 = 0xAAAA0001_usize;
let fake_addr2 = 0xAAAA0002_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr1,
&bao_stealth::StealthProfile::firefox_default(),
);
bao_stealth::engine_props::set_profile_for_global(
fake_addr2,
&bao_stealth::StealthProfile::firefox_default(),
);
let mut state = BaoWebViewState::default();
let h1 = WorkerHandle::new("worker1.js".to_string());
h1.set_worker_global_addr(fake_addr1);
let h2 = WorkerHandle::new("worker2.js".to_string());
h2.set_worker_global_addr(fake_addr2);
state.track_worker(h1);
state.track_worker(h2);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_some());
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_some());
state.terminate_all_workers();
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_none());
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_none());
assert!(state.active_workers.iter().all(|g| g.handle().is_closing()));
assert!(state
.active_workers
.iter()
.all(|g| g.handle().is_terminated()));
}
#[test]
fn test_terminate_worker_via_path_terminate() {
let fake_addr = 0xCCCC0001_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let mut state = BaoWebViewState::default();
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let worker_id = WorkerId("worker.js".to_string());
state.track_worker(handle.clone());
let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.path, WorkerTeardownPath::Terminate);
assert!(result.closing_flag_set);
assert!(result.thread_joined);
assert!(result.realm_profile_unregistered);
assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
assert!(handle.is_closing());
assert!(handle.is_terminated());
}
#[test]
fn test_terminate_worker_via_path_self_close() {
let fake_addr = 0xDDDD0001_usize;
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let mut state = BaoWebViewState::default();
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let worker_id = WorkerId("worker.js".to_string());
state.track_worker(handle.clone());
let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::SelfClose);
assert!(result.is_some());
let result = result.unwrap();
assert_eq!(result.path, WorkerTeardownPath::SelfClose);
assert!(result.closing_flag_set);
assert!(result.realm_profile_unregistered);
}
#[test]
fn test_terminate_worker_via_path_not_found() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("nonexistent.js".to_string());
let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
assert!(result.is_none());
}
#[test]
fn test_three_paths_all_crash_safe() {
for path in [
WorkerTeardownPath::Terminate,
WorkerTeardownPath::SelfClose,
WorkerTeardownPath::PageUnload,
] {
let fake_addr = 0x12340000_usize
+ match &path {
WorkerTeardownPath::Terminate => 1,
WorkerTeardownPath::SelfClose => 2,
WorkerTeardownPath::PageUnload => 3,
};
bao_stealth::engine_props::set_profile_for_global(
fake_addr,
&bao_stealth::StealthProfile::firefox_default(),
);
let handle = WorkerHandle::new("worker.js".to_string());
handle.set_worker_global_addr(fake_addr);
let result = crash_safe_teardown_worker(&handle, path.clone());
assert!(
result.closing_flag_set,
"closing flag not set for {:?}",
path
);
assert!(result.thread_joined, "thread not joined for {:?}", path);
assert!(
result.realm_profile_unregistered,
"profile not unregistered for {:?}",
path
);
assert!(result.is_crash_safe(), "not crash-safe for {:?}", path);
assert!(handle.is_closing(), "handle not closing for {:?}", path);
assert!(
handle.is_terminated(),
"handle not terminated for {:?}",
path
);
assert!(
bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none(),
"profile not removed for {:?}",
path
);
}
}
#[test]
fn test_track_worker_guard() {
let handle = WorkerHandle::new("worker.js".to_string());
let guard = AutoCloseWorker::new(handle);
let mut state = BaoWebViewState::default();
state.track_worker_guard(guard);
assert_eq!(state.active_worker_count(), 1);
}
#[test]
fn test_worker_scope_config_default() {
let config = WorkerScopeConfig::default();
assert!(config.stealth_profile.is_none());
assert!(config.user_agent.is_empty());
assert!(config.platform.is_empty());
assert!(config.hardware_concurrency > 0);
assert_eq!(config.language, "en-US");
assert!(!config.languages.is_empty());
}
#[test]
fn test_worker_scope_config_set_on_state() {
let mut state = BaoWebViewState::default();
let config = WorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux x86_64".to_string(),
hardware_concurrency: 8,
language: "zh-CN".to_string(),
languages: vec!["zh-CN".to_string(), "zh".to_string(), "en".to_string()],
};
state.set_worker_scope_config(config);
assert_eq!(state.worker_scope_config.user_agent, "Bao/1.0");
assert_eq!(state.worker_scope_config.platform, "Linux x86_64");
assert_eq!(state.worker_scope_config.hardware_concurrency, 8);
assert_eq!(state.worker_scope_config.language, "zh-CN");
assert_eq!(state.worker_scope_config.languages.len(), 3);
}
#[test]
fn test_webview_state_default_worker_scope_config() {
let state = BaoWebViewState::default();
assert!(state.worker_scope_config.stealth_profile.is_none());
assert!(state.worker_scope_config.hardware_concurrency > 0);
}
#[test]
fn test_shared_worker_id_equality() {
let id1 = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "myworker".to_string(),
};
let id2 = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "myworker".to_string(),
};
let id3 = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "other".to_string(),
};
let id4 = SharedWorkerId {
script_url: "other.js".to_string(),
name: "myworker".to_string(),
};
assert_eq!(id1, id2);
assert_ne!(id1, id3); assert_ne!(id1, id4); }
#[test]
fn test_shared_worker_id_default_name() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
};
assert!(id.name.is_empty());
}
#[test]
fn test_shared_worker_handle_new_is_running() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
assert_eq!(handle.script_url, "sw.js");
assert_eq!(handle.name, "myname");
assert!(!handle.is_closing());
assert!(!handle.is_terminated());
assert_eq!(handle.connected_page_count(), 0);
}
#[test]
fn test_shared_worker_handle_id() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
let id = handle.id();
assert_eq!(id.script_url, "sw.js");
assert_eq!(id.name, "myname");
}
#[test]
fn test_shared_worker_handle_close() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
assert!(!handle.is_closing());
handle.close();
assert!(handle.is_closing());
handle.close();
assert!(handle.is_closing());
}
#[test]
fn test_shared_worker_handle_mark_terminated() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
assert!(!handle.is_terminated());
handle.mark_terminated();
assert!(handle.is_terminated());
}
#[test]
fn test_shared_worker_handle_connected_pages() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
assert_eq!(handle.connected_page_count(), 0);
handle.page_connected();
assert_eq!(handle.connected_page_count(), 1);
handle.page_connected();
assert_eq!(handle.connected_page_count(), 2);
handle.page_disconnected();
assert_eq!(handle.connected_page_count(), 1);
handle.page_disconnected();
assert_eq!(handle.connected_page_count(), 0);
}
#[test]
fn test_shared_worker_handle_clone_shares_state() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), "name".to_string());
let clone = handle.clone();
handle.close();
assert!(
clone.is_closing(),
"clone should see closing flag from original"
);
clone.mark_terminated();
assert!(
handle.is_terminated(),
"original should see terminated flag from clone"
);
}
#[test]
fn test_shared_worker_port_ref_increments_connected() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
let port = SharedWorkerPortRef::new(handle.clone());
assert_eq!(handle.connected_page_count(), 1);
assert_eq!(port.handle().script_url, "sw.js");
}
#[test]
fn test_shared_worker_port_ref_drop_decrements_connected() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
{
let _port = SharedWorkerPortRef::new(handle.clone());
assert_eq!(handle.connected_page_count(), 1);
}
assert_eq!(
handle.connected_page_count(),
0,
"dropping port should decrement connected count"
);
}
#[test]
fn test_shared_worker_port_ref_clone_increments_connected() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
let port = SharedWorkerPortRef::new(handle.clone());
assert_eq!(handle.connected_page_count(), 1);
let _port2 = port.clone();
assert_eq!(handle.connected_page_count(), 2);
}
#[test]
fn test_shared_worker_port_ref_multiple_pages() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
let _port1 = SharedWorkerPortRef::new(handle.clone());
let _port2 = SharedWorkerPortRef::new(handle.clone());
assert_eq!(handle.connected_page_count(), 2);
}
#[test]
fn test_webview_state_track_shared_worker_port() {
let mut state = BaoWebViewState::default();
let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
state.track_shared_worker_port(SharedWorkerPortRef::new(handle));
assert_eq!(state.shared_worker_port_count(), 1);
}
#[test]
fn test_webview_state_disconnect_shared_worker_ports() {
let mut state = BaoWebViewState::default();
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
assert_eq!(state.shared_worker_port_count(), 2);
assert_eq!(handle.connected_page_count(), 2);
state.disconnect_shared_worker_ports();
assert_eq!(state.shared_worker_port_count(), 0);
assert_eq!(
handle.connected_page_count(),
0,
"disconnect should drop ports and decrement counter"
);
}
#[test]
fn test_webview_state_disconnect_shared_worker_ports_empty() {
let mut state = BaoWebViewState::default();
state.disconnect_shared_worker_ports();
assert_eq!(state.shared_worker_port_count(), 0);
}
#[test]
fn test_delegate_register_shared_worker_new() {
let delegate = BaoServoDelegate::new();
let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
let returned = delegate.register_shared_worker(handle);
assert_eq!(returned.script_url, "sw.js");
assert_eq!(returned.name, "myname");
assert_eq!(delegate.shared_worker_count(), 1);
}
#[test]
fn test_delegate_register_shared_worker_dedup() {
let delegate = BaoServoDelegate::new();
let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
delegate.register_shared_worker(handle1);
let returned = delegate.register_shared_worker(handle2);
assert_eq!(delegate.shared_worker_count(), 1);
assert_eq!(returned.script_url, "sw.js");
}
#[test]
fn test_delegate_find_shared_worker() {
let delegate = BaoServoDelegate::new();
let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
delegate.register_shared_worker(handle);
let found = delegate.find_shared_worker("sw.js", "myname");
assert!(found.is_some());
assert_eq!(found.unwrap().script_url, "sw.js");
assert!(delegate.find_shared_worker("other.js", "myname").is_none());
assert!(delegate.find_shared_worker("sw.js", "other").is_none());
}
#[test]
fn test_delegate_reap_terminated_shared_workers() {
let delegate = BaoServoDelegate::new();
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
delegate.register_shared_worker(handle.clone());
assert_eq!(delegate.shared_worker_count(), 1);
handle.close();
handle.mark_terminated();
delegate.reap_terminated_shared_workers();
assert_eq!(
delegate.shared_worker_count(),
0,
"terminated shared worker with zero pages should be reaped"
);
}
#[test]
fn test_delegate_reap_keeps_terminated_with_connected_pages() {
let delegate = BaoServoDelegate::new();
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
delegate.register_shared_worker(handle.clone());
let _port = SharedWorkerPortRef::new(handle.clone());
handle.close();
handle.mark_terminated();
delegate.reap_terminated_shared_workers();
assert_eq!(
delegate.shared_worker_count(),
1,
"terminated but still has connected pages — keep in registry"
);
}
#[test]
fn test_shared_worker_connect_event_creation() {
let event = SharedWorkerConnectEvent {
shared_worker_id: SharedWorkerId {
script_url: "sw.js".to_string(),
name: "myname".to_string(),
},
page_url: "https://example.com/page1".to_string(),
};
assert_eq!(event.shared_worker_id.script_url, "sw.js");
assert_eq!(event.shared_worker_id.name, "myname");
assert_eq!(event.page_url, "https://example.com/page1");
}
#[test]
fn test_forward_shared_worker_connect_event() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let event = SharedWorkerConnectEvent {
shared_worker_id: SharedWorkerId {
script_url: "sw.js".to_string(),
name: "myname".to_string(),
},
page_url: "https://example.com".to_string(),
};
state.forward_shared_worker_connect_event(event);
let recv = rx.try_recv().unwrap();
match recv {
ServoEvent::Console { level, text, .. } => {
assert_eq!(level, ConsoleLevel::Debug);
assert!(text.contains("sw.js"));
assert!(text.contains("myname"));
assert!(text.contains("https://example.com"));
}
_ => panic!("expected Console event for shared worker connect"),
}
}
#[test]
fn test_forward_shared_worker_connect_event_no_tx() {
let state = BaoWebViewState::default();
let event = SharedWorkerConnectEvent {
shared_worker_id: SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
},
page_url: "https://example.com".to_string(),
};
state.forward_shared_worker_connect_event(event);
}
#[test]
fn test_shared_worker_scope_config_default() {
let config = SharedWorkerScopeConfig::default();
assert!(config.stealth_profile.is_none());
assert!(config.user_agent.is_empty());
assert!(config.platform.is_empty());
assert!(config.hardware_concurrency > 0);
assert_eq!(config.language, "en-US");
assert!(!config.languages.is_empty());
}
#[test]
fn test_page_navigation_disconnects_shared_workers() {
let mut state = BaoWebViewState::default();
let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
assert_eq!(state.shared_worker_port_count(), 1);
assert_eq!(handle.connected_page_count(), 1);
state.disconnect_shared_worker_ports();
assert_eq!(state.shared_worker_port_count(), 0);
assert_eq!(handle.connected_page_count(), 0);
}
#[test]
fn test_shared_worker_cross_page_sharing() {
let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
let mut state1 = BaoWebViewState::default();
state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
assert_eq!(handle.connected_page_count(), 1);
let mut state2 = BaoWebViewState::default();
state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
assert_eq!(handle.connected_page_count(), 2);
state1.disconnect_shared_worker_ports();
assert_eq!(handle.connected_page_count(), 1);
assert_eq!(state2.shared_worker_port_count(), 1);
assert!(!handle.is_closing());
assert!(!handle.is_terminated());
}
#[test]
fn test_structured_clone_payload_creation() {
let payload = StructuredClonePayload {
data: vec![1, 2, 3, 4, 5],
transferable_count: 0,
};
assert_eq!(payload.data.len(), 5);
assert_eq!(payload.transferable_count, 0);
}
#[test]
fn test_structured_clone_payload_with_transferables() {
let payload = StructuredClonePayload {
data: vec![0u8; 1024],
transferable_count: 2,
};
assert_eq!(payload.data.len(), 1024);
assert_eq!(payload.transferable_count, 2);
}
#[test]
fn test_structured_clone_payload_clone() {
let payload = StructuredClonePayload {
data: vec![42u8; 100],
transferable_count: 1,
};
let cloned = payload.clone();
assert_eq!(cloned.data, payload.data);
assert_eq!(cloned.transferable_count, payload.transferable_count);
}
#[test]
fn test_worker_structured_message_metadata_only() {
let msg = WorkerStructuredMessage::metadata_only(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::PageToWorker,
);
assert!(msg.payload.is_none());
assert_eq!(msg.worker_id.0, "worker1.js");
assert_eq!(msg.direction, WorkerMessageDirection::PageToWorker);
assert!(msg.message_id > 0);
}
#[test]
fn test_worker_structured_message_with_payload() {
let msg = WorkerStructuredMessage::with_payload(
WorkerId("worker2.js".to_string()),
WorkerMessageDirection::WorkerToPage,
vec![1, 2, 3],
1,
);
assert!(msg.payload.is_some());
let payload = msg.payload.unwrap();
assert_eq!(payload.data, vec![1, 2, 3]);
assert_eq!(payload.transferable_count, 1);
assert_eq!(msg.direction, WorkerMessageDirection::WorkerToPage);
}
#[test]
fn test_worker_structured_message_unique_ids() {
let msg1 = WorkerStructuredMessage::metadata_only(
WorkerId("w.js".to_string()),
WorkerMessageDirection::PageToWorker,
);
let msg2 = WorkerStructuredMessage::metadata_only(
WorkerId("w.js".to_string()),
WorkerMessageDirection::PageToWorker,
);
assert_ne!(msg1.message_id, msg2.message_id);
}
#[test]
fn test_worker_channel_bridge_creation() {
let worker_id = WorkerId("worker1.js".to_string());
let (bridge, endpoints) = WorkerChannelBridge::new(worker_id.clone());
assert_eq!(bridge.worker_id, worker_id);
assert_eq!(endpoints.worker_id, worker_id);
assert!(endpoints.page_to_worker_rx.is_some());
assert!(endpoints.worker_to_page_tx.is_some());
}
#[test]
fn test_worker_channel_bridge_page_to_worker() {
let worker_id = WorkerId("worker1.js".to_string());
let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
let payload = StructuredClonePayload {
data: vec![1, 2, 3],
transferable_count: 0,
};
bridge.post_message_to_worker(payload).unwrap();
let rx = endpoints.page_to_worker_rx.unwrap();
let received = rx.try_recv().unwrap();
assert_eq!(received.data, vec![1, 2, 3]);
}
#[test]
fn test_worker_channel_bridge_worker_to_page() {
let worker_id = WorkerId("worker1.js".to_string());
let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
let msg = WorkerStructuredMessage::with_payload(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::WorkerToPage,
vec![4, 5, 6],
0,
);
let tx = endpoints.worker_to_page_tx.unwrap();
tx.send(msg).unwrap();
let result = bridge.try_recv_from_worker().unwrap();
assert!(result.is_some());
let received = result.unwrap();
assert_eq!(received.payload.unwrap().data, vec![4, 5, 6]);
}
#[test]
fn test_worker_channel_bridge_drain() {
let worker_id = WorkerId("worker1.js".to_string());
let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
let tx = endpoints.worker_to_page_tx.unwrap();
for i in 0..3 {
let msg = WorkerStructuredMessage::with_payload(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::WorkerToPage,
vec![i],
0,
);
tx.send(msg).unwrap();
}
let result = bridge.drain_worker_messages();
assert_eq!(result.messages.len(), 3);
assert!(!result.disconnected);
let empty = bridge.drain_worker_messages();
assert!(empty.messages.is_empty());
assert!(!empty.disconnected);
}
#[test]
fn test_webview_state_worker_channel_registration() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id.clone());
state.register_worker_channel(bridge);
assert_eq!(state.worker_channel_count(), 1);
assert!(state.worker_channel(&worker_id).is_some());
}
#[test]
fn test_webview_state_create_worker_channel() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
let endpoints = state.create_worker_channel(worker_id.clone());
assert_eq!(state.worker_channel_count(), 1);
assert_eq!(endpoints.worker_id, worker_id);
assert!(endpoints.page_to_worker_rx.is_some());
assert!(endpoints.worker_to_page_tx.is_some());
}
#[test]
fn test_webview_state_post_to_worker() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
let endpoints = state.create_worker_channel(worker_id.clone());
let payload = StructuredClonePayload {
data: vec![42],
transferable_count: 0,
};
let result = state.post_to_worker(&worker_id, payload);
assert!(result.is_ok());
let rx = endpoints.page_to_worker_rx.unwrap();
let received = rx.try_recv().unwrap();
assert_eq!(received.data, vec![42]);
let result = state.post_to_worker(
&WorkerId("nonexistent.js".to_string()),
StructuredClonePayload {
data: vec![],
transferable_count: 0,
},
);
assert!(result.is_err());
}
#[test]
fn test_webview_state_drain_all_worker_messages() {
let mut state = BaoWebViewState::default();
let worker_id1 = WorkerId("worker1.js".to_string());
let worker_id2 = WorkerId("worker2.js".to_string());
let endpoints1 = state.create_worker_channel(worker_id1);
let endpoints2 = state.create_worker_channel(worker_id2);
let tx1 = endpoints1.worker_to_page_tx.unwrap();
let tx2 = endpoints2.worker_to_page_tx.unwrap();
tx1.send(WorkerStructuredMessage::metadata_only(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
tx2.send(WorkerStructuredMessage::metadata_only(
WorkerId("worker2.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
let (messages, disconnected) = state.drain_all_worker_messages();
assert_eq!(messages.len(), 2);
assert!(disconnected.is_empty());
}
#[test]
fn test_webview_state_terminate_clears_channels() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.create_worker_channel(WorkerId("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
state.create_worker_channel(WorkerId("worker2.js".to_string()));
assert_eq!(state.worker_channel_count(), 2);
state.terminate_all_workers();
assert_eq!(state.worker_channel_count(), 0);
}
#[test]
fn test_webview_state_reap_terminated_worker_channels() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.create_worker_channel(WorkerId("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
state.create_worker_channel(WorkerId("worker2.js".to_string()));
state.active_workers[0].handle().terminate();
state.active_workers[0].handle().mark_terminated();
state.reap_terminated_workers();
assert_eq!(state.worker_channel_count(), 1);
assert!(state
.worker_channel(&WorkerId("worker2.js".to_string()))
.is_some());
}
#[test]
fn test_webview_state_remove_worker_channel() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.create_worker_channel(worker_id.clone());
assert_eq!(state.worker_channel_count(), 1);
let removed = state.remove_worker_channel(&worker_id);
assert!(removed.is_some());
assert_eq!(state.worker_channel_count(), 0);
}
#[test]
fn test_forward_worker_structured_message_with_payload() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let msg = WorkerStructuredMessage::with_payload(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::WorkerToPage,
vec![1, 2, 3],
1,
);
state.forward_worker_structured_message(&msg);
let event = rx.try_recv().unwrap();
match event {
ServoEvent::Console { level, text, .. } => {
assert_eq!(level, ConsoleLevel::Debug);
assert!(text.contains("worker→page"));
assert!(text.contains("worker1.js"));
assert!(text.contains("3 bytes"));
assert!(text.contains("1 transferable"));
}
_ => panic!("expected Console event for structured message"),
}
}
#[test]
fn test_forward_worker_structured_message_metadata_only() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let msg = WorkerStructuredMessage::metadata_only(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::PageToWorker,
);
state.forward_worker_structured_message(&msg);
let event = rx.try_recv().unwrap();
match event {
ServoEvent::Console { text, .. } => {
assert!(text.contains("metadata-only"));
}
_ => panic!("expected Console event"),
}
}
#[test]
fn test_drain_and_forward_worker_messages() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let mut state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let endpoints = state.create_worker_channel(WorkerId("worker1.js".to_string()));
let worker_tx = endpoints.worker_to_page_tx.unwrap();
worker_tx
.send(WorkerStructuredMessage::metadata_only(
WorkerId("worker1.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
let disconnected = state.drain_and_forward_worker_messages();
assert!(disconnected.is_empty());
let event = rx.try_recv().unwrap();
match event {
ServoEvent::Console { text, .. } => {
assert!(text.contains("worker→page"));
}
_ => panic!("expected Console event"),
}
}
#[test]
fn test_worker_channel_bridge_disconnected() {
let worker_id = WorkerId("worker1.js".to_string());
let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id);
let result = bridge.try_recv_from_worker();
assert!(result.is_ok()); assert!(result.unwrap().is_none());
}
#[test]
fn test_worker_location_from_https_url() {
let loc = WorkerLocation::from_url("https://example.com:8080/path?q=1#hash").unwrap();
assert_eq!(loc.href, "https://example.com:8080/path?q=1#hash");
assert_eq!(loc.protocol, "https:");
assert_eq!(loc.host, "example.com:8080");
assert_eq!(loc.hostname, "example.com");
assert_eq!(loc.port, "8080");
assert_eq!(loc.pathname, "/path");
assert_eq!(loc.search, "?q=1");
assert_eq!(loc.hash, "#hash");
assert_eq!(loc.origin, "https://example.com:8080");
}
#[test]
fn test_worker_location_from_default_port() {
let loc = WorkerLocation::from_url("https://example.com/path").unwrap();
assert_eq!(loc.host, "example.com");
assert_eq!(loc.port, "");
assert_eq!(loc.origin, "https://example.com");
}
#[test]
fn test_worker_location_from_http_url() {
let loc = WorkerLocation::from_url("http://localhost:3000/worker.js").unwrap();
assert_eq!(loc.protocol, "http:");
assert_eq!(loc.hostname, "localhost");
assert_eq!(loc.port, "3000");
assert_eq!(loc.pathname, "/worker.js");
}
#[test]
fn test_worker_location_from_url_no_query_no_hash() {
let loc = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
assert_eq!(loc.search, "");
assert_eq!(loc.hash, "");
}
#[test]
fn test_worker_location_from_invalid_url() {
assert!(WorkerLocation::from_url("not a url").is_none());
}
#[test]
fn test_worker_location_from_url_value() {
let url = url::Url::parse("https://example.com/worker.js").unwrap();
let loc = WorkerLocation::from_url_value(url);
assert_eq!(loc.protocol, "https:");
assert_eq!(loc.hostname, "example.com");
assert_eq!(loc.pathname, "/worker.js");
}
#[test]
fn test_worker_navigator_default() {
let nav = WorkerNavigator::default();
assert!(nav.user_agent.is_empty());
assert!(nav.platform.is_empty());
assert!(nav.hardware_concurrency > 0);
assert_eq!(nav.language, "en-US");
assert!(!nav.languages.is_empty());
assert!(nav.connection.is_none());
assert!(!nav.cookie_enabled);
assert_eq!(nav.max_touch_points, 0);
assert_eq!(nav.product, "Gecko");
assert_eq!(nav.app_code_name, "Mozilla");
assert_eq!(nav.app_name, "Netscape");
assert!(nav.app_version.is_empty());
}
#[test]
fn test_worker_navigator_from_scope_config() {
let config = WorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux x86_64".to_string(),
hardware_concurrency: 8,
language: "zh-CN".to_string(),
languages: vec!["zh-CN".to_string(), "zh".to_string()],
};
let nav = WorkerNavigator::from_scope_config(&config);
assert_eq!(nav.user_agent, "Bao/1.0");
assert_eq!(nav.platform, "Linux x86_64");
assert_eq!(nav.hardware_concurrency, 8);
assert_eq!(nav.language, "zh-CN");
assert_eq!(nav.languages.len(), 2);
assert_eq!(nav.app_version, "Bao/1.0"); assert_eq!(nav.product, "Gecko");
assert_eq!(nav.app_code_name, "Mozilla");
assert_eq!(nav.app_name, "Netscape");
}
#[test]
fn test_worker_navigator_from_shared_scope_config() {
let config = SharedWorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/2.0".to_string(),
platform: "MacOS".to_string(),
hardware_concurrency: 4,
language: "ja".to_string(),
languages: vec!["ja".to_string(), "en".to_string()],
};
let nav = WorkerNavigator::from_shared_scope_config(&config);
assert_eq!(nav.user_agent, "Bao/2.0");
assert_eq!(nav.platform, "MacOS");
assert_eq!(nav.hardware_concurrency, 4);
assert_eq!(nav.app_version, "Bao/2.0");
}
#[test]
fn test_worker_network_information() {
let info = WorkerNetworkInformation {
effective_type: "4g".to_string(),
downlink: 10,
rtt: 50,
save_data: false,
};
assert_eq!(info.effective_type, "4g");
assert_eq!(info.downlink, 10);
assert_eq!(info.rtt, 50);
assert!(!info.save_data);
}
#[test]
fn test_worker_global_scope_state_new() {
let config = WorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux".to_string(),
hardware_concurrency: 8,
language: "en-US".to_string(),
languages: vec!["en-US".to_string()],
};
let scope =
WorkerGlobalScopeState::new("https://example.com/worker.js".to_string(), &config);
assert_eq!(scope.worker_url, "https://example.com/worker.js");
assert!(!scope.closing);
assert!(scope.location.is_some());
assert_eq!(scope.navigator.user_agent, "Bao/1.0");
}
#[test]
fn test_worker_global_scope_state_new_shared() {
let config = SharedWorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/2.0".to_string(),
platform: "MacOS".to_string(),
hardware_concurrency: 4,
language: "ja".to_string(),
languages: vec!["ja".to_string()],
};
let scope =
WorkerGlobalScopeState::new_shared("https://example.com/sw.js".to_string(), &config);
assert_eq!(scope.worker_url, "https://example.com/sw.js");
assert_eq!(scope.navigator.user_agent, "Bao/2.0");
}
#[test]
fn test_worker_global_scope_state_location_parsed() {
let config = WorkerScopeConfig::default();
let scope = WorkerGlobalScopeState::new(
"https://example.com:8080/app/worker.js?debug=true#section".to_string(),
&config,
);
let loc = scope.location.unwrap();
assert_eq!(loc.hostname, "example.com");
assert_eq!(loc.port, "8080");
assert_eq!(loc.pathname, "/app/worker.js");
assert_eq!(loc.search, "?debug=true");
assert_eq!(loc.hash, "#section");
}
#[test]
fn test_worker_global_scope_state_invalid_url_no_location() {
let config = WorkerScopeConfig::default();
let scope = WorkerGlobalScopeState::new("not-a-url".to_string(), &config);
assert!(scope.location.is_none());
}
#[test]
fn test_dedicated_worker_global_scope_state_new() {
let worker_id = WorkerId("https://example.com/worker.js".to_string());
let config = WorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux".to_string(),
hardware_concurrency: 8,
language: "en-US".to_string(),
languages: vec!["en-US".to_string()],
};
let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
assert_eq!(scope.worker_id, worker_id);
assert!(!scope.has_onmessage);
assert!(!scope.has_onerror);
assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
}
#[test]
fn test_dedicated_worker_global_scope_state_location() {
let worker_id = WorkerId("https://example.com/worker.js".to_string());
let config = WorkerScopeConfig::default();
let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
let loc = scope.location().unwrap();
assert_eq!(loc.hostname, "example.com");
assert_eq!(loc.pathname, "/worker.js");
}
#[test]
fn test_dedicated_worker_global_scope_state_navigator() {
let worker_id = WorkerId("worker.js".to_string());
let config = WorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux".to_string(),
hardware_concurrency: 8,
language: "zh-CN".to_string(),
languages: vec!["zh-CN".to_string()],
};
let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
let nav = scope.navigator();
assert_eq!(nav.user_agent, "Bao/1.0");
assert_eq!(nav.hardware_concurrency, 8);
}
#[test]
fn test_dedicated_worker_global_scope_state_event_handlers() {
let worker_id = WorkerId("worker.js".to_string());
let config = WorkerScopeConfig::default();
let mut scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
assert!(!scope.has_onmessage);
assert!(!scope.has_onerror);
scope.set_onmessage();
assert!(scope.has_onmessage);
assert!(!scope.has_onerror);
scope.set_onerror();
assert!(scope.has_onmessage);
assert!(scope.has_onerror);
}
#[test]
fn test_webview_state_dedicated_worker_scope_registration() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
let config = WorkerScopeConfig::default();
let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
state.register_dedicated_worker_scope(worker_id.clone(), scope);
assert_eq!(state.dedicated_worker_scope_count(), 1);
assert!(state.dedicated_worker_scope(&worker_id).is_some());
}
#[test]
fn test_webview_state_dedicated_worker_scope_get_mut() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
let config = WorkerScopeConfig::default();
let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
state.register_dedicated_worker_scope(worker_id.clone(), scope);
state
.dedicated_worker_scope_mut(&worker_id)
.unwrap()
.set_onmessage();
assert!(
state
.dedicated_worker_scope(&worker_id)
.unwrap()
.has_onmessage
);
}
#[test]
fn test_webview_state_dedicated_worker_scope_remove() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
let config = WorkerScopeConfig::default();
let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
state.register_dedicated_worker_scope(worker_id.clone(), scope);
let removed = state.remove_dedicated_worker_scope(&worker_id);
assert!(removed.is_some());
assert_eq!(state.dedicated_worker_scope_count(), 0);
}
#[test]
fn test_webview_state_dedicated_worker_scopes_snapshot() {
let mut state = BaoWebViewState::default();
let config = WorkerScopeConfig::default();
let id1 = WorkerId("worker1.js".to_string());
let id2 = WorkerId("worker2.js".to_string());
state.register_dedicated_worker_scope(
id1,
DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
);
state.register_dedicated_worker_scope(
id2,
DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
);
let scopes = state.dedicated_worker_scopes();
assert_eq!(scopes.len(), 2);
}
#[test]
fn test_webview_state_terminate_clears_dedicated_worker_scopes() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
let config = WorkerScopeConfig::default();
state.register_dedicated_worker_scope(
WorkerId("worker1.js".to_string()),
DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
);
assert_eq!(state.dedicated_worker_scope_count(), 1);
state.terminate_all_workers();
assert_eq!(state.dedicated_worker_scope_count(), 0);
}
#[test]
fn test_webview_state_reap_terminated_dedicated_worker_scopes() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
let config = WorkerScopeConfig::default();
state.register_dedicated_worker_scope(
WorkerId("worker1.js".to_string()),
DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
);
state.register_dedicated_worker_scope(
WorkerId("worker2.js".to_string()),
DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
);
state.active_workers[0].handle().terminate();
state.active_workers[0].handle().mark_terminated();
state.reap_terminated_workers();
assert_eq!(state.dedicated_worker_scope_count(), 1);
assert!(state
.dedicated_worker_scope(&WorkerId("worker2.js".to_string()))
.is_some());
}
#[test]
fn test_worker_location_equality() {
let loc1 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
let loc2 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
assert_eq!(loc1, loc2);
}
#[test]
fn test_worker_script_source_inline() {
let source = WorkerScriptSource::Inline("var x = 1;".to_string());
assert_eq!(source, WorkerScriptSource::Inline("var x = 1;".to_string()));
assert_ne!(source, WorkerScriptSource::Inline("var y = 2;".to_string()));
}
#[test]
fn test_worker_script_source_url() {
let source = WorkerScriptSource::Url("https://example.com/worker.js".to_string());
assert_eq!(
source,
WorkerScriptSource::Url("https://example.com/worker.js".to_string())
);
assert_ne!(
source,
WorkerScriptSource::Url("https://other.com/worker.js".to_string())
);
}
#[test]
fn test_worker_script_load_result() {
let result = WorkerScriptLoadResult {
source: "self.onmessage = function(e) {}".to_string(),
final_url: "https://example.com/worker.js".to_string(),
mime_type: Some("text/javascript".to_string()),
};
assert_eq!(result.source, "self.onmessage = function(e) {}");
assert_eq!(result.final_url, "https://example.com/worker.js");
assert_eq!(result.mime_type.as_deref(), Some("text/javascript"));
}
#[test]
fn test_worker_script_load_error_network() {
let err = WorkerScriptLoadError::NetworkError("404 Not Found".to_string());
assert_eq!(
err,
WorkerScriptLoadError::NetworkError("404 Not Found".to_string())
);
}
#[test]
fn test_worker_script_load_error_invalid_mime() {
let err = WorkerScriptLoadError::InvalidMimeType {
received: "text/html".to_string(),
url: "https://example.com/worker.js".to_string(),
};
match err {
WorkerScriptLoadError::InvalidMimeType { received, url } => {
assert_eq!(received, "text/html");
assert_eq!(url, "https://example.com/worker.js");
}
_ => panic!("expected InvalidMimeType"),
}
}
#[test]
fn test_worker_script_load_error_utf8() {
let err = WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string());
assert_eq!(
err,
WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string())
);
}
#[test]
fn test_worker_script_load_error_invalid_url() {
let err = WorkerScriptLoadError::InvalidUrl("bad url".to_string());
assert_eq!(
err,
WorkerScriptLoadError::InvalidUrl("bad url".to_string())
);
}
#[test]
fn test_worker_script_load_error_cancelled() {
let err = WorkerScriptLoadError::Cancelled;
assert_eq!(err, WorkerScriptLoadError::Cancelled);
}
#[test]
fn test_worker_script_type_default_classic() {
assert_eq!(WorkerScriptType::default(), WorkerScriptType::Classic);
}
#[test]
fn test_worker_script_type_equality() {
assert_eq!(WorkerScriptType::Classic, WorkerScriptType::Classic);
assert_eq!(WorkerScriptType::Module, WorkerScriptType::Module);
assert_ne!(WorkerScriptType::Classic, WorkerScriptType::Module);
}
#[test]
fn test_is_javascript_mime_type_valid() {
assert!(is_javascript_mime_type("text/javascript"));
assert!(is_javascript_mime_type("application/javascript"));
assert!(is_javascript_mime_type("application/ecmascript"));
assert!(is_javascript_mime_type("application/x-javascript"));
assert!(is_javascript_mime_type("text/ecmascript"));
assert!(is_javascript_mime_type("text/x-javascript"));
assert!(is_javascript_mime_type("text/jscript"));
assert!(is_javascript_mime_type("text/livescript"));
}
#[test]
fn test_is_javascript_mime_type_case_insensitive() {
assert!(is_javascript_mime_type("Text/JavaScript"));
assert!(is_javascript_mime_type("APPLICATION/JAVASCRIPT"));
assert!(is_javascript_mime_type("text/JavaScript"));
}
#[test]
fn test_is_javascript_mime_type_with_charset() {
assert!(is_javascript_mime_type("text/javascript; charset=utf-8"));
assert!(is_javascript_mime_type(
"application/javascript;charset=utf-8"
));
}
#[test]
fn test_is_javascript_mime_type_invalid() {
assert!(!is_javascript_mime_type("text/html"));
assert!(!is_javascript_mime_type("application/json"));
assert!(!is_javascript_mime_type("text/plain"));
assert!(!is_javascript_mime_type("application/octet-stream"));
assert!(!is_javascript_mime_type("text/css"));
}
#[test]
fn test_worker_script_loader_inline() {
let loader =
WorkerScriptLoader::inline("var x = 1;".to_string(), WorkerScriptType::Classic);
assert!(loader.script_url().is_none());
assert!(!loader.requires_fetch());
let resolved = loader.resolve().unwrap();
assert_eq!(
resolved,
WorkerScriptSource::Inline("var x = 1;".to_string())
);
}
#[test]
fn test_worker_script_loader_url_https() {
let loader = WorkerScriptLoader::url(
"https://example.com/worker.js".to_string(),
WorkerScriptType::Classic,
);
assert_eq!(loader.script_url(), Some("https://example.com/worker.js"));
assert!(loader.requires_fetch());
let resolved = loader.resolve().unwrap();
assert_eq!(
resolved,
WorkerScriptSource::Url("https://example.com/worker.js".to_string())
);
}
#[test]
fn test_worker_script_loader_url_http() {
let loader = WorkerScriptLoader::url(
"http://localhost:3000/worker.js".to_string(),
WorkerScriptType::Module,
);
assert!(loader.requires_fetch());
assert_eq!(loader.script_type, WorkerScriptType::Module);
}
#[test]
fn test_worker_script_loader_url_invalid() {
let loader = WorkerScriptLoader::url("not a url".to_string(), WorkerScriptType::Classic);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidUrl(msg) => {
assert!(msg.contains("Invalid Worker script URL"));
}
_ => panic!("expected InvalidUrl error"),
}
}
#[test]
fn test_worker_script_loader_url_unsupported_scheme() {
let loader = WorkerScriptLoader::url(
"ftp://example.com/worker.js".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidUrl(msg) => {
assert!(msg.contains("Unsupported") || msg.contains("ftp"));
}
_ => panic!("expected InvalidUrl error"),
}
}
#[test]
fn test_worker_script_loader_data_url_text() {
let loader = WorkerScriptLoader::url(
"data:text/javascript,self.postMessage('hello')".to_string(),
WorkerScriptType::Classic,
);
let resolved = loader.resolve().unwrap();
match resolved {
WorkerScriptSource::Inline(script) => {
assert_eq!(script, "self.postMessage('hello')");
}
WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
}
}
#[test]
fn test_worker_script_loader_data_url_base64() {
let loader = WorkerScriptLoader::url(
"data:text/javascript;base64,dmFyIHggPSAxOw==".to_string(),
WorkerScriptType::Classic,
);
let resolved = loader.resolve().unwrap();
match resolved {
WorkerScriptSource::Inline(script) => {
assert_eq!(script, "var x = 1;");
}
WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
}
}
#[test]
fn test_worker_script_loader_data_url_invalid_base64() {
let loader = WorkerScriptLoader::url(
"data:text/javascript;base64,!!!invalid!!!".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
}
#[test]
fn test_worker_script_loader_data_url_missing_comma() {
let loader = WorkerScriptLoader::url(
"data:text/javascript".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidUrl(msg) => {
assert!(msg.contains("comma separator"));
}
_ => panic!("expected InvalidUrl error"),
}
}
#[test]
fn test_worker_script_loader_blob_url_passthrough() {
let loader = WorkerScriptLoader::url(
"blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string(),
WorkerScriptType::Classic,
);
let resolved = loader.resolve().unwrap();
assert_eq!(
resolved,
WorkerScriptSource::Url(
"blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string()
)
);
}
#[test]
fn test_worker_script_loader_from_source() {
let loader = WorkerScriptLoader::from_source(
WorkerScriptSource::Inline("code".to_string()),
WorkerScriptType::Module,
);
assert_eq!(loader.script_type, WorkerScriptType::Module);
assert!(loader.script_url().is_none());
}
#[test]
fn test_worker_script_loader_validate_mime_type_valid() {
assert!(WorkerScriptLoader::validate_mime_type(
"text/javascript",
"https://example.com/worker.js"
)
.is_ok());
assert!(WorkerScriptLoader::validate_mime_type(
"application/javascript",
"https://example.com/worker.js"
)
.is_ok());
}
#[test]
fn test_worker_script_loader_validate_mime_type_invalid() {
let result =
WorkerScriptLoader::validate_mime_type("text/html", "https://example.com/worker.js");
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidMimeType { received, url } => {
assert_eq!(received, "text/html");
assert_eq!(url, "https://example.com/worker.js");
}
_ => panic!("expected InvalidMimeType error"),
}
}
#[test]
fn test_worker_script_load_state_transitions() {
let mut state = WorkerScriptLoadState::Pending;
assert!(state.is_loading());
assert!(!state.is_ready());
assert!(!state.is_failed());
state = WorkerScriptLoadState::Fetching;
assert!(state.is_loading());
state = WorkerScriptLoadState::Validating;
assert!(state.is_loading());
state = WorkerScriptLoadState::Decoding;
assert!(state.is_loading());
state = WorkerScriptLoadState::Compiling;
assert!(state.is_loading());
state = WorkerScriptLoadState::Ready;
assert!(!state.is_loading());
assert!(state.is_ready());
state = WorkerScriptLoadState::Failed(WorkerScriptLoadError::NetworkError(
"timeout".to_string(),
));
assert!(!state.is_loading());
assert!(state.is_failed());
}
#[test]
fn test_webview_state_worker_script_load_state_registration() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
assert_eq!(state.worker_script_load_state_count(), 1);
assert!(state.worker_script_load_state(&worker_id).is_some());
assert_eq!(
state.worker_script_load_state(&worker_id).unwrap(),
&WorkerScriptLoadState::Pending
);
}
#[test]
fn test_webview_state_worker_script_load_state_update() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
assert_eq!(
state.worker_script_load_state(&worker_id).unwrap(),
&WorkerScriptLoadState::Fetching
);
}
#[test]
fn test_webview_state_worker_script_load_state_remove() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Ready);
let removed = state.remove_worker_script_load_state(&worker_id);
assert!(removed.is_some());
assert_eq!(removed.unwrap(), WorkerScriptLoadState::Ready);
assert_eq!(state.worker_script_load_state_count(), 0);
}
#[test]
fn test_webview_state_terminate_clears_script_load_states() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.register_worker_script_load_state(
WorkerId("worker1.js".to_string()),
WorkerScriptLoadState::Fetching,
);
assert_eq!(state.worker_script_load_state_count(), 1);
state.terminate_all_workers();
assert_eq!(state.worker_script_load_state_count(), 0);
}
#[test]
fn test_webview_state_reap_terminated_worker_script_load_states() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
state.register_worker_script_load_state(
WorkerId("worker1.js".to_string()),
WorkerScriptLoadState::Ready,
);
state.register_worker_script_load_state(
WorkerId("worker2.js".to_string()),
WorkerScriptLoadState::Fetching,
);
state.active_workers[0].handle().terminate();
state.active_workers[0].handle().mark_terminated();
state.reap_terminated_workers();
assert_eq!(state.worker_script_load_state_count(), 1);
assert!(state
.worker_script_load_state(&WorkerId("worker2.js".to_string()))
.is_some());
}
#[test]
fn test_worker_script_loader_file_url() {
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join("bao_test_worker_script.js");
std::fs::write(&temp_file, "var x = 42;").unwrap();
let file_url = format!("file://{}", temp_file.display());
let loader = WorkerScriptLoader::url(file_url, WorkerScriptType::Classic);
let resolved = loader.resolve().unwrap();
match resolved {
WorkerScriptSource::Inline(script) => {
assert_eq!(script, "var x = 42;");
}
WorkerScriptSource::Url(_) => panic!("expected inline source from file: URL"),
}
let _ = std::fs::remove_file(&temp_file);
}
#[test]
fn test_worker_script_loader_file_url_not_found() {
let loader = WorkerScriptLoader::url(
"file:///nonexistent/path/worker.js".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::NetworkError(msg) => {
assert!(msg.contains("Failed to read") || msg.contains("No such file"));
}
_ => panic!("expected NetworkError for missing file"),
}
}
#[test]
fn test_worker_script_loader_full_pipeline_states() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("https://example.com/worker.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
assert!(state
.worker_script_load_state(&worker_id)
.unwrap()
.is_loading());
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
assert!(matches!(
state.worker_script_load_state(&worker_id).unwrap(),
WorkerScriptLoadState::Fetching
));
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Validating);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Decoding);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Compiling);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Ready);
assert!(state
.worker_script_load_state(&worker_id)
.unwrap()
.is_ready());
}
#[test]
fn test_worker_script_loader_pipeline_failure() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("https://example.com/bad-worker.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
state.update_worker_script_load_state(
&worker_id,
WorkerScriptLoadState::Failed(WorkerScriptLoadError::InvalidMimeType {
received: "text/html".to_string(),
url: "https://example.com/bad-worker.js".to_string(),
}),
);
assert!(state
.worker_script_load_state(&worker_id)
.unwrap()
.is_failed());
}
#[test]
fn test_worker_scope_config_from_stealth_profile_chrome() {
let profile = bao_stealth::StealthProfile::chrome_default();
let config = WorkerScopeConfig::from(&profile);
assert!(
config.stealth_profile.is_some(),
"stealth_profile must be Some"
);
assert_eq!(config.user_agent, profile.navigator.user_agent);
assert_eq!(config.platform, profile.navigator.platform);
assert_eq!(
config.hardware_concurrency,
profile.navigator.hardware_concurrency as usize
);
assert_eq!(config.language, profile.navigator.language);
assert_eq!(config.languages, profile.navigator.languages);
assert!(
config.user_agent.contains("Chrome"),
"Chrome profile UA must contain Chrome"
);
}
#[test]
fn test_worker_scope_config_from_stealth_profile_firefox() {
let profile = bao_stealth::StealthProfile::firefox_default();
let config = WorkerScopeConfig::from(&profile);
assert!(
config.stealth_profile.is_some(),
"stealth_profile must be Some"
);
assert_eq!(config.user_agent, profile.navigator.user_agent);
assert_eq!(config.platform, profile.navigator.platform);
assert_eq!(
config.hardware_concurrency,
profile.navigator.hardware_concurrency as usize
);
assert_eq!(config.language, profile.navigator.language);
assert_eq!(config.languages, profile.navigator.languages);
assert!(
config.user_agent.contains("Firefox"),
"Firefox profile UA must contain Firefox"
);
}
#[test]
fn test_shared_worker_scope_config_from_stealth_profile() {
let profile = bao_stealth::StealthProfile::chrome_default();
let config = SharedWorkerScopeConfig::from(&profile);
assert!(
config.stealth_profile.is_some(),
"stealth_profile must be Some"
);
assert_eq!(config.user_agent, profile.navigator.user_agent);
assert_eq!(config.platform, profile.navigator.platform);
assert_eq!(
config.hardware_concurrency,
profile.navigator.hardware_concurrency as usize
);
assert_eq!(config.language, profile.navigator.language);
assert_eq!(config.languages, profile.navigator.languages);
}
#[test]
fn test_worker_scope_config_from_stealth_profile_carries_canvas_webgl_audio() {
let profile = bao_stealth::StealthProfile::chrome_default();
let config = WorkerScopeConfig::from(&profile);
let worker_profile = config.stealth_profile.unwrap();
assert_eq!(
worker_profile.canvas.seed(),
profile.canvas.seed(),
"Canvas seed must match"
);
assert!(
(worker_profile.canvas.noise_amplitude() - profile.canvas.noise_amplitude()).abs()
< f64::EPSILON,
"Canvas amplitude must match"
);
assert_eq!(
worker_profile.audio.seed(),
profile.audio.seed(),
"Audio seed must match"
);
assert_eq!(
worker_profile.webgl.vendor, profile.webgl.vendor,
"WebGL vendor must match"
);
assert_eq!(
worker_profile.webgl.renderer, profile.webgl.renderer,
"WebGL renderer must match"
);
}
#[test]
fn test_worker_scope_config_from_different_profiles_produces_different_configs() {
let chrome = bao_stealth::StealthProfile::chrome_default();
let firefox = bao_stealth::StealthProfile::firefox_default();
let chrome_config = WorkerScopeConfig::from(&chrome);
let firefox_config = WorkerScopeConfig::from(&firefox);
assert_ne!(chrome_config.user_agent, firefox_config.user_agent);
assert_ne!(
chrome_config.stealth_profile.unwrap().canvas.seed(),
firefox_config.stealth_profile.unwrap().canvas.seed()
);
}
#[test]
fn test_shared_worker_global_scope_state_new() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "myworker".to_string(),
};
let config = SharedWorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux".to_string(),
hardware_concurrency: 8,
language: "en-US".to_string(),
languages: vec!["en-US".to_string()],
};
let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
assert_eq!(scope.shared_worker_id, id);
assert!(!scope.has_onconnect);
assert_eq!(scope.connect_count, 0);
assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
}
#[test]
fn test_shared_worker_global_scope_state_location() {
let id = SharedWorkerId {
script_url: "https://example.com/sw.js".to_string(),
name: String::new(),
};
let config = SharedWorkerScopeConfig::default();
let scope = SharedWorkerGlobalScopeState::new(id, &config);
let loc = scope.location().unwrap();
assert_eq!(loc.hostname, "example.com");
assert_eq!(loc.pathname, "/sw.js");
}
#[test]
fn test_shared_worker_global_scope_state_navigator() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let config = SharedWorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/2.0".to_string(),
platform: "MacOS".to_string(),
hardware_concurrency: 4,
language: "ja".to_string(),
languages: vec!["ja".to_string()],
};
let scope = SharedWorkerGlobalScopeState::new(id, &config);
let nav = scope.navigator();
assert_eq!(nav.user_agent, "Bao/2.0");
assert_eq!(nav.hardware_concurrency, 4);
}
#[test]
fn test_shared_worker_global_scope_state_onconnect() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
};
let config = SharedWorkerScopeConfig::default();
let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
assert!(!scope.has_onconnect);
scope.set_onconnect();
assert!(scope.has_onconnect);
}
#[test]
fn test_shared_worker_global_scope_state_connect_count() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
};
let config = SharedWorkerScopeConfig::default();
let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
assert_eq!(scope.connect_count, 0);
scope.page_connected();
assert_eq!(scope.connect_count, 1);
scope.page_connected();
assert_eq!(scope.connect_count, 2);
}
#[test]
fn test_shared_worker_port_channel_creation() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let (port, endpoints) = SharedWorkerPortChannel::new(id.clone());
assert_eq!(port.shared_worker_id, id);
assert_eq!(endpoints.shared_worker_id, id);
assert!(endpoints.page_to_worker_rx.is_some());
assert!(endpoints.worker_to_page_tx.is_some());
}
#[test]
fn test_shared_worker_port_channel_page_to_worker() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
};
let (port, endpoints) = SharedWorkerPortChannel::new(id);
let payload = StructuredClonePayload {
data: vec![1, 2, 3],
transferable_count: 0,
};
port.post_message_to_worker(payload).unwrap();
let rx = endpoints.page_to_worker_rx.unwrap();
let received = rx.try_recv().unwrap();
assert_eq!(received.data, vec![1, 2, 3]);
}
#[test]
fn test_shared_worker_port_channel_worker_to_page() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
};
let (port, endpoints) = SharedWorkerPortChannel::new(id);
let msg = WorkerStructuredMessage::with_payload(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
vec![4, 5, 6],
0,
);
let tx = endpoints.worker_to_page_tx.unwrap();
tx.send(msg).unwrap();
let result = port.try_recv_from_worker().unwrap();
assert!(result.is_some());
assert_eq!(result.unwrap().payload.unwrap().data, vec![4, 5, 6]);
}
#[test]
fn test_shared_worker_port_channel_drain() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: String::new(),
};
let (port, endpoints) = SharedWorkerPortChannel::new(id);
let tx = endpoints.worker_to_page_tx.unwrap();
for i in 0..3 {
let msg = WorkerStructuredMessage::with_payload(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
vec![i],
0,
);
tx.send(msg).unwrap();
}
let result = port.drain_worker_messages();
assert_eq!(result.messages.len(), 3);
assert!(!result.disconnected);
let empty = port.drain_worker_messages();
assert!(empty.messages.is_empty());
assert!(!empty.disconnected);
}
#[test]
fn test_shared_worker_channel_bridge_new() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let bridge = SharedWorkerChannelBridge::new(id.clone());
assert_eq!(bridge.shared_worker_id, id);
assert_eq!(bridge.port_count(), 0);
}
#[test]
fn test_shared_worker_channel_bridge_add_port() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let mut bridge = SharedWorkerChannelBridge::new(id.clone());
let endpoints = bridge.add_port();
assert_eq!(bridge.port_count(), 1);
assert_eq!(endpoints.shared_worker_id, id);
assert!(endpoints.page_to_worker_rx.is_some());
assert!(endpoints.worker_to_page_tx.is_some());
}
#[test]
fn test_shared_worker_channel_bridge_multiple_ports() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let mut bridge = SharedWorkerChannelBridge::new(id);
bridge.add_port(); bridge.add_port(); bridge.add_port(); assert_eq!(bridge.port_count(), 3);
}
#[test]
fn test_shared_worker_channel_bridge_drain_all() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let mut bridge = SharedWorkerChannelBridge::new(id);
let endpoints1 = bridge.add_port();
let endpoints2 = bridge.add_port();
let tx1 = endpoints1.worker_to_page_tx.unwrap();
let tx2 = endpoints2.worker_to_page_tx.unwrap();
tx1.send(WorkerStructuredMessage::metadata_only(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
tx2.send(WorkerStructuredMessage::metadata_only(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
let (messages, disconnected) = bridge.drain_all_worker_messages();
assert_eq!(messages.len(), 2);
assert!(disconnected.is_empty());
}
#[test]
fn test_shared_worker_channel_bridge_post_to_worker() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let mut bridge = SharedWorkerChannelBridge::new(id);
let endpoints = bridge.add_port();
let payload = StructuredClonePayload {
data: vec![42],
transferable_count: 0,
};
bridge.post_to_worker_from_port(0, payload).unwrap();
let rx = endpoints.page_to_worker_rx.unwrap();
let received = rx.try_recv().unwrap();
assert_eq!(received.data, vec![42]);
}
#[test]
fn test_shared_worker_channel_bridge_post_invalid_port() {
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let mut bridge = SharedWorkerChannelBridge::new(id);
bridge.add_port();
let payload = StructuredClonePayload {
data: vec![],
transferable_count: 0,
};
let result = bridge.post_to_worker_from_port(99, payload);
assert!(result.is_err());
}
#[test]
fn test_webview_state_shared_worker_channel_registration() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let bridge = SharedWorkerChannelBridge::new(id.clone());
state.register_shared_worker_channel(bridge);
assert!(state.shared_worker_channel(&id).is_some());
assert_eq!(state.shared_worker_channel_count(), 0); }
#[test]
fn test_webview_state_create_shared_worker_channel() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
state.create_shared_worker_channel(id.clone());
assert!(state.shared_worker_channel(&id).is_some());
}
#[test]
fn test_webview_state_add_shared_worker_port() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let endpoints = state.add_shared_worker_port(id.clone());
assert_eq!(state.shared_worker_channel_count(), 1);
assert_eq!(endpoints.shared_worker_id, id);
assert!(endpoints.page_to_worker_rx.is_some());
assert!(endpoints.worker_to_page_tx.is_some());
}
#[test]
fn test_webview_state_add_shared_worker_port_multiple() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
state.add_shared_worker_port(id.clone());
state.add_shared_worker_port(id.clone());
assert_eq!(state.shared_worker_channel_count(), 2); }
#[test]
fn test_webview_state_drain_all_shared_worker_messages() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let endpoints = state.add_shared_worker_port(id);
let tx = endpoints.worker_to_page_tx.unwrap();
tx.send(WorkerStructuredMessage::metadata_only(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
let (messages, disconnected) = state.drain_all_shared_worker_messages();
assert_eq!(messages.len(), 1);
assert!(disconnected.is_empty());
}
#[test]
fn test_webview_state_drain_and_forward_shared_worker_messages() {
let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
let mut state = BaoWebViewState {
event_tx: Some(tx),
..Default::default()
};
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let endpoints = state.add_shared_worker_port(id);
let worker_tx = endpoints.worker_to_page_tx.unwrap();
worker_tx
.send(WorkerStructuredMessage::metadata_only(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
state.drain_and_forward_shared_worker_messages();
let event = rx.try_recv().unwrap();
match event {
ServoEvent::Console { text, .. } => {
assert!(text.contains("worker→page"));
}
_ => panic!("expected Console event for shared worker message"),
}
}
#[test]
fn test_webview_state_disconnect_shared_worker_clears_channels() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
state.track_shared_worker_port(SharedWorkerPortRef::new(SharedWorkerHandle::new(
"sw.js".to_string(),
"test".to_string(),
)));
state.add_shared_worker_port(id.clone());
assert_eq!(state.shared_worker_port_count(), 1);
assert_eq!(state.shared_worker_channel_count(), 1);
state.disconnect_shared_worker_ports();
assert_eq!(state.shared_worker_port_count(), 0);
assert_eq!(state.shared_worker_channel_count(), 0);
}
#[test]
fn test_webview_state_shared_worker_scope_registration() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let config = SharedWorkerScopeConfig::default();
let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
state.register_shared_worker_scope(id.clone(), scope);
assert_eq!(state.shared_worker_scope_count(), 1);
assert!(state.shared_worker_scope(&id).is_some());
}
#[test]
fn test_webview_state_shared_worker_scope_get_mut() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let config = SharedWorkerScopeConfig::default();
let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
state.register_shared_worker_scope(id.clone(), scope);
state.shared_worker_scope_mut(&id).unwrap().set_onconnect();
assert!(state.shared_worker_scope(&id).unwrap().has_onconnect);
}
#[test]
fn test_webview_state_shared_worker_scope_remove() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let config = SharedWorkerScopeConfig::default();
let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
state.register_shared_worker_scope(id.clone(), scope);
let removed = state.remove_shared_worker_scope(&id);
assert!(removed.is_some());
assert_eq!(state.shared_worker_scope_count(), 0);
}
#[test]
fn test_webview_state_shared_worker_scopes_snapshot() {
let mut state = BaoWebViewState::default();
let id1 = SharedWorkerId {
script_url: "sw1.js".to_string(),
name: "a".to_string(),
};
let id2 = SharedWorkerId {
script_url: "sw2.js".to_string(),
name: "b".to_string(),
};
let config = SharedWorkerScopeConfig::default();
state.register_shared_worker_scope(
id1,
SharedWorkerGlobalScopeState::new(
SharedWorkerId {
script_url: "sw1.js".to_string(),
name: "a".to_string(),
},
&config,
),
);
state.register_shared_worker_scope(
id2,
SharedWorkerGlobalScopeState::new(
SharedWorkerId {
script_url: "sw2.js".to_string(),
name: "b".to_string(),
},
&config,
),
);
let scopes = state.shared_worker_scopes();
assert_eq!(scopes.len(), 2);
}
#[test]
fn test_webview_state_disconnect_shared_worker_clears_scopes() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let config = SharedWorkerScopeConfig::default();
state.register_shared_worker_scope(
id,
SharedWorkerGlobalScopeState::new(
SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
},
&config,
),
);
assert_eq!(state.shared_worker_scope_count(), 1);
state.disconnect_shared_worker_ports();
assert_eq!(state.shared_worker_scope_count(), 0);
}
#[test]
fn test_webview_state_set_shared_worker_scope_config() {
let mut state = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "test".to_string(),
};
let config = SharedWorkerScopeConfig::default();
state.register_shared_worker_scope(
id.clone(),
SharedWorkerGlobalScopeState::new(id.clone(), &config),
);
assert!(state
.shared_worker_scope(&id)
.unwrap()
.navigator()
.user_agent
.is_empty());
let new_config = SharedWorkerScopeConfig {
stealth_profile: None,
user_agent: "Bao/1.0".to_string(),
platform: "Linux".to_string(),
hardware_concurrency: 8,
language: "en-US".to_string(),
languages: vec!["en-US".to_string()],
};
state.set_shared_worker_scope_config(&id, &new_config);
assert_eq!(
state
.shared_worker_scope(&id)
.unwrap()
.navigator()
.user_agent,
"Bao/1.0"
);
}
#[test]
fn test_delegate_route_shared_worker_new() {
let delegate = BaoServoDelegate::new();
let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
let (returned, is_new) = delegate.route_shared_worker(handle);
assert!(is_new);
assert_eq!(returned.script_url, "sw.js");
assert_eq!(delegate.shared_worker_count(), 1);
}
#[test]
fn test_delegate_route_shared_worker_existing() {
let delegate = BaoServoDelegate::new();
let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
delegate.route_shared_worker(handle1);
let (_, is_new) = delegate.route_shared_worker(handle2);
assert!(
!is_new,
"same (url, name) should return existing, not create new"
);
assert_eq!(delegate.shared_worker_count(), 1);
}
#[test]
fn test_delegate_get_or_create_shared_worker_new() {
let delegate = BaoServoDelegate::new();
let (handle, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
assert!(is_new);
assert_eq!(handle.script_url, "sw.js");
assert_eq!(handle.name, "myname");
}
#[test]
fn test_delegate_get_or_create_shared_worker_existing() {
let delegate = BaoServoDelegate::new();
delegate.get_or_create_shared_worker("sw.js", "myname");
let (_, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
assert!(!is_new);
assert_eq!(delegate.shared_worker_count(), 1);
}
#[test]
fn test_delegate_unregister_shared_worker() {
let delegate = BaoServoDelegate::new();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "myname".to_string(),
};
delegate.get_or_create_shared_worker("sw.js", "myname");
assert_eq!(delegate.shared_worker_count(), 1);
let removed = delegate.unregister_shared_worker(&id);
assert!(removed);
assert_eq!(delegate.shared_worker_count(), 0);
}
#[test]
fn test_delegate_unregister_nonexistent_shared_worker() {
let delegate = BaoServoDelegate::new();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "nonexistent".to_string(),
};
let removed = delegate.unregister_shared_worker(&id);
assert!(!removed);
}
#[test]
fn test_delegate_all_shared_workers() {
let delegate = BaoServoDelegate::new();
delegate.get_or_create_shared_worker("sw1.js", "a");
delegate.get_or_create_shared_worker("sw2.js", "b");
let all = delegate.all_shared_workers();
assert_eq!(all.len(), 2);
}
#[test]
fn test_shared_worker_cross_page_routing_full_lifecycle() {
let delegate = BaoServoDelegate::new();
let (handle, is_new) = delegate.route_shared_worker(SharedWorkerHandle::new(
"sw.js".to_string(),
"shared".to_string(),
));
assert!(is_new);
assert_eq!(handle.connected_page_count(), 0);
let mut state1 = BaoWebViewState::default();
let id = SharedWorkerId {
script_url: "sw.js".to_string(),
name: "shared".to_string(),
};
let config = SharedWorkerScopeConfig::default();
state1.register_shared_worker_scope(
id.clone(),
SharedWorkerGlobalScopeState::new(id.clone(), &config),
);
state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
assert_eq!(handle.connected_page_count(), 1);
let endpoints1 = state1.add_shared_worker_port(id.clone());
let mut state2 = BaoWebViewState::default();
state2.register_shared_worker_scope(
id.clone(),
SharedWorkerGlobalScopeState::new(id.clone(), &config),
);
state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
let endpoints2 = state2.add_shared_worker_port(id.clone());
assert_eq!(handle.connected_page_count(), 2);
let payload1 = StructuredClonePayload {
data: vec![1],
transferable_count: 0,
};
state1
.post_to_worker_via_shared_port(&id, 0, payload1)
.unwrap();
let payload2 = StructuredClonePayload {
data: vec![2],
transferable_count: 0,
};
state2
.post_to_worker_via_shared_port(&id, 0, payload2)
.unwrap();
let rx1 = endpoints1.page_to_worker_rx.unwrap();
let rx2 = endpoints2.page_to_worker_rx.unwrap();
assert_eq!(rx1.try_recv().unwrap().data, vec![1]);
assert_eq!(rx2.try_recv().unwrap().data, vec![2]);
let tx1 = endpoints1.worker_to_page_tx.unwrap();
let tx2 = endpoints2.worker_to_page_tx.unwrap();
tx1.send(WorkerStructuredMessage::metadata_only(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
tx2.send(WorkerStructuredMessage::metadata_only(
WorkerId("sw.js".to_string()),
WorkerMessageDirection::WorkerToPage,
))
.unwrap();
let (msgs1, disc1) = state1.drain_all_shared_worker_messages();
assert_eq!(msgs1.len(), 1);
assert!(disc1.is_empty());
let (msgs2, disc2) = state2.drain_all_shared_worker_messages();
assert_eq!(msgs2.len(), 1);
assert!(disc2.is_empty());
state1.disconnect_shared_worker_ports();
assert_eq!(handle.connected_page_count(), 1);
assert!(!handle.is_closing());
assert_eq!(state2.shared_worker_port_count(), 1);
handle.close();
handle.mark_terminated();
assert!(handle.is_closing());
assert!(handle.is_terminated());
state2.disconnect_shared_worker_ports();
assert_eq!(handle.connected_page_count(), 0);
delegate.reap_terminated_shared_workers();
assert_eq!(delegate.shared_worker_count(), 0);
}
#[test]
fn test_service_worker_registration_id_equality() {
let id1 = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let id2 = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let id3 = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/app/".to_string(),
};
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_service_worker_handle_lifecycle() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
assert!(!handle.is_closing());
assert!(!handle.is_terminated());
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installing
);
assert!(!handle.is_intercepting_fetch());
}
#[test]
fn test_service_worker_handle_state_transitions() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
handle.transition_state(ServiceWorkerRegistrationState::Installed);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installed
);
handle.transition_state(ServiceWorkerRegistrationState::Activating);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activating
);
handle.transition_state(ServiceWorkerRegistrationState::Activated);
handle.enable_fetch_interception();
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activated
);
assert!(handle.is_intercepting_fetch());
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::Intercepting
);
}
#[test]
fn test_service_worker_handle_terminate_disables_interception() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
handle.enable_fetch_interception();
assert!(handle.is_intercepting_fetch());
handle.terminate();
assert!(handle.is_closing());
assert!(!handle.is_intercepting_fetch());
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::None
);
}
#[test]
fn test_service_worker_scope_config_from_stealth_profile() {
let profile = bao_stealth::StealthProfile::chrome_default();
let config = ServiceWorkerScopeConfig::from(&profile);
assert!(config.stealth_profile.is_some());
assert_eq!(config.user_agent, profile.navigator.user_agent);
assert_eq!(config.platform, profile.navigator.platform);
assert_eq!(
config.hardware_concurrency,
profile.navigator.hardware_concurrency as usize
);
assert_eq!(config.language, profile.navigator.language);
}
#[test]
fn test_service_worker_global_scope_state() {
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/app/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let scope = ServiceWorkerGlobalScopeState::new(reg_id.clone(), &config);
assert!(!scope.has_fetch_handler);
assert!(!scope.has_activate_handler);
assert!(!scope.has_install_handler);
assert!(!scope.has_message_handler);
assert_eq!(scope.scope_url, "/app/");
assert!(scope.is_url_in_scope("/app/page1"));
assert!(scope.is_url_in_scope("/app/sub/page2"));
assert!(!scope.is_url_in_scope("/other/page"));
}
#[test]
fn test_service_worker_global_scope_fetch_handler() {
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let mut scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
scope.set_fetch_handler();
assert!(scope.has_fetch_handler);
assert!(scope.is_url_in_scope("/anything"));
}
#[test]
fn test_webview_state_service_worker_control() {
let mut state = BaoWebViewState::default();
assert!(!state.is_controlled_by_service_worker());
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
state.set_controlling_service_worker(handle);
assert!(state.is_controlled_by_service_worker());
state.clear_controlling_service_worker();
assert!(!state.is_controlled_by_service_worker());
}
#[test]
fn test_webview_state_service_worker_scope_matching() {
let mut state = BaoWebViewState::default();
assert!(!state.is_url_in_service_worker_scope("/app/page1"));
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/app/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/app/".to_string(), None);
state.set_controlling_service_worker(handle);
state.register_service_worker_scope(scope);
assert!(state.is_url_in_service_worker_scope("/app/page1"));
assert!(state.is_url_in_service_worker_scope("/app/sub/page2"));
assert!(!state.is_url_in_service_worker_scope("/other/page"));
}
#[test]
fn test_delegate_service_worker_registration() {
let delegate = BaoServoDelegate::new();
assert_eq!(delegate.service_worker_count(), 0);
let (handle, is_new) = delegate.get_or_create_service_worker("sw.js", "/", None);
assert!(is_new);
assert_eq!(delegate.service_worker_count(), 1);
let (handle2, is_new2) = delegate.get_or_create_service_worker("sw.js", "/", None);
assert!(!is_new2);
assert_eq!(delegate.service_worker_count(), 1);
}
#[test]
fn test_delegate_find_service_worker_for_url() {
let delegate = BaoServoDelegate::new();
let handle = delegate
.get_or_create_service_worker("sw.js", "/app/", None)
.0;
assert!(delegate.find_service_worker_for_url("/app/page1").is_none());
handle.transition_state(ServiceWorkerRegistrationState::Activated);
handle.enable_fetch_interception();
let found = delegate.find_service_worker_for_url("/app/page1");
assert!(found.is_some());
assert_eq!(found.unwrap().script_url, "sw.js");
assert!(delegate
.find_service_worker_for_url("/other/page")
.is_none());
}
#[test]
fn test_delegate_service_worker_unregistration() {
let delegate = BaoServoDelegate::new();
let (handle, _) = delegate.get_or_create_service_worker("sw.js", "/", None);
assert_eq!(delegate.service_worker_count(), 1);
let id = handle.id();
assert!(delegate.unregister_service_worker(&id));
assert_eq!(delegate.service_worker_count(), 0);
assert!(!delegate.unregister_service_worker(&id));
}
#[test]
fn test_delegate_service_worker_stealth_consistency_no_violations() {
let delegate = BaoServoDelegate::new();
let profile = bao_stealth::StealthProfile::chrome_default();
let handle =
ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), Some(profile.clone()));
delegate.register_service_worker(handle);
let all = delegate.all_service_workers();
all[0].transition_state(ServiceWorkerRegistrationState::Activated);
all[0].enable_fetch_interception();
let violations = delegate.verify_service_worker_stealth_consistency(&profile);
assert!(violations.is_empty());
}
#[test]
fn test_delegate_service_worker_stealth_consistency_violation_no_profile() {
let delegate = BaoServoDelegate::new();
let profile = bao_stealth::StealthProfile::chrome_default();
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
delegate.register_service_worker(handle);
let all = delegate.all_service_workers();
all[0].transition_state(ServiceWorkerRegistrationState::Activated);
all[0].enable_fetch_interception();
let violations = delegate.verify_service_worker_stealth_consistency(&profile);
assert_eq!(violations.len(), 1);
}
#[test]
fn test_service_worker_persistent_lifecycle_across_page_navigation() {
let delegate = BaoServoDelegate::new();
let profile = bao_stealth::StealthProfile::chrome_default();
let (handle, is_new) =
delegate.get_or_create_service_worker("sw.js", "/", Some(profile.clone()));
assert!(is_new);
handle.transition_state(ServiceWorkerRegistrationState::Activated);
handle.enable_fetch_interception();
let mut page_state = BaoWebViewState::default();
page_state.set_controlling_service_worker(handle.clone());
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let config = ServiceWorkerScopeConfig::from(&profile);
page_state
.register_service_worker_scope(ServiceWorkerGlobalScopeState::new(reg_id, &config));
assert!(page_state.is_controlled_by_service_worker());
page_state.clear_controlling_service_worker();
assert!(!page_state.is_controlled_by_service_worker());
assert_eq!(delegate.service_worker_count(), 1);
assert!(delegate.find_service_worker_for_url("/page2").is_some());
let mut page2_state = BaoWebViewState::default();
page2_state.set_controlling_service_worker(handle.clone());
assert!(page2_state.is_controlled_by_service_worker());
}
#[test]
fn test_service_worker_fetch_intercept_mode() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::None
);
handle.enable_fetch_interception();
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::Intercepting
);
handle.disable_fetch_interception();
assert_eq!(
handle.fetch_intercept_mode(),
ServiceWorkerFetchInterceptMode::None
);
}
#[test]
fn test_service_worker_registration_state_all_transitions() {
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installing
);
handle.transition_state(ServiceWorkerRegistrationState::Installed);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Installed
);
handle.transition_state(ServiceWorkerRegistrationState::Activating);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activating
);
handle.transition_state(ServiceWorkerRegistrationState::Activated);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Activated
);
handle.transition_state(ServiceWorkerRegistrationState::Redundant);
assert_eq!(
handle.registration_state(),
ServiceWorkerRegistrationState::Redundant
);
}
#[test]
fn test_service_worker_navigator_from_scope_config() {
let config = ServiceWorkerScopeConfig {
stealth_profile: None,
user_agent: "Mozilla/5.0 Test".to_string(),
platform: "Linux x86_64".to_string(),
hardware_concurrency: 4,
language: "zh-CN".to_string(),
languages: vec!["zh-CN".to_string(), "zh".to_string()],
registering_page_url: "https://example.com/".to_string(),
};
let nav = WorkerNavigator::from_service_scope_config(&config);
assert_eq!(nav.user_agent, "Mozilla/5.0 Test");
assert_eq!(nav.platform, "Linux x86_64");
assert_eq!(nav.hardware_concurrency, 4);
assert_eq!(nav.language, "zh-CN");
assert_eq!(nav.languages, vec!["zh-CN".to_string(), "zh".to_string()]);
}
#[test]
fn test_webview_state_service_worker_scope_config() {
let mut state = BaoWebViewState::default();
let reg_id = ServiceWorkerRegistrationId {
script_url: "sw.js".to_string(),
scope: "/".to_string(),
};
let config = ServiceWorkerScopeConfig::default();
let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
state.set_controlling_service_worker(handle);
state.register_service_worker_scope(scope);
let new_config = ServiceWorkerScopeConfig {
user_agent: "Updated Agent".to_string(),
..ServiceWorkerScopeConfig::default()
};
state.set_service_worker_scope_config(&new_config);
assert_eq!(
state.service_worker_scope().unwrap().navigator().user_agent,
"Updated Agent"
);
}
}