use super::*;
use bao_ipc::{Receiver, Sender};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use dpi::PhysicalSize;
use servo::{
AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
DeviceIntSize, EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, LoadStatus,
NavigationRequest, PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, TraversalId,
WebView, WebViewDelegate,
};
use bao_cdp::servo_bridge::main_frame_id_for_target;
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) = bao_ipc::unbounded::<StructuredClonePayload>();
let (worker_to_page_tx, worker_to_page_rx) =
bao_ipc::unbounded::<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<(), bao_ipc::SendError<StructuredClonePayload>> {
self.page_to_worker_tx.send(payload)
}
pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
try_recv_worker_msg(&self.worker_to_page_rx)
}
pub fn drain_worker_messages(&self) -> WorkerDrainResult {
drain_worker_rx(&self.worker_to_page_rx)
}
}
#[derive(Debug)]
pub struct WorkerDrainResult {
pub messages: Vec<WorkerStructuredMessage>,
pub disconnected: bool,
}
pub(super) fn try_recv_worker_msg(
rx: &Receiver<WorkerStructuredMessage>,
) -> Result<Option<WorkerStructuredMessage>, ()> {
match rx.try_recv() {
Ok(msg) => Ok(Some(msg)),
Err(bao_ipc::TryRecvError::Empty) => Ok(None),
Err(bao_ipc::TryRecvError::Disconnected) => Err(()),
}
}
pub(super) fn drain_worker_rx(rx: &Receiver<WorkerStructuredMessage>) -> WorkerDrainResult {
let mut messages = Vec::new();
let mut disconnected = false;
loop {
match rx.try_recv() {
Ok(msg) => messages.push(msg),
Err(bao_ipc::TryRecvError::Empty) => break,
Err(bao_ipc::TryRecvError::Disconnected) => {
disconnected = true;
break;
}
}
}
WorkerDrainResult {
messages,
disconnected,
}
}
pub struct WorkerChannelEndpoints {
pub worker_id: WorkerId,
pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
}
#[cfg(test)]
mod tests {
use super::*;
#[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) = bao_ipc::unbounded::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
cdp_target_id: Some("7".to_string()),
..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) = bao_ipc::unbounded::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
cdp_target_id: Some("7".to_string()),
..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_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) = bao_ipc::unbounded::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
cdp_target_id: Some("7".to_string()),
..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) = bao_ipc::unbounded::<ServoEvent>();
let state = BaoWebViewState {
event_tx: Some(tx),
cdp_target_id: Some("7".to_string()),
..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) = bao_ipc::unbounded::<ServoEvent>();
let mut state = BaoWebViewState {
event_tx: Some(tx),
cdp_target_id: Some("7".to_string()),
..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());
}
}