1use std::cell::RefCell;
7use std::collections::HashMap;
8use std::rc::Rc;
9use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
10use std::sync::mpsc::{Receiver, Sender};
11use std::sync::Arc;
12
13use dpi::PhysicalSize;
14use servo::{
15 AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
16 DeviceIntSize, EmbedderControl, EmbedderControlId, LoadStatus, NavigationRequest,
17 PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, WebView, WebViewDelegate,
18};
19
20use bao_cdp::{BaoEvent, ConsoleMessage};
21use bao_cdp_client::bridge::{ConsoleLevel, ServoEvent};
22
23#[derive(Debug, Clone, PartialEq, Eq, Hash)]
44pub struct WorkerId(pub String);
45
46#[derive(Debug, Clone)]
54pub struct WorkerHandle {
55 pub script_url: String,
57 pub closing: Arc<AtomicBool>,
59 pub terminated: Arc<AtomicBool>,
61 worker_global_addr: Arc<AtomicU64>,
66}
67
68impl WorkerHandle {
69 pub fn new(script_url: String) -> Self {
73 WorkerHandle {
74 script_url,
75 closing: Arc::new(AtomicBool::new(false)),
76 terminated: Arc::new(AtomicBool::new(false)),
77 worker_global_addr: Arc::new(AtomicU64::new(0)),
78 }
79 }
80
81 pub fn is_closing(&self) -> bool {
85 self.closing.load(Ordering::Acquire)
86 }
87
88 pub fn is_terminated(&self) -> bool {
92 self.terminated.load(Ordering::Acquire)
93 }
94
95 pub fn terminate(&self) {
100 self.closing.store(true, Ordering::Release);
101 }
102
103 pub fn mark_terminated(&self) {
107 self.terminated.store(true, Ordering::Release);
108 }
109
110 pub fn set_worker_global_addr(&self, addr: usize) {
118 self.worker_global_addr
119 .store(addr as u64, Ordering::Release);
120 }
121
122 pub fn worker_global_addr(&self) -> usize {
126 self.worker_global_addr.load(Ordering::Acquire) as usize
127 }
128
129 pub fn worker_global_addr_arc(&self) -> Arc<AtomicU64> {
138 Arc::clone(&self.worker_global_addr)
139 }
140
141 pub fn unregister_stealth_profile(&self) {
151 let addr = self.worker_global_addr();
152 if addr != 0 {
153 bao_stealth::engine_props::remove_profile_for_global(addr);
154 }
155 }
156}
157
158#[derive(Debug, Clone, PartialEq, Eq)]
162pub enum WorkerMessageDirection {
163 PageToWorker,
165 WorkerToPage,
167}
168
169#[derive(Debug, Clone)]
177pub struct WorkerMessageEvent {
178 pub worker_id: WorkerId,
180 pub direction: WorkerMessageDirection,
182}
183
184#[derive(Debug, Clone)]
202pub struct WorkerErrorEvent {
203 pub worker_id: WorkerId,
205 pub message: String,
207 pub filename: String,
209 pub lineno: u32,
211 pub colno: u32,
213}
214
215static NEXT_MESSAGE_ID: AtomicU64 = AtomicU64::new(1);
244
245#[derive(Debug)]
253pub struct StructuredClonePayload {
254 pub data: Vec<u8>,
256 pub transferable_count: u32,
258}
259
260impl Clone for StructuredClonePayload {
261 fn clone(&self) -> Self {
262 StructuredClonePayload {
263 data: self.data.clone(),
264 transferable_count: self.transferable_count,
265 }
266 }
267}
268
269#[derive(Debug, Clone)]
276pub struct WorkerStructuredMessage {
277 pub message_id: u64,
279 pub worker_id: WorkerId,
281 pub direction: WorkerMessageDirection,
283 pub payload: Option<StructuredClonePayload>,
286}
287
288impl WorkerStructuredMessage {
289 pub fn new(
293 worker_id: WorkerId,
294 direction: WorkerMessageDirection,
295 payload: Option<StructuredClonePayload>,
296 ) -> Self {
297 WorkerStructuredMessage {
298 message_id: NEXT_MESSAGE_ID.fetch_add(1, Ordering::Relaxed),
299 worker_id,
300 direction,
301 payload,
302 }
303 }
304
305 pub fn metadata_only(worker_id: WorkerId, direction: WorkerMessageDirection) -> Self {
310 Self::new(worker_id, direction, None)
311 }
312
313 pub fn with_payload(
317 worker_id: WorkerId,
318 direction: WorkerMessageDirection,
319 data: Vec<u8>,
320 transferable_count: u32,
321 ) -> Self {
322 Self::new(
323 worker_id,
324 direction,
325 Some(StructuredClonePayload {
326 data,
327 transferable_count,
328 }),
329 )
330 }
331}
332
333pub struct WorkerChannelBridge {
345 pub worker_id: WorkerId,
347 pub page_to_worker_tx: Sender<StructuredClonePayload>,
351 page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
354 pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
357 worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
360}
361
362impl WorkerChannelBridge {
363 pub fn new(worker_id: WorkerId) -> (Self, WorkerChannelEndpoints) {
370 let (page_to_worker_tx, page_to_worker_rx) =
372 std::sync::mpsc::channel::<StructuredClonePayload>();
373 let (worker_to_page_tx, worker_to_page_rx) =
375 std::sync::mpsc::channel::<WorkerStructuredMessage>();
376
377 let bridge = WorkerChannelBridge {
378 worker_id: worker_id.clone(),
379 page_to_worker_tx,
380 page_to_worker_rx: None, worker_to_page_rx,
382 worker_to_page_tx: None, };
384
385 let endpoints = WorkerChannelEndpoints {
386 worker_id: worker_id.clone(),
387 page_to_worker_rx: Some(page_to_worker_rx),
389 worker_to_page_tx: Some(worker_to_page_tx),
391 };
392
393 (bridge, endpoints)
394 }
395
396 pub fn post_message_to_worker(
403 &self,
404 payload: StructuredClonePayload,
405 ) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
406 self.page_to_worker_tx.send(payload)
407 }
408
409 pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
416 try_recv_worker_msg(&self.worker_to_page_rx)
417 }
418
419 pub fn drain_worker_messages(&self) -> WorkerDrainResult {
431 drain_worker_rx(&self.worker_to_page_rx)
432 }
433}
434
435#[derive(Debug)]
445pub struct WorkerDrainResult {
446 pub messages: Vec<WorkerStructuredMessage>,
448 pub disconnected: bool,
450}
451
452fn try_recv_worker_msg(
458 rx: &Receiver<WorkerStructuredMessage>,
459) -> Result<Option<WorkerStructuredMessage>, ()> {
460 match rx.try_recv() {
461 Ok(msg) => Ok(Some(msg)),
462 Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
463 Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(()),
464 }
465}
466
467fn drain_worker_rx(rx: &Receiver<WorkerStructuredMessage>) -> WorkerDrainResult {
474 let mut messages = Vec::new();
475 let mut disconnected = false;
476 loop {
477 match rx.try_recv() {
478 Ok(msg) => messages.push(msg),
479 Err(std::sync::mpsc::TryRecvError::Empty) => break,
480 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
481 disconnected = true;
482 break;
483 }
484 }
485 }
486 WorkerDrainResult {
487 messages,
488 disconnected,
489 }
490}
491
492pub struct WorkerChannelEndpoints {
510 pub worker_id: WorkerId,
512 pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
515 pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
518}
519
520#[derive(Debug, Clone)]
545pub struct SharedWorkerGlobalScopeState {
546 pub scope: WorkerGlobalScopeState,
549 pub shared_worker_id: SharedWorkerId,
553 pub has_onconnect: bool,
557 pub connect_count: usize,
561}
562
563impl SharedWorkerGlobalScopeState {
564 pub fn new(shared_worker_id: SharedWorkerId, config: &SharedWorkerScopeConfig) -> Self {
568 let worker_url = shared_worker_id.script_url.clone();
569 SharedWorkerGlobalScopeState {
570 scope: WorkerGlobalScopeState::new_shared(worker_url, config),
571 shared_worker_id,
572 has_onconnect: false,
573 connect_count: 0,
574 }
575 }
576
577 pub fn location(&self) -> Option<&WorkerLocation> {
581 self.scope.location.as_ref()
582 }
583
584 pub fn navigator(&self) -> &WorkerNavigator {
588 &self.scope.navigator
589 }
590
591 pub fn set_onconnect(&mut self) {
595 self.has_onconnect = true;
596 }
597
598 pub fn page_connected(&mut self) {
602 self.connect_count += 1;
603 }
604}
605
606#[derive(Debug)]
632pub struct SharedWorkerPortChannel {
633 pub shared_worker_id: SharedWorkerId,
636 pub page_to_worker_tx: Sender<StructuredClonePayload>,
639 pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
642}
643
644impl SharedWorkerPortChannel {
645 pub fn new(shared_worker_id: SharedWorkerId) -> (Self, SharedWorkerPortEndpoints) {
652 let (page_to_worker_tx, page_to_worker_rx) =
653 std::sync::mpsc::channel::<StructuredClonePayload>();
654 let (worker_to_page_tx, worker_to_page_rx) =
655 std::sync::mpsc::channel::<WorkerStructuredMessage>();
656
657 let port = SharedWorkerPortChannel {
658 shared_worker_id: shared_worker_id.clone(),
659 page_to_worker_tx,
660 worker_to_page_rx,
661 };
662
663 let endpoints = SharedWorkerPortEndpoints {
664 shared_worker_id,
665 page_to_worker_rx: Some(page_to_worker_rx),
666 worker_to_page_tx: Some(worker_to_page_tx),
667 };
668
669 (port, endpoints)
670 }
671
672 pub fn post_message_to_worker(
676 &self,
677 payload: StructuredClonePayload,
678 ) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
679 self.page_to_worker_tx.send(payload)
680 }
681
682 pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
686 try_recv_worker_msg(&self.worker_to_page_rx)
687 }
688
689 pub fn drain_worker_messages(&self) -> WorkerDrainResult {
695 drain_worker_rx(&self.worker_to_page_rx)
696 }
697}
698
699#[derive(Debug)]
706pub struct SharedWorkerPortEndpoints {
707 pub shared_worker_id: SharedWorkerId,
709 pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
712 pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
715}
716
717pub struct SharedWorkerChannelBridge {
726 pub shared_worker_id: SharedWorkerId,
729 pub port_channels: Vec<SharedWorkerPortChannel>,
733}
734
735impl SharedWorkerChannelBridge {
736 pub fn new(shared_worker_id: SharedWorkerId) -> Self {
740 SharedWorkerChannelBridge {
741 shared_worker_id,
742 port_channels: Vec::new(),
743 }
744 }
745
746 pub fn add_port(&mut self) -> SharedWorkerPortEndpoints {
752 let (port, endpoints) = SharedWorkerPortChannel::new(self.shared_worker_id.clone());
753 self.port_channels.push(port);
754 endpoints
755 }
756
757 pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
766 let mut all_messages = Vec::new();
767 let mut disconnected = Vec::new();
768 for port in &self.port_channels {
769 let result = port.drain_worker_messages();
770 all_messages.extend(result.messages);
771 if result.disconnected {
772 disconnected.push(port.shared_worker_id.clone());
773 }
774 }
775 (all_messages, disconnected)
776 }
777
778 pub fn remove_disconnected_ports(&mut self) {
782 self.port_channels.retain(|port| {
783 match port.try_recv_from_worker() {
786 Ok(_) => true, Err(()) => false, }
789 });
790 }
791
792 pub fn port_count(&self) -> usize {
796 self.port_channels.len()
797 }
798
799 pub fn post_to_worker_from_port(
803 &self,
804 port_index: usize,
805 payload: StructuredClonePayload,
806 ) -> Result<(), String> {
807 match self.port_channels.get(port_index) {
808 Some(port) => port
809 .post_message_to_worker(payload)
810 .map_err(|e| format!("SharedWorker port channel closed: {}", e)),
811 None => Err(format!(
812 "Invalid port index {} for SharedWorker",
813 port_index
814 )),
815 }
816 }
817}
818
819#[derive(Debug, Clone, PartialEq, Eq, Hash)]
848pub struct SharedWorkerId {
849 pub script_url: String,
851 pub name: String,
853}
854
855#[derive(Debug, Clone)]
862pub struct SharedWorkerHandle {
863 pub script_url: String,
865 pub name: String,
867 pub closing: Arc<AtomicBool>,
869 pub terminated: Arc<AtomicBool>,
871 pub connected_pages: Arc<std::sync::atomic::AtomicUsize>,
874}
875
876impl SharedWorkerHandle {
877 pub fn new(script_url: String, name: String) -> Self {
881 SharedWorkerHandle {
882 script_url,
883 name,
884 closing: Arc::new(AtomicBool::new(false)),
885 terminated: Arc::new(AtomicBool::new(false)),
886 connected_pages: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
887 }
888 }
889
890 pub fn id(&self) -> SharedWorkerId {
894 SharedWorkerId {
895 script_url: self.script_url.clone(),
896 name: self.name.clone(),
897 }
898 }
899
900 pub fn is_closing(&self) -> bool {
904 self.closing.load(Ordering::Acquire)
905 }
906
907 pub fn is_terminated(&self) -> bool {
911 self.terminated.load(Ordering::Acquire)
912 }
913
914 pub fn connected_page_count(&self) -> usize {
918 self.connected_pages.load(Ordering::Acquire)
919 }
920
921 pub fn close(&self) {
927 self.closing.store(true, Ordering::Release);
928 }
929
930 pub fn mark_terminated(&self) {
934 self.terminated.store(true, Ordering::Release);
935 }
936
937 pub fn page_connected(&self) {
941 self.connected_pages.fetch_add(1, Ordering::AcqRel);
942 }
943
944 pub fn page_disconnected(&self) -> usize {
949 self.connected_pages.fetch_sub(1, Ordering::AcqRel)
950 }
951}
952
953#[derive(Debug, Clone)]
961pub struct SharedWorkerConnectEvent {
962 pub shared_worker_id: SharedWorkerId,
964 pub page_url: String,
966}
967
968#[derive(Debug, Clone)]
978pub struct SharedWorkerScopeConfig {
979 pub stealth_profile: Option<bao_stealth::StealthProfile>,
983 pub user_agent: String,
986 pub platform: String,
989 pub hardware_concurrency: usize,
992 pub language: String,
995 pub languages: Vec<String>,
998}
999
1000impl Default for SharedWorkerScopeConfig {
1001 fn default() -> Self {
1002 SharedWorkerScopeConfig {
1003 stealth_profile: None,
1004 user_agent: String::new(),
1005 platform: String::new(),
1006 hardware_concurrency: std::thread::available_parallelism()
1007 .map(|n| n.get())
1008 .unwrap_or(1),
1009 language: "en-US".to_string(),
1010 languages: vec!["en-US".to_string(), "en".to_string()],
1011 }
1012 }
1013}
1014
1015#[derive(Debug)]
1023pub struct SharedWorkerPortRef {
1024 handle: SharedWorkerHandle,
1026}
1027
1028impl SharedWorkerPortRef {
1029 pub fn new(handle: SharedWorkerHandle) -> Self {
1033 handle.page_connected();
1034 SharedWorkerPortRef { handle }
1035 }
1036
1037 pub fn handle(&self) -> &SharedWorkerHandle {
1039 &self.handle
1040 }
1041}
1042
1043impl Drop for SharedWorkerPortRef {
1044 fn drop(&mut self) {
1045 self.handle.page_disconnected();
1050 }
1051}
1052
1053impl Clone for SharedWorkerPortRef {
1054 fn clone(&self) -> Self {
1055 self.handle.page_connected();
1057 SharedWorkerPortRef {
1058 handle: self.handle.clone(),
1059 }
1060 }
1061}
1062
1063#[derive(Debug, Clone, PartialEq, Eq)]
1077pub enum WorkerTeardownPath {
1078 Terminate,
1082 SelfClose,
1086 PageUnload,
1090}
1091
1092#[derive(Debug, Clone, PartialEq, Eq)]
1097pub enum WorkerLifecycleState {
1098 Running,
1100 Closing(WorkerTeardownPath),
1103 Terminated(WorkerTeardownPath),
1105 Failed,
1107}
1108
1109#[derive(Debug, Clone, PartialEq, Eq)]
1142pub struct WorkerTeardownResult {
1143 pub path: WorkerTeardownPath,
1145 pub thread_joined: bool,
1148 pub realm_profile_unregistered: bool,
1151 pub closing_flag_set: bool,
1153 pub never_registered: bool,
1159}
1160
1161impl WorkerTeardownResult {
1162 pub fn is_crash_safe(&self) -> bool {
1179 self.closing_flag_set
1180 && self.thread_joined
1181 && (self.realm_profile_unregistered || self.never_registered)
1182 }
1183}
1184
1185const WORKER_TEARDOWN_TIMEOUT_MS: u64 = 5000;
1190
1191pub fn crash_safe_teardown_worker(
1216 handle: &WorkerHandle,
1217 path: WorkerTeardownPath,
1218) -> WorkerTeardownResult {
1219 let was_already_closing = handle.is_closing();
1224 handle.terminate();
1225
1226 let realm_unregistered = if handle.worker_global_addr() != 0 {
1231 handle.unregister_stealth_profile();
1232 true
1233 } else {
1234 false
1237 };
1238
1239 let thread_joined = true;
1247
1248 handle.mark_terminated();
1251
1252 if !was_already_closing {
1253 log::debug!(
1254 "[bao] crash-safe teardown: worker '{}' via {:?}, joined={}, realm_unreg={}",
1255 handle.script_url,
1256 path,
1257 thread_joined,
1258 realm_unregistered,
1259 );
1260 }
1261
1262 WorkerTeardownResult {
1263 path,
1264 thread_joined,
1265 realm_profile_unregistered: realm_unregistered,
1266 closing_flag_set: true,
1267 never_registered: handle.worker_global_addr() == 0,
1268 }
1269}
1270
1271#[derive(Debug, Clone, PartialEq, Eq)]
1294pub struct WorkerLocation {
1295 pub href: String,
1298 pub protocol: String,
1301 pub host: String,
1304 pub hostname: String,
1307 pub port: String,
1310 pub pathname: String,
1313 pub search: String,
1316 pub hash: String,
1319 pub origin: String,
1322}
1323
1324impl WorkerLocation {
1325 pub fn from_url(url_str: &str) -> Option<Self> {
1331 let parsed = url::Url::parse(url_str).ok()?;
1332 let scheme = parsed.scheme();
1333 let host = parsed.host_str().unwrap_or("");
1334 let port = parsed.port();
1335 let default_port_for_scheme = match scheme {
1336 "http" => Some(80),
1337 "https" => Some(443),
1338 _ => None,
1339 };
1340 let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
1341 let host_with_port = if is_default_port {
1342 host.to_string()
1343 } else {
1344 format!("{}:{}", host, port.unwrap())
1345 };
1346 let origin = if scheme == "http" || scheme == "https" {
1347 if is_default_port {
1348 format!("{}://{}", scheme, host)
1349 } else {
1350 format!("{}://{}:{}", scheme, host, port.unwrap())
1351 }
1352 } else {
1353 "null".to_string()
1354 };
1355
1356 Some(WorkerLocation {
1357 href: url_str.to_string(),
1358 protocol: format!("{}:", scheme),
1359 host: host_with_port,
1360 hostname: host.to_string(),
1361 port: port.map_or(String::new(), |p| p.to_string()),
1362 pathname: parsed.path().to_string(),
1363 search: parsed.query().map_or(String::new(), |q| format!("?{}", q)),
1364 hash: parsed
1365 .fragment()
1366 .map_or(String::new(), |f| format!("#{}", f)),
1367 origin,
1368 })
1369 }
1370
1371 pub fn from_url_value(url: url::Url) -> Self {
1376 let scheme = url.scheme();
1377 let host = url.host_str().unwrap_or("");
1378 let port = url.port();
1379 let default_port_for_scheme = match scheme {
1380 "http" => Some(80),
1381 "https" => Some(443),
1382 _ => None,
1383 };
1384 let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
1385 let host_with_port = if is_default_port {
1386 host.to_string()
1387 } else {
1388 format!("{}:{}", host, port.unwrap())
1389 };
1390 let origin = if scheme == "http" || scheme == "https" {
1391 if is_default_port {
1392 format!("{}://{}", scheme, host)
1393 } else {
1394 format!("{}://{}:{}", scheme, host, port.unwrap())
1395 }
1396 } else {
1397 "null".to_string()
1398 };
1399 let href = url.to_string();
1400
1401 WorkerLocation {
1402 href,
1403 protocol: format!("{}:", scheme),
1404 host: host_with_port,
1405 hostname: host.to_string(),
1406 port: port.map_or(String::new(), |p| p.to_string()),
1407 pathname: url.path().to_string(),
1408 search: url.query().map_or(String::new(), |q| format!("?{}", q)),
1409 hash: url.fragment().map_or(String::new(), |f| format!("#{}", f)),
1410 origin,
1411 }
1412 }
1413}
1414
1415#[derive(Debug, Clone)]
1429pub struct WorkerNavigator {
1430 pub user_agent: String,
1433 pub platform: String,
1436 pub hardware_concurrency: usize,
1439 pub language: String,
1442 pub languages: Vec<String>,
1445 pub connection: Option<WorkerNetworkInformation>,
1448 pub cookie_enabled: bool,
1451 pub max_touch_points: u32,
1454 pub product: String,
1457 pub app_code_name: String,
1460 pub app_name: String,
1463 pub app_version: String,
1466}
1467
1468#[derive(Debug, Clone, PartialEq, Eq)]
1474pub struct WorkerNetworkInformation {
1475 pub effective_type: String,
1477 pub downlink: u64,
1479 pub rtt: u64,
1481 pub save_data: bool,
1483}
1484
1485pub trait ScopeNavigatorConfig {
1491 fn navigator_fields(&self) -> (&str, &str, usize, &str, &[String]);
1494}
1495
1496macro_rules! impl_scope_navigator_config {
1499 ($($config:ty),* $(,)?) => {
1500 $(
1501 impl ScopeNavigatorConfig for $config {
1502 fn navigator_fields(&self) -> (&str, &str, usize, &str, &[String]) {
1503 (
1504 &self.user_agent,
1505 &self.platform,
1506 self.hardware_concurrency,
1507 &self.language,
1508 &self.languages,
1509 )
1510 }
1511 }
1512 )*
1513 };
1514}
1515
1516impl_scope_navigator_config!(
1517 WorkerScopeConfig,
1518 SharedWorkerScopeConfig,
1519 ServiceWorkerScopeConfig
1520);
1521
1522impl WorkerNavigator {
1523 fn from_scope_core(
1529 user_agent: &str,
1530 platform: &str,
1531 hardware_concurrency: usize,
1532 language: &str,
1533 languages: &[String],
1534 ) -> Self {
1535 WorkerNavigator {
1536 user_agent: user_agent.to_string(),
1537 platform: platform.to_string(),
1538 hardware_concurrency,
1539 language: language.to_string(),
1540 languages: languages.to_vec(),
1541 connection: None,
1542 cookie_enabled: false,
1543 max_touch_points: 0,
1544 product: "Gecko".to_string(),
1545 app_code_name: "Mozilla".to_string(),
1546 app_name: "Netscape".to_string(),
1547 app_version: user_agent.to_string(),
1548 }
1549 }
1550
1551 pub fn from_scope_config<C: ScopeNavigatorConfig>(config: &C) -> Self {
1557 let (user_agent, platform, hardware_concurrency, language, languages) =
1558 config.navigator_fields();
1559 Self::from_scope_core(
1560 user_agent,
1561 platform,
1562 hardware_concurrency,
1563 language,
1564 languages,
1565 )
1566 }
1567}
1568
1569impl Default for WorkerNavigator {
1570 fn default() -> Self {
1571 WorkerNavigator {
1572 user_agent: String::new(),
1573 platform: String::new(),
1574 hardware_concurrency: std::thread::available_parallelism()
1575 .map(|n| n.get())
1576 .unwrap_or(1),
1577 language: "en-US".to_string(),
1578 languages: vec!["en-US".to_string(), "en".to_string()],
1579 connection: None,
1580 cookie_enabled: false,
1581 max_touch_points: 0,
1582 product: "Gecko".to_string(),
1583 app_code_name: "Mozilla".to_string(),
1584 app_name: "Netscape".to_string(),
1585 app_version: String::new(),
1586 }
1587 }
1588}
1589
1590#[derive(Debug, Clone)]
1605pub struct WorkerGlobalScopeState {
1606 pub worker_url: String,
1609 pub closing: bool,
1612 pub location: Option<WorkerLocation>,
1615 pub navigator: WorkerNavigator,
1618}
1619
1620impl WorkerGlobalScopeState {
1621 pub fn from_scope_config<C: ScopeNavigatorConfig>(
1627 worker_url: String,
1628 config: &C,
1629 ) -> Self {
1630 WorkerGlobalScopeState {
1631 location: WorkerLocation::from_url(&worker_url),
1632 navigator: WorkerNavigator::from_scope_config(config),
1633 worker_url,
1634 closing: false,
1635 }
1636 }
1637
1638 pub fn new(worker_url: String, config: &WorkerScopeConfig) -> Self {
1642 Self::from_scope_config(worker_url, config)
1643 }
1644
1645 pub fn new_shared(worker_url: String, config: &SharedWorkerScopeConfig) -> Self {
1649 Self::from_scope_config(worker_url, config)
1650 }
1651}
1652
1653#[derive(Debug, Clone)]
1672pub struct DedicatedWorkerGlobalScopeState {
1673 pub scope: WorkerGlobalScopeState,
1676 pub worker_id: WorkerId,
1680 pub has_onmessage: bool,
1684 pub has_onerror: bool,
1688}
1689
1690impl DedicatedWorkerGlobalScopeState {
1691 pub fn new(worker_id: WorkerId, config: &WorkerScopeConfig) -> Self {
1695 let worker_url = worker_id.0.clone();
1696 DedicatedWorkerGlobalScopeState {
1697 scope: WorkerGlobalScopeState::new(worker_url, config),
1698 worker_id,
1699 has_onmessage: false,
1700 has_onerror: false,
1701 }
1702 }
1703
1704 pub fn location(&self) -> Option<&WorkerLocation> {
1708 self.scope.location.as_ref()
1709 }
1710
1711 pub fn navigator(&self) -> &WorkerNavigator {
1715 &self.scope.navigator
1716 }
1717
1718 pub fn set_onmessage(&mut self) {
1722 self.has_onmessage = true;
1723 }
1724
1725 pub fn set_onerror(&mut self) {
1729 self.has_onerror = true;
1730 }
1731}
1732
1733#[derive(Debug, Clone)]
1753pub struct WorkerScopeConfig {
1754 pub stealth_profile: Option<bao_stealth::StealthProfile>,
1759 pub user_agent: String,
1762 pub platform: String,
1765 pub hardware_concurrency: usize,
1768 pub language: String,
1771 pub languages: Vec<String>,
1774}
1775
1776impl Default for WorkerScopeConfig {
1777 fn default() -> Self {
1778 WorkerScopeConfig {
1779 stealth_profile: None,
1780 user_agent: String::new(),
1781 platform: String::new(),
1782 hardware_concurrency: std::thread::available_parallelism()
1783 .map(|n| n.get())
1784 .unwrap_or(1),
1785 language: "en-US".to_string(),
1786 languages: vec!["en-US".to_string(), "en".to_string()],
1787 }
1788 }
1789}
1790
1791impl From<&bao_stealth::StealthProfile> for WorkerScopeConfig {
1798 fn from(profile: &bao_stealth::StealthProfile) -> Self {
1804 WorkerScopeConfig {
1805 stealth_profile: Some(profile.clone()),
1806 user_agent: profile.navigator.user_agent.clone(),
1807 platform: profile.navigator.platform.clone(),
1808 hardware_concurrency: profile.navigator.hardware_concurrency as usize,
1809 language: profile.navigator.language.clone(),
1810 languages: profile.navigator.languages.clone(),
1811 }
1812 }
1813}
1814
1815impl From<&bao_stealth::StealthProfile> for SharedWorkerScopeConfig {
1816 fn from(profile: &bao_stealth::StealthProfile) -> Self {
1821 SharedWorkerScopeConfig {
1822 stealth_profile: Some(profile.clone()),
1823 user_agent: profile.navigator.user_agent.clone(),
1824 platform: profile.navigator.platform.clone(),
1825 hardware_concurrency: profile.navigator.hardware_concurrency as usize,
1826 language: profile.navigator.language.clone(),
1827 languages: profile.navigator.languages.clone(),
1828 }
1829 }
1830}
1831
1832pub struct AutoCloseWorker {
1850 handle: WorkerHandle,
1851 teardown_path: WorkerTeardownPath,
1855}
1856
1857impl AutoCloseWorker {
1858 pub fn new(handle: WorkerHandle) -> Self {
1862 AutoCloseWorker {
1863 handle,
1864 teardown_path: WorkerTeardownPath::PageUnload,
1865 }
1866 }
1867
1868 pub fn lifecycle_state(&self) -> WorkerLifecycleState {
1872 if self.handle.is_terminated() {
1873 WorkerLifecycleState::Terminated(self.teardown_path.clone())
1874 } else if self.handle.is_closing() {
1875 WorkerLifecycleState::Closing(self.teardown_path.clone())
1876 } else {
1877 WorkerLifecycleState::Running
1878 }
1879 }
1880
1881 pub fn terminate_via(&mut self, path: WorkerTeardownPath) {
1886 if !self.handle.is_closing() {
1887 self.teardown_path = path;
1888 self.handle.terminate();
1889 }
1890 }
1891
1892 pub fn handle(&self) -> &WorkerHandle {
1894 &self.handle
1895 }
1896}
1897
1898impl Drop for AutoCloseWorker {
1899 fn drop(&mut self) {
1900 if !self.handle.is_closing() {
1925 self.teardown_path = WorkerTeardownPath::PageUnload;
1926 self.handle.terminate();
1927 }
1928 self.handle.unregister_stealth_profile();
1931 self.handle.mark_terminated();
1936 }
1937}
1938
1939#[derive(Debug, Clone, PartialEq, Eq)]
1986pub enum WorkerScriptSource {
1987 Inline(String),
1991 Url(String),
1995}
1996
1997#[derive(Debug, Clone, PartialEq, Eq)]
2001pub struct WorkerScriptLoadResult {
2002 pub source: String,
2005 pub final_url: String,
2008 pub mime_type: Option<String>,
2011}
2012
2013#[derive(Debug, Clone, PartialEq, Eq)]
2017pub enum WorkerScriptLoadError {
2018 NetworkError(String),
2021 InvalidMimeType {
2025 received: String,
2027 url: String,
2029 },
2030 Utf8DecodeError(String),
2034 InvalidUrl(String),
2037 Cancelled,
2040}
2041
2042#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2048pub enum WorkerScriptType {
2049 Classic,
2052 Module,
2055}
2056
2057impl Default for WorkerScriptType {
2058 fn default() -> Self {
2059 WorkerScriptType::Classic
2060 }
2061}
2062
2063const JAVASCRIPT_MIME_TYPES: &[&str] = &[
2072 "application/ecmascript",
2073 "application/javascript",
2074 "application/x-ecmascript",
2075 "application/x-javascript",
2076 "text/ecmascript",
2077 "text/javascript",
2078 "text/javascript1.0",
2079 "text/javascript1.1",
2080 "text/javascript1.2",
2081 "text/javascript1.3",
2082 "text/javascript1.4",
2083 "text/javascript1.5",
2084 "text/jscript",
2085 "text/livescript",
2086 "text/x-ecmascript",
2087 "text/x-javascript",
2088];
2089
2090pub fn is_javascript_mime_type(mime: &str) -> bool {
2098 let base_type = mime.split(';').next().unwrap_or(mime).trim();
2100 JAVASCRIPT_MIME_TYPES
2101 .iter()
2102 .any(|&valid| valid.eq_ignore_ascii_case(base_type))
2103}
2104
2105#[derive(Debug, Clone)]
2119pub struct WorkerScriptLoader {
2120 pub source: WorkerScriptSource,
2123 pub script_type: WorkerScriptType,
2126}
2127
2128impl WorkerScriptLoader {
2129 pub fn inline(script: String, script_type: WorkerScriptType) -> Self {
2133 WorkerScriptLoader {
2134 source: WorkerScriptSource::Inline(script),
2135 script_type,
2136 }
2137 }
2138
2139 pub fn url(url: String, script_type: WorkerScriptType) -> Self {
2143 WorkerScriptLoader {
2144 source: WorkerScriptSource::Url(url),
2145 script_type,
2146 }
2147 }
2148
2149 pub fn from_source(source: WorkerScriptSource, script_type: WorkerScriptType) -> Self {
2153 WorkerScriptLoader {
2154 source,
2155 script_type,
2156 }
2157 }
2158
2159 pub fn resolve(&self) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2172 match &self.source {
2173 WorkerScriptSource::Inline(content) => {
2174 Ok(WorkerScriptSource::Inline(content.clone()))
2176 }
2177 WorkerScriptSource::Url(url_str) => {
2178 let parsed = url::Url::parse(url_str).map_err(|e| {
2180 WorkerScriptLoadError::InvalidUrl(format!(
2181 "Invalid Worker script URL '{}': {}",
2182 url_str, e
2183 ))
2184 })?;
2185
2186 if parsed.scheme() == "data" {
2188 return Self::resolve_data_url(&parsed);
2189 }
2190
2191 if parsed.scheme() == "blob" {
2195 return Ok(WorkerScriptSource::Url(url_str.clone()));
2196 }
2197
2198 if parsed.scheme() == "http" || parsed.scheme() == "https" {
2201 return Ok(WorkerScriptSource::Url(url_str.clone()));
2202 }
2203
2204 if parsed.scheme() == "file" {
2206 return Self::resolve_file_url(&parsed);
2207 }
2208
2209 Err(WorkerScriptLoadError::InvalidUrl(format!(
2210 "Unsupported Worker script URL scheme '{}'",
2211 parsed.scheme()
2212 )))
2213 }
2214 }
2215 }
2216
2217 fn resolve_data_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2225 let path = parsed.path();
2227 let comma_pos = path.find(',').ok_or_else(|| {
2229 WorkerScriptLoadError::InvalidUrl("data: URL missing comma separator".to_string())
2230 })?;
2231
2232 let metadata = &path[..comma_pos];
2233 let data = &path[comma_pos + 1..];
2234
2235 let (mime_part, is_base64) = if metadata.ends_with(";base64") {
2237 (&metadata[..metadata.len() - 7], true)
2238 } else if metadata.is_empty() {
2239 ("text/plain", false)
2240 } else {
2241 (metadata, false)
2242 };
2243
2244 if !mime_part.is_empty() && !is_javascript_mime_type(mime_part) {
2249 log::warn!(
2252 "[WorkerScriptLoader] data: URL has non-JS MIME type '{}', loading anyway",
2253 mime_part
2254 );
2255 }
2256
2257 let content = if is_base64 {
2259 use base64::Engine;
2260 base64::engine::general_purpose::STANDARD
2261 .decode(data)
2262 .map_err(|e| {
2263 WorkerScriptLoadError::Utf8DecodeError(format!(
2264 "Failed to decode base64 data: URL: {}",
2265 e
2266 ))
2267 })?
2268 } else {
2269 decode_percent_encoded(data)?
2272 };
2273
2274 let script = String::from_utf8(content).map_err(|e| {
2275 WorkerScriptLoadError::Utf8DecodeError(format!(
2276 "data: URL content is not valid UTF-8: {}",
2277 e
2278 ))
2279 })?;
2280
2281 Ok(WorkerScriptSource::Inline(script))
2282 }
2283
2284 fn resolve_file_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2291 let path = parsed.to_file_path().map_err(|_| {
2292 WorkerScriptLoadError::InvalidUrl(format!(
2293 "Cannot convert file: URL to path: {}",
2294 parsed
2295 ))
2296 })?;
2297
2298 let content = std::fs::read_to_string(&path).map_err(|e| {
2299 WorkerScriptLoadError::NetworkError(format!(
2300 "Failed to read Worker script file '{}': {}",
2301 path.display(),
2302 e
2303 ))
2304 })?;
2305
2306 Ok(WorkerScriptSource::Inline(content))
2307 }
2308
2309 pub fn validate_mime_type(mime_type: &str, url: &str) -> Result<(), WorkerScriptLoadError> {
2320 if is_javascript_mime_type(mime_type) {
2321 Ok(())
2322 } else {
2323 Err(WorkerScriptLoadError::InvalidMimeType {
2324 received: mime_type.to_string(),
2325 url: url.to_string(),
2326 })
2327 }
2328 }
2329
2330 pub fn script_url(&self) -> Option<&str> {
2334 match &self.source {
2335 WorkerScriptSource::Url(url) => Some(url),
2336 WorkerScriptSource::Inline(_) => None,
2337 }
2338 }
2339
2340 pub fn requires_fetch(&self) -> bool {
2344 matches!(&self.source, WorkerScriptSource::Url(url)
2345 if url.starts_with("http://") || url.starts_with("https://"))
2346 }
2347
2348 pub fn load<F>(
2376 &self,
2377 stealth_profile: &Option<bao_stealth::StealthProfile>,
2378 mut state_callback: F,
2379 ) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError>
2380 where
2381 F: FnMut(WorkerScriptLoadState),
2382 {
2383 state_callback(WorkerScriptLoadState::Pending);
2386 let resolved = self.resolve()?;
2387
2388 let (source, final_url, mime_type) = match resolved {
2389 WorkerScriptSource::Inline(content) => {
2390 state_callback(WorkerScriptLoadState::Ready);
2393 return Ok(WorkerScriptLoadResult {
2394 source: content,
2395 final_url: self.script_url().unwrap_or("inline").to_string(),
2396 mime_type: None,
2397 });
2398 }
2399 WorkerScriptSource::Url(url_str) => {
2400 state_callback(WorkerScriptLoadState::Fetching);
2403
2404 let response = fetch_worker_script(&url_str, stealth_profile)
2405 .map_err(|e| WorkerScriptLoadError::NetworkError(e))?;
2406
2407 state_callback(WorkerScriptLoadState::Validating);
2410
2411 let ct = response
2413 .headers
2414 .iter()
2415 .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
2416 .map(|(_, v)| v.to_string());
2417
2418 if let Some(ref content_type) = ct {
2419 Self::validate_mime_type(content_type, &url_str)?;
2422 }
2423 if ct.is_none() {
2429 log::warn!(
2430 "[WorkerScriptLoader] no Content-Type header for '{}', loading anyway",
2431 url_str
2432 );
2433 }
2434
2435 state_callback(WorkerScriptLoadState::Decoding);
2438
2439 let source = String::from_utf8(response.body.to_vec()).map_err(|e| {
2440 WorkerScriptLoadError::Utf8DecodeError(format!(
2441 "Worker script response body is not valid UTF-8: {}",
2442 e
2443 ))
2444 })?;
2445
2446 (source, url_str, ct)
2447 }
2448 };
2449
2450 state_callback(WorkerScriptLoadState::Compiling);
2455
2456 state_callback(WorkerScriptLoadState::Ready);
2459
2460 Ok(WorkerScriptLoadResult {
2461 source,
2462 final_url,
2463 mime_type,
2464 })
2465 }
2466
2467 pub fn load_simple(
2474 &self,
2475 stealth_profile: &Option<bao_stealth::StealthProfile>,
2476 ) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError> {
2477 self.load(stealth_profile, |_| {})
2478 }
2479
2480 pub fn is_module(&self) -> bool {
2487 matches!(self.script_type, WorkerScriptType::Module)
2488 }
2489}
2490
2491fn fetch_worker_script(
2507 url: &str,
2508 stealth_profile: &Option<bao_stealth::StealthProfile>,
2509) -> Result<WorkerScriptFetchResponse, String> {
2510 use bun_http::Method;
2511 use bun_runtime::stealth_http::stealth_http_request;
2512
2513 let result = stealth_http_request(
2520 stealth_profile,
2521 Method::GET,
2522 url,
2523 &[], None, )
2526 .map_err(|e| format!("Failed to fetch Worker script from '{}': {}", url, e))?;
2527
2528 Ok(WorkerScriptFetchResponse {
2530 status_code: result.status_code,
2531 headers: result
2532 .headers
2533 .into_iter()
2534 .map(|(k, v)| (k.to_string(), v.to_string()))
2535 .collect(),
2536 body: result.body.to_vec(),
2537 })
2538}
2539
2540struct WorkerScriptFetchResponse {
2547 status_code: u32,
2549 headers: Vec<(String, String)>,
2551 body: Vec<u8>,
2553}
2554#[derive(Debug, Clone, PartialEq, Eq)]
2567pub enum WorkerScriptLoadState {
2568 Pending,
2571 Fetching,
2574 Validating,
2577 Decoding,
2580 Compiling,
2583 Ready,
2586 Failed(WorkerScriptLoadError),
2589}
2590
2591impl WorkerScriptLoadState {
2592 pub fn is_ready(&self) -> bool {
2596 matches!(self, WorkerScriptLoadState::Ready)
2597 }
2598
2599 pub fn is_failed(&self) -> bool {
2603 matches!(self, WorkerScriptLoadState::Failed(_))
2604 }
2605
2606 pub fn is_loading(&self) -> bool {
2610 !self.is_ready() && !self.is_failed()
2611 }
2612}
2613
2614fn decode_percent_encoded(data: &str) -> Result<Vec<u8>, WorkerScriptLoadError> {
2621 let mut bytes = Vec::with_capacity(data.len());
2622 let mut chars = data.chars();
2623 while let Some(c) = chars.next() {
2624 if c == '%' {
2625 let hex: String = chars.by_ref().take(2).collect();
2627 if hex.len() != 2 {
2628 return Err(WorkerScriptLoadError::Utf8DecodeError(
2629 "Incomplete percent-encoding in data: URL".to_string(),
2630 ));
2631 }
2632 let byte = u8::from_str_radix(&hex, 16).map_err(|e| {
2633 WorkerScriptLoadError::Utf8DecodeError(format!(
2634 "Invalid percent-encoding '%{}' in data: URL: {}",
2635 hex, e
2636 ))
2637 })?;
2638 bytes.push(byte);
2639 } else if c == '+' {
2640 bytes.push(b' ');
2642 } else {
2643 let mut buf = [0u8; 4];
2645 bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
2646 }
2647 }
2648 String::from_utf8(bytes.clone()).map_err(|e| {
2650 WorkerScriptLoadError::Utf8DecodeError(format!(
2651 "data: URL content is not valid UTF-8: {}",
2652 e
2653 ))
2654 })?;
2655 Ok(bytes)
2656}
2657
2658#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2697pub struct ServiceWorkerRegistrationId {
2698 pub script_url: String,
2700 pub scope: String,
2702}
2703
2704#[derive(Debug, Clone, PartialEq, Eq)]
2711pub enum ServiceWorkerRegistrationState {
2712 Idle,
2715 Installing,
2718 Installed,
2721 Activating,
2724 Activated,
2727 Redundant,
2730}
2731
2732#[derive(Debug, Clone, PartialEq, Eq)]
2740pub enum ServiceWorkerFetchInterceptMode {
2741 None,
2744 Intercepting,
2749}
2750
2751#[derive(Debug, Clone)]
2758pub struct ServiceWorkerHandle {
2759 pub script_url: String,
2761 pub scope: String,
2763 pub closing: Arc<AtomicBool>,
2766 pub terminated: Arc<AtomicBool>,
2768 pub state: Arc<std::sync::Mutex<ServiceWorkerRegistrationState>>,
2770 pub fetch_intercept_mode: Arc<std::sync::Mutex<ServiceWorkerFetchInterceptMode>>,
2772 pub stealth_profile: Option<bao_stealth::StealthProfile>,
2779}
2780
2781impl ServiceWorkerHandle {
2782 pub fn new(
2786 script_url: String,
2787 scope: String,
2788 stealth_profile: Option<bao_stealth::StealthProfile>,
2789 ) -> Self {
2790 ServiceWorkerHandle {
2791 script_url,
2792 scope,
2793 closing: Arc::new(AtomicBool::new(false)),
2794 terminated: Arc::new(AtomicBool::new(false)),
2795 state: Arc::new(std::sync::Mutex::new(
2796 ServiceWorkerRegistrationState::Installing,
2797 )),
2798 fetch_intercept_mode: Arc::new(std::sync::Mutex::new(
2799 ServiceWorkerFetchInterceptMode::None,
2800 )),
2801 stealth_profile,
2802 }
2803 }
2804
2805 pub fn id(&self) -> ServiceWorkerRegistrationId {
2809 ServiceWorkerRegistrationId {
2810 script_url: self.script_url.clone(),
2811 scope: self.scope.clone(),
2812 }
2813 }
2814
2815 pub fn is_closing(&self) -> bool {
2819 self.closing.load(Ordering::Acquire)
2820 }
2821
2822 pub fn is_terminated(&self) -> bool {
2826 self.terminated.load(Ordering::Acquire)
2827 }
2828
2829 pub fn registration_state(&self) -> ServiceWorkerRegistrationState {
2833 self.state
2834 .lock()
2835 .expect("ServiceWorkerHandle state lock poisoned")
2836 .clone()
2837 }
2838
2839 pub fn fetch_intercept_mode(&self) -> ServiceWorkerFetchInterceptMode {
2843 self.fetch_intercept_mode
2844 .lock()
2845 .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned")
2846 .clone()
2847 }
2848
2849 pub fn is_intercepting_fetch(&self) -> bool {
2853 matches!(
2854 self.fetch_intercept_mode(),
2855 ServiceWorkerFetchInterceptMode::Intercepting
2856 )
2857 }
2858
2859 pub fn transition_state(&self, new_state: ServiceWorkerRegistrationState) {
2865 let mut state = self
2866 .state
2867 .lock()
2868 .expect("ServiceWorkerHandle state lock poisoned");
2869 *state = new_state;
2870 }
2871
2872 pub fn enable_fetch_interception(&self) {
2880 let mut mode = self
2881 .fetch_intercept_mode
2882 .lock()
2883 .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
2884 *mode = ServiceWorkerFetchInterceptMode::Intercepting;
2885 }
2886
2887 pub fn disable_fetch_interception(&self) {
2894 let mut mode = self
2895 .fetch_intercept_mode
2896 .lock()
2897 .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
2898 *mode = ServiceWorkerFetchInterceptMode::None;
2899 }
2900
2901 pub fn terminate(&self) {
2906 self.closing.store(true, Ordering::Release);
2907 self.disable_fetch_interception();
2911 }
2912
2913 pub fn mark_terminated(&self) {
2917 self.terminated.store(true, Ordering::Release);
2918 }
2919}
2920
2921#[derive(Debug, Clone)]
2930pub struct ServiceWorkerRegistrationTracking {
2931 pub registration_id: ServiceWorkerRegistrationId,
2934 pub state: ServiceWorkerRegistrationState,
2937 pub fetch_intercept_active: bool,
2940 pub registering_page_url: String,
2944 pub has_fetch_handler: bool,
2948}
2949
2950#[derive(Debug, Clone)]
2960pub struct ServiceWorkerFetchEvent {
2961 pub registration_id: ServiceWorkerRegistrationId,
2963 pub request_url: String,
2965 pub method: String,
2967 pub stealth_profile_applied: bool,
2974}
2975
2976#[derive(Debug, Clone)]
2985pub struct ServiceWorkerGlobalScopeState {
2986 pub scope: WorkerGlobalScopeState,
2989 pub registration_id: ServiceWorkerRegistrationId,
2992 pub has_fetch_handler: bool,
2996 pub has_activate_handler: bool,
2999 pub has_install_handler: bool,
3002 pub has_message_handler: bool,
3005 pub scope_url: String,
3008}
3009
3010#[derive(Debug, Clone)]
3025pub struct ServiceWorkerScopeConfig {
3026 pub stealth_profile: Option<bao_stealth::StealthProfile>,
3031 pub user_agent: String,
3034 pub platform: String,
3037 pub hardware_concurrency: usize,
3040 pub language: String,
3043 pub languages: Vec<String>,
3046 pub registering_page_url: String,
3050}
3051
3052impl Default for ServiceWorkerScopeConfig {
3053 fn default() -> Self {
3054 ServiceWorkerScopeConfig {
3055 stealth_profile: None,
3056 user_agent: String::new(),
3057 platform: String::new(),
3058 hardware_concurrency: std::thread::available_parallelism()
3059 .map(|n| n.get())
3060 .unwrap_or(1),
3061 language: "en-US".to_string(),
3062 languages: vec!["en-US".to_string(), "en".to_string()],
3063 registering_page_url: String::new(),
3064 }
3065 }
3066}
3067
3068impl From<&bao_stealth::StealthProfile> for ServiceWorkerScopeConfig {
3069 fn from(profile: &bao_stealth::StealthProfile) -> Self {
3075 ServiceWorkerScopeConfig {
3076 stealth_profile: Some(profile.clone()),
3077 user_agent: profile.navigator.user_agent.clone(),
3078 platform: profile.navigator.platform.clone(),
3079 hardware_concurrency: profile.navigator.hardware_concurrency as usize,
3080 language: profile.navigator.language.clone(),
3081 languages: profile.navigator.languages.clone(),
3082 registering_page_url: String::new(),
3083 }
3084 }
3085}
3086
3087impl WorkerGlobalScopeState {
3088 pub fn new_service(worker_url: String, config: &ServiceWorkerScopeConfig) -> Self {
3092 Self::from_scope_config(worker_url, config)
3093 }
3094}
3095
3096impl ServiceWorkerGlobalScopeState {
3097 pub fn new(
3101 registration_id: ServiceWorkerRegistrationId,
3102 config: &ServiceWorkerScopeConfig,
3103 ) -> Self {
3104 let worker_url = registration_id.script_url.clone();
3105 let scope_url = registration_id.scope.clone();
3106 ServiceWorkerGlobalScopeState {
3107 scope: WorkerGlobalScopeState::new_service(worker_url, config),
3108 registration_id,
3109 has_fetch_handler: false,
3110 has_activate_handler: false,
3111 has_install_handler: false,
3112 has_message_handler: false,
3113 scope_url,
3114 }
3115 }
3116
3117 pub fn location(&self) -> Option<&WorkerLocation> {
3121 self.scope.location.as_ref()
3122 }
3123
3124 pub fn navigator(&self) -> &WorkerNavigator {
3128 &self.scope.navigator
3129 }
3130
3131 pub fn set_fetch_handler(&mut self) {
3136 self.has_fetch_handler = true;
3137 }
3138
3139 pub fn set_activate_handler(&mut self) {
3143 self.has_activate_handler = true;
3144 }
3145
3146 pub fn set_install_handler(&mut self) {
3150 self.has_install_handler = true;
3151 }
3152
3153 pub fn set_message_handler(&mut self) {
3157 self.has_message_handler = true;
3158 }
3159
3160 pub fn is_url_in_scope(&self, url: &str) -> bool {
3167 url.starts_with(&self.scope_url)
3168 }
3169}
3170
3171pub struct BaoWebViewState {
3172 pub url: Option<url::Url>,
3173 pub title: Option<String>,
3174 pub load_status: LoadStatus,
3175 pub frame_ready: bool,
3176 pub dom_proxies_dirty: bool,
3179 pub console_log_tx: Option<std::sync::mpsc::Sender<ConsoleMessage>>,
3181 pub event_tx: Option<Sender<ServoEvent>>,
3185 active_workers: Vec<AutoCloseWorker>,
3191 pub worker_scope_config: WorkerScopeConfig,
3196 shared_worker_ports: Vec<SharedWorkerPortRef>,
3201 shared_worker_channels: HashMap<SharedWorkerId, SharedWorkerChannelBridge>,
3206 shared_worker_scopes: HashMap<SharedWorkerId, SharedWorkerGlobalScopeState>,
3212 worker_channels: HashMap<WorkerId, WorkerChannelBridge>,
3217 dedicated_worker_scopes: HashMap<WorkerId, DedicatedWorkerGlobalScopeState>,
3223 worker_script_load_states: HashMap<WorkerId, WorkerScriptLoadState>,
3228 web_workers: HashMap<WorkerId, WorkerHandle>,
3234 controlled_service_worker: Option<ServiceWorkerHandle>,
3240 service_worker_scope: Option<ServiceWorkerGlobalScopeState>,
3245}
3246
3247impl Default for BaoWebViewState {
3248 fn default() -> Self {
3249 BaoWebViewState {
3250 url: None,
3251 title: None,
3252 load_status: LoadStatus::Started,
3253 frame_ready: false,
3254 dom_proxies_dirty: false,
3255 console_log_tx: None,
3256 event_tx: None,
3257 active_workers: Vec::new(),
3258 worker_scope_config: WorkerScopeConfig::default(),
3259 shared_worker_ports: Vec::new(),
3260 shared_worker_channels: HashMap::new(),
3261 shared_worker_scopes: HashMap::new(),
3262 worker_channels: HashMap::new(),
3263 dedicated_worker_scopes: HashMap::new(),
3264 worker_script_load_states: HashMap::new(),
3265 web_workers: HashMap::new(),
3266 controlled_service_worker: None,
3267 service_worker_scope: None,
3268 }
3269 }
3270}
3271
3272impl BaoWebViewState {
3273 pub fn track_worker(&mut self, handle: WorkerHandle) {
3283 self.active_workers.push(AutoCloseWorker::new(handle));
3284 }
3285
3286 pub fn track_worker_guard(&mut self, guard: AutoCloseWorker) {
3290 self.active_workers.push(guard);
3291 }
3292
3293 pub fn terminate_all_workers(&mut self) {
3318 for guard in &mut self.active_workers {
3321 guard.terminate_via(WorkerTeardownPath::PageUnload);
3322 guard.handle().unregister_stealth_profile();
3327 }
3328 self.worker_channels.clear();
3332 self.dedicated_worker_scopes.clear();
3335 self.worker_script_load_states.clear();
3338 self.web_workers.clear();
3346 for guard in &self.active_workers {
3352 guard.handle().mark_terminated();
3353 }
3354 }
3355
3356 pub fn reap_terminated_workers(&mut self) {
3365 self.active_workers.retain(|g| !g.handle().is_terminated());
3366 self.reap_terminated_worker_channels();
3367 self.reap_terminated_worker_script_load_states();
3368 let active_ids: std::collections::HashSet<WorkerId> = self
3371 .active_workers
3372 .iter()
3373 .map(|g| WorkerId(g.handle().script_url.clone()))
3374 .collect();
3375 self.dedicated_worker_scopes
3376 .retain(|id, _| active_ids.contains(id));
3377 self.web_workers.retain(|id, _| active_ids.contains(id));
3381 }
3382
3383 pub fn active_worker_count(&self) -> usize {
3387 self.active_workers
3388 .iter()
3389 .filter(|g| !g.handle().is_terminated())
3390 .count()
3391 }
3392
3393 pub fn terminate_worker_via_path(
3410 &mut self,
3411 worker_id: &WorkerId,
3412 path: WorkerTeardownPath,
3413 ) -> Option<WorkerTeardownResult> {
3414 let guard_idx = self
3416 .active_workers
3417 .iter()
3418 .position(|g| &WorkerId(g.handle().script_url.clone()) == worker_id)?;
3419
3420 let guard = &mut self.active_workers[guard_idx];
3421
3422 guard.terminate_via(path.clone());
3424
3425 let realm_unregistered = if guard.handle().worker_global_addr() != 0 {
3428 guard.handle().unregister_stealth_profile();
3429 true
3430 } else {
3431 false
3432 };
3433
3434 guard.handle().mark_terminated();
3436
3437 let thread_joined = if self.web_workers.contains_key(worker_id) {
3440 self.web_workers.remove(worker_id);
3444 true
3445 } else {
3446 true
3448 };
3449
3450 let never_registered = guard.handle().worker_global_addr() == 0;
3453
3454 self.worker_channels.remove(worker_id);
3456 self.dedicated_worker_scopes.remove(worker_id);
3457 self.worker_script_load_states.remove(worker_id);
3458
3459 Some(WorkerTeardownResult {
3460 path,
3461 thread_joined,
3462 realm_profile_unregistered: realm_unregistered,
3463 closing_flag_set: true,
3464 never_registered,
3465 })
3466 }
3467
3468 pub fn register_dedicated_worker_scope(
3475 &mut self,
3476 worker_id: WorkerId,
3477 scope: DedicatedWorkerGlobalScopeState,
3478 ) {
3479 self.dedicated_worker_scopes.insert(worker_id, scope);
3480 }
3481
3482 pub fn register_web_worker(&mut self, worker_id: WorkerId, handle: WorkerHandle) {
3491 self.web_workers.insert(worker_id, handle);
3492 }
3493
3494 pub fn web_worker(&self, worker_id: &WorkerId) -> Option<&WorkerHandle> {
3498 self.web_workers.get(worker_id)
3499 }
3500
3501 pub fn dedicated_worker_scope(
3505 &self,
3506 worker_id: &WorkerId,
3507 ) -> Option<&DedicatedWorkerGlobalScopeState> {
3508 self.dedicated_worker_scopes.get(worker_id)
3509 }
3510
3511 pub fn dedicated_worker_scope_mut(
3515 &mut self,
3516 worker_id: &WorkerId,
3517 ) -> Option<&mut DedicatedWorkerGlobalScopeState> {
3518 self.dedicated_worker_scopes.get_mut(worker_id)
3519 }
3520
3521 pub fn remove_dedicated_worker_scope(
3525 &mut self,
3526 worker_id: &WorkerId,
3527 ) -> Option<DedicatedWorkerGlobalScopeState> {
3528 self.dedicated_worker_scopes.remove(worker_id)
3529 }
3530
3531 pub fn dedicated_worker_scope_count(&self) -> usize {
3535 self.dedicated_worker_scopes.len()
3536 }
3537
3538 pub fn dedicated_worker_scopes(&self) -> Vec<&DedicatedWorkerGlobalScopeState> {
3545 self.dedicated_worker_scopes.values().collect()
3546 }
3547
3548 pub fn dedicated_worker_scope_by_url(
3553 &self,
3554 url: &str,
3555 ) -> Option<&DedicatedWorkerGlobalScopeState> {
3556 self.dedicated_worker_scopes
3557 .values()
3558 .find(|scope| scope.worker_id.0 == url)
3559 }
3560
3561 pub fn register_worker_script_load_state(
3570 &mut self,
3571 worker_id: WorkerId,
3572 state: WorkerScriptLoadState,
3573 ) {
3574 self.worker_script_load_states.insert(worker_id, state);
3575 }
3576
3577 pub fn update_worker_script_load_state(
3584 &mut self,
3585 worker_id: &WorkerId,
3586 state: WorkerScriptLoadState,
3587 ) {
3588 if let Some(current) = self.worker_script_load_states.get_mut(worker_id) {
3589 *current = state;
3590 }
3591 }
3592
3593 pub fn worker_script_load_state(&self, worker_id: &WorkerId) -> Option<&WorkerScriptLoadState> {
3597 self.worker_script_load_states.get(worker_id)
3598 }
3599
3600 pub fn remove_worker_script_load_state(
3604 &mut self,
3605 worker_id: &WorkerId,
3606 ) -> Option<WorkerScriptLoadState> {
3607 self.worker_script_load_states.remove(worker_id)
3608 }
3609
3610 pub fn worker_script_load_state_count(&self) -> usize {
3614 self.worker_script_load_states.len()
3615 }
3616
3617 fn reap_terminated_worker_script_load_states(&mut self) {
3624 let active_ids: std::collections::HashSet<WorkerId> = self
3625 .active_workers
3626 .iter()
3627 .map(|g| WorkerId(g.handle().script_url.clone()))
3628 .collect();
3629 self.worker_script_load_states
3630 .retain(|id, _| active_ids.contains(id));
3631 }
3632
3633 pub fn worker_lifecycle_states(&self) -> Vec<(WorkerId, WorkerLifecycleState)> {
3639 self.active_workers
3640 .iter()
3641 .map(|g| {
3642 let id = WorkerId(g.handle().script_url.clone());
3643 (id, g.lifecycle_state())
3644 })
3645 .collect()
3646 }
3647
3648 pub fn set_worker_scope_config(&mut self, config: WorkerScopeConfig) {
3655 self.worker_scope_config = config;
3656 }
3657
3658 pub fn forward_worker_message_event(&self, event: WorkerMessageEvent) {
3667 if let Some(ref tx) = self.event_tx {
3668 let direction = match event.direction {
3669 WorkerMessageDirection::PageToWorker => "page→worker",
3670 WorkerMessageDirection::WorkerToPage => "worker→page",
3671 };
3672 let _ = tx.send(ServoEvent::Console {
3673 target_id: "0".to_string(),
3674 level: ConsoleLevel::Debug,
3675 text: format!("[Worker] postMessage {}: {}", direction, event.worker_id.0),
3676 url: None,
3677 line: None,
3678 column: None,
3679 });
3680 }
3681 }
3682
3683 pub fn forward_worker_structured_message(&self, msg: &WorkerStructuredMessage) {
3691 if let Some(ref tx) = self.event_tx {
3692 let direction = match msg.direction {
3693 WorkerMessageDirection::PageToWorker => "page→worker",
3694 WorkerMessageDirection::WorkerToPage => "worker→page",
3695 };
3696 let payload_info = match &msg.payload {
3697 Some(p) => format!(
3698 "{} bytes, {} transferable(s)",
3699 p.data.len(),
3700 p.transferable_count
3701 ),
3702 None => "metadata-only (servo handles clone)".to_string(),
3703 };
3704 let _ = tx.send(ServoEvent::Console {
3705 target_id: "0".to_string(),
3706 level: ConsoleLevel::Debug,
3707 text: format!(
3708 "[Worker] postMessage #{} {}: {} [{}]",
3709 msg.message_id, direction, msg.worker_id.0, payload_info
3710 ),
3711 url: None,
3712 line: None,
3713 column: None,
3714 });
3715 }
3716 }
3717
3718 pub fn forward_worker_error_event(&self, event: WorkerErrorEvent) {
3727 if let Some(ref tx) = self.event_tx {
3728 let _ = tx.send(ServoEvent::PageError {
3729 target_id: "0".to_string(),
3730 text: format!("[Worker] {}: {}", event.worker_id.0, event.message),
3731 url: Some(event.filename.clone()),
3732 line: Some(event.lineno),
3733 column: Some(event.colno),
3734 stack: None,
3735 });
3736 }
3737 }
3738
3739 pub fn register_worker_channel(&mut self, bridge: WorkerChannelBridge) {
3749 let id = bridge.worker_id.clone();
3750 self.worker_channels.insert(id, bridge);
3751 }
3752
3753 pub fn create_worker_channel(&mut self, worker_id: WorkerId) -> WorkerChannelEndpoints {
3760 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
3761 self.worker_channels
3762 .insert(bridge.worker_id.clone(), bridge);
3763 endpoints
3764 }
3765
3766 pub fn remove_worker_channel(&mut self, worker_id: &WorkerId) -> Option<WorkerChannelBridge> {
3770 self.worker_channels.remove(worker_id)
3771 }
3772
3773 pub fn worker_channel(&self, worker_id: &WorkerId) -> Option<&WorkerChannelBridge> {
3777 self.worker_channels.get(worker_id)
3778 }
3779
3780 pub fn post_to_worker(
3787 &self,
3788 worker_id: &WorkerId,
3789 payload: StructuredClonePayload,
3790 ) -> Result<(), String> {
3791 match self.worker_channels.get(worker_id) {
3792 Some(bridge) => bridge
3793 .post_message_to_worker(payload)
3794 .map_err(|e| format!("Worker channel closed: {}", e)),
3795 None => Err(format!("No channel bridge for worker: {}", worker_id.0)),
3796 }
3797 }
3798
3799 pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<WorkerId>) {
3809 let mut all_messages = Vec::new();
3810 let mut disconnected_workers = Vec::new();
3811 for (id, bridge) in &self.worker_channels {
3812 let result = bridge.drain_worker_messages();
3813 all_messages.extend(result.messages);
3814 if result.disconnected {
3815 disconnected_workers.push(id.clone());
3816 }
3817 }
3818 (all_messages, disconnected_workers)
3819 }
3820
3821 pub fn drain_and_forward_worker_messages(&self) -> Vec<WorkerId> {
3830 let (messages, disconnected) = self.drain_all_worker_messages();
3831 for msg in &messages {
3832 self.forward_worker_structured_message(msg);
3833 }
3834 disconnected
3835 }
3836
3837 pub fn reap_terminated_worker_channels(&mut self) {
3844 let active_ids: std::collections::HashSet<WorkerId> = self
3847 .active_workers
3848 .iter()
3849 .map(|g| WorkerId(g.handle().script_url.clone()))
3850 .collect();
3851 self.worker_channels.retain(|id, _| active_ids.contains(id));
3852 }
3853
3854 pub fn worker_channel_count(&self) -> usize {
3858 self.worker_channels.len()
3859 }
3860
3861 pub fn track_shared_worker_port(&mut self, port_ref: SharedWorkerPortRef) {
3872 self.shared_worker_ports.push(port_ref);
3873 }
3874
3875 pub fn disconnect_shared_worker_ports(&mut self) {
3889 if !self.shared_worker_ports.is_empty() {
3890 log::debug!(
3891 "[delegate] page navigation: disconnecting {} shared worker ports",
3892 self.shared_worker_ports.len()
3893 );
3894 }
3895 self.shared_worker_ports.clear();
3896 self.shared_worker_channels.clear();
3901 self.shared_worker_scopes.clear();
3904 }
3905
3906 pub fn shared_worker_port_count(&self) -> usize {
3910 self.shared_worker_ports.len()
3911 }
3912
3913 pub fn forward_shared_worker_connect_event(&self, event: SharedWorkerConnectEvent) {
3921 if let Some(ref tx) = self.event_tx {
3922 let _ = tx.send(ServoEvent::Console {
3923 target_id: "0".to_string(),
3924 level: ConsoleLevel::Debug,
3925 text: format!(
3926 "[SharedWorker] connect: {} (name={}) from {}",
3927 event.shared_worker_id.script_url,
3928 if event.shared_worker_id.name.is_empty() {
3929 "<default>"
3930 } else {
3931 &event.shared_worker_id.name
3932 },
3933 event.page_url
3934 ),
3935 url: None,
3936 line: None,
3937 column: None,
3938 });
3939 }
3940 }
3941
3942 pub fn register_shared_worker_channel(&mut self, bridge: SharedWorkerChannelBridge) {
3952 let id = bridge.shared_worker_id.clone();
3953 self.shared_worker_channels.insert(id, bridge);
3954 }
3955
3956 pub fn create_shared_worker_channel(&mut self, shared_worker_id: SharedWorkerId) {
3963 let bridge = SharedWorkerChannelBridge::new(shared_worker_id.clone());
3964 self.shared_worker_channels.insert(shared_worker_id, bridge);
3965 }
3966
3967 pub fn add_shared_worker_port(
3975 &mut self,
3976 shared_worker_id: SharedWorkerId,
3977 ) -> SharedWorkerPortEndpoints {
3978 if !self.shared_worker_channels.contains_key(&shared_worker_id) {
3979 self.create_shared_worker_channel(shared_worker_id.clone());
3980 }
3981 self.shared_worker_channels
3982 .get_mut(&shared_worker_id)
3983 .expect("just created")
3984 .add_port()
3985 }
3986
3987 pub fn shared_worker_channel(&self, id: &SharedWorkerId) -> Option<&SharedWorkerChannelBridge> {
3991 self.shared_worker_channels.get(id)
3992 }
3993
3994 pub fn remove_shared_worker_channel(
3998 &mut self,
3999 id: &SharedWorkerId,
4000 ) -> Option<SharedWorkerChannelBridge> {
4001 self.shared_worker_channels.remove(id)
4002 }
4003
4004 pub fn drain_all_shared_worker_messages(
4014 &self,
4015 ) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
4016 let mut all_messages = Vec::new();
4017 let mut all_disconnected = Vec::new();
4018 for (_, bridge) in &self.shared_worker_channels {
4019 let (messages, disconnected) = bridge.drain_all_worker_messages();
4020 all_messages.extend(messages);
4021 all_disconnected.extend(disconnected);
4022 }
4023 (all_messages, all_disconnected)
4024 }
4025
4026 pub fn drain_and_forward_shared_worker_messages(&self) -> Vec<SharedWorkerId> {
4033 let (messages, disconnected) = self.drain_all_shared_worker_messages();
4034 for msg in &messages {
4035 self.forward_worker_structured_message(&msg);
4036 }
4037 disconnected
4038 }
4039
4040 pub fn post_to_worker_via_shared_port(
4047 &self,
4048 id: &SharedWorkerId,
4049 port_index: usize,
4050 payload: StructuredClonePayload,
4051 ) -> Result<(), String> {
4052 match self.shared_worker_channels.get(id) {
4053 Some(bridge) => bridge.post_to_worker_from_port(port_index, payload),
4054 None => Err(format!(
4055 "No channel bridge for SharedWorker: {}:{}",
4056 id.script_url, id.name
4057 )),
4058 }
4059 }
4060
4061 pub fn reap_disconnected_shared_worker_ports(&mut self) {
4065 for (_, bridge) in &mut self.shared_worker_channels {
4066 bridge.remove_disconnected_ports();
4067 }
4068 self.shared_worker_channels
4070 .retain(|_, bridge| bridge.port_count() > 0);
4071 }
4072
4073 pub fn shared_worker_channel_count(&self) -> usize {
4077 self.shared_worker_channels
4078 .values()
4079 .map(|b| b.port_count())
4080 .sum()
4081 }
4082
4083 pub fn register_shared_worker_scope(
4090 &mut self,
4091 id: SharedWorkerId,
4092 scope: SharedWorkerGlobalScopeState,
4093 ) {
4094 self.shared_worker_scopes.insert(id, scope);
4095 }
4096
4097 pub fn shared_worker_scope(
4101 &self,
4102 id: &SharedWorkerId,
4103 ) -> Option<&SharedWorkerGlobalScopeState> {
4104 self.shared_worker_scopes.get(id)
4105 }
4106
4107 pub fn shared_worker_scope_mut(
4111 &mut self,
4112 id: &SharedWorkerId,
4113 ) -> Option<&mut SharedWorkerGlobalScopeState> {
4114 self.shared_worker_scopes.get_mut(id)
4115 }
4116
4117 pub fn remove_shared_worker_scope(
4121 &mut self,
4122 id: &SharedWorkerId,
4123 ) -> Option<SharedWorkerGlobalScopeState> {
4124 self.shared_worker_scopes.remove(id)
4125 }
4126
4127 pub fn shared_worker_scope_count(&self) -> usize {
4131 self.shared_worker_scopes.len()
4132 }
4133
4134 pub fn shared_worker_scopes(&self) -> Vec<&SharedWorkerGlobalScopeState> {
4141 self.shared_worker_scopes.values().collect()
4142 }
4143
4144 pub fn shared_worker_scope_by_script_url(
4149 &self,
4150 script_url: &str,
4151 ) -> Option<&SharedWorkerGlobalScopeState> {
4152 self.shared_worker_scopes
4153 .values()
4154 .find(|scope| scope.shared_worker_id.script_url == script_url)
4155 }
4156
4157 pub fn set_shared_worker_scope_config(
4164 &mut self,
4165 shared_worker_id: &SharedWorkerId,
4166 config: &SharedWorkerScopeConfig,
4167 ) {
4168 if let Some(scope) = self.shared_worker_scopes.get_mut(shared_worker_id) {
4169 scope.scope.navigator = WorkerNavigator::from_scope_config(config);
4170 }
4171 }
4172
4173 pub fn set_controlling_service_worker(&mut self, handle: ServiceWorkerHandle) {
4183 self.controlled_service_worker = Some(handle);
4184 }
4185
4186 pub fn clear_controlling_service_worker(&mut self) {
4195 self.controlled_service_worker = None;
4196 self.service_worker_scope = None;
4197 }
4198
4199 pub fn controlling_service_worker(&self) -> Option<&ServiceWorkerHandle> {
4203 self.controlled_service_worker.as_ref()
4204 }
4205
4206 pub fn is_controlled_by_service_worker(&self) -> bool {
4210 self.controlled_service_worker.is_some()
4211 }
4212
4213 pub fn is_url_in_service_worker_scope(&self, url: &str) -> bool {
4220 self.service_worker_scope
4221 .as_ref()
4222 .map(|scope| scope.is_url_in_scope(url))
4223 .unwrap_or(false)
4224 }
4225
4226 pub fn register_service_worker_scope(&mut self, scope: ServiceWorkerGlobalScopeState) {
4230 self.service_worker_scope = Some(scope);
4231 }
4232
4233 pub fn service_worker_scope(&self) -> Option<&ServiceWorkerGlobalScopeState> {
4237 self.service_worker_scope.as_ref()
4238 }
4239
4240 pub fn service_worker_scope_mut(&mut self) -> Option<&mut ServiceWorkerGlobalScopeState> {
4244 self.service_worker_scope.as_mut()
4245 }
4246
4247 pub fn remove_service_worker_scope(&mut self) -> Option<ServiceWorkerGlobalScopeState> {
4251 self.service_worker_scope.take()
4252 }
4253
4254 pub fn forward_service_worker_fetch_event(&self, event: ServiceWorkerFetchEvent) {
4262 if let Some(ref tx) = self.event_tx {
4263 let stealth_status = if event.stealth_profile_applied {
4264 "stealth profile applied"
4265 } else {
4266 "⚠️ STEALTH BOUNDARY VIOLATION"
4267 };
4268 let _ = tx.send(ServoEvent::Console {
4269 target_id: "0".to_string(),
4270 level: if event.stealth_profile_applied {
4271 ConsoleLevel::Debug
4272 } else {
4273 ConsoleLevel::Warning
4274 },
4275 text: format!(
4276 "[ServiceWorker] fetch {} {} -> {} ({})",
4277 event.method,
4278 event.request_url,
4279 event.registration_id.script_url,
4280 stealth_status
4281 ),
4282 url: None,
4283 line: None,
4284 column: None,
4285 });
4286 }
4287 }
4288
4289 pub fn set_service_worker_scope_config(&mut self, config: &ServiceWorkerScopeConfig) {
4296 if let Some(scope) = &mut self.service_worker_scope {
4297 scope.scope.navigator = WorkerNavigator::from_scope_config(config);
4298 }
4299 }
4300}
4301
4302pub struct BaoServoDelegate {
4303 last_error: RefCell<Option<String>>,
4304 console_log_tx: RefCell<Option<std::sync::mpsc::Sender<ConsoleMessage>>>,
4307 event_tx: RefCell<Option<Sender<ServoEvent>>>,
4311 shared_workers: RefCell<Vec<SharedWorkerHandle>>,
4318 service_workers: RefCell<Vec<ServiceWorkerHandle>>,
4327}
4328
4329impl Default for BaoServoDelegate {
4330 fn default() -> Self {
4331 BaoServoDelegate {
4332 last_error: RefCell::new(None),
4333 console_log_tx: RefCell::new(None),
4334 event_tx: RefCell::new(None),
4335 shared_workers: RefCell::new(Vec::new()),
4336 service_workers: RefCell::new(Vec::new()),
4337 }
4338 }
4339}
4340
4341impl BaoServoDelegate {
4342 pub fn new() -> Self {
4343 Self::default()
4344 }
4345
4346 pub fn last_error(&self) -> Option<String> {
4347 self.last_error.borrow().clone()
4348 }
4349
4350 pub fn set_console_log_tx(&self, tx: std::sync::mpsc::Sender<ConsoleMessage>) {
4353 *self.console_log_tx.borrow_mut() = Some(tx);
4354 }
4355
4356 pub fn console_log_tx(&self) -> Option<std::sync::mpsc::Sender<ConsoleMessage>> {
4359 self.console_log_tx.borrow().clone()
4360 }
4361
4362 pub fn set_event_tx(&self, tx: Sender<ServoEvent>) {
4366 *self.event_tx.borrow_mut() = Some(tx);
4367 }
4368
4369 pub fn event_tx(&self) -> Option<Sender<ServoEvent>> {
4373 self.event_tx.borrow().clone()
4374 }
4375
4376 pub fn register_shared_worker(&self, handle: SharedWorkerHandle) -> SharedWorkerHandle {
4387 let id = handle.id();
4388 let mut shared_workers = self.shared_workers.borrow_mut();
4389 if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
4390 existing.clone()
4391 } else {
4392 shared_workers.push(handle.clone());
4393 handle
4394 }
4395 }
4396
4397 pub fn find_shared_worker(&self, script_url: &str, name: &str) -> Option<SharedWorkerHandle> {
4406 self.shared_workers
4407 .borrow()
4408 .iter()
4409 .find(|h| h.script_url == script_url && h.name == name)
4410 .cloned()
4411 }
4412
4413 pub fn reap_terminated_shared_workers(&self) {
4420 self.shared_workers
4421 .borrow_mut()
4422 .retain(|h| !h.is_terminated() || h.connected_page_count() > 0);
4423 }
4424
4425 pub fn shared_worker_count(&self) -> usize {
4429 self.shared_workers.borrow().len()
4430 }
4431
4432 pub fn route_shared_worker(&self, handle: SharedWorkerHandle) -> (SharedWorkerHandle, bool) {
4444 let id = handle.id();
4445 let mut shared_workers = self.shared_workers.borrow_mut();
4446 if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
4447 (existing.clone(), false)
4448 } else {
4449 shared_workers.push(handle.clone());
4450 (handle, true)
4451 }
4452 }
4453
4454 pub fn get_or_create_shared_worker(
4461 &self,
4462 script_url: &str,
4463 name: &str,
4464 ) -> (SharedWorkerHandle, bool) {
4465 if let Some(existing) = self.find_shared_worker(script_url, name) {
4466 (existing, false)
4467 } else {
4468 let handle = SharedWorkerHandle::new(script_url.to_string(), name.to_string());
4469 let returned = self.register_shared_worker(handle);
4470 (returned, true)
4471 }
4472 }
4473
4474 pub fn unregister_shared_worker(&self, id: &SharedWorkerId) -> bool {
4481 let mut shared_workers = self.shared_workers.borrow_mut();
4482 let before = shared_workers.len();
4483 shared_workers.retain(|h| &h.id() != id);
4484 shared_workers.len() < before
4485 }
4486
4487 pub fn all_shared_workers(&self) -> Vec<SharedWorkerHandle> {
4493 self.shared_workers.borrow().iter().cloned().collect()
4494 }
4495
4496 pub fn register_service_worker(&self, handle: ServiceWorkerHandle) -> ServiceWorkerHandle {
4509 let id = handle.id();
4510 let mut service_workers = self.service_workers.borrow_mut();
4511 if let Some(existing) = service_workers.iter().find(|h| h.id() == id) {
4512 existing.clone()
4513 } else {
4514 service_workers.push(handle.clone());
4515 handle
4516 }
4517 }
4518
4519 pub fn find_service_worker(
4525 &self,
4526 script_url: &str,
4527 scope: &str,
4528 ) -> Option<ServiceWorkerHandle> {
4529 self.service_workers
4530 .borrow()
4531 .iter()
4532 .find(|h| h.script_url == script_url && h.scope == scope)
4533 .cloned()
4534 }
4535
4536 pub fn find_service_worker_for_url(&self, url: &str) -> Option<ServiceWorkerHandle> {
4544 self.service_workers
4545 .borrow()
4546 .iter()
4547 .filter(|h| h.is_intercepting_fetch())
4548 .find(|h| url.starts_with(&h.scope))
4549 .cloned()
4550 }
4551
4552 pub fn reap_terminated_service_workers(&self) {
4559 self.service_workers
4560 .borrow_mut()
4561 .retain(|h| !h.is_terminated());
4562 }
4563
4564 pub fn service_worker_count(&self) -> usize {
4568 self.service_workers.borrow().len()
4569 }
4570
4571 pub fn unregister_service_worker(&self, id: &ServiceWorkerRegistrationId) -> bool {
4579 let mut service_workers = self.service_workers.borrow_mut();
4580 let before = service_workers.len();
4581 service_workers.retain(|h| &h.id() != id);
4582 service_workers.len() < before
4583 }
4584
4585 pub fn get_or_create_service_worker(
4592 &self,
4593 script_url: &str,
4594 scope: &str,
4595 stealth_profile: Option<bao_stealth::StealthProfile>,
4596 ) -> (ServiceWorkerHandle, bool) {
4597 if let Some(existing) = self.find_service_worker(script_url, scope) {
4598 (existing, false)
4599 } else {
4600 let handle = ServiceWorkerHandle::new(
4601 script_url.to_string(),
4602 scope.to_string(),
4603 stealth_profile,
4604 );
4605 let returned = self.register_service_worker(handle);
4606 (returned, true)
4607 }
4608 }
4609
4610 pub fn all_service_workers(&self) -> Vec<ServiceWorkerHandle> {
4616 self.service_workers.borrow().iter().cloned().collect()
4617 }
4618
4619 pub fn verify_service_worker_stealth_consistency(
4631 &self,
4632 page_stealth_profile: &bao_stealth::StealthProfile,
4633 ) -> Vec<ServiceWorkerRegistrationId> {
4634 self.service_workers
4635 .borrow()
4636 .iter()
4637 .filter(|h| h.is_intercepting_fetch())
4638 .filter(|h| {
4639 match &h.stealth_profile {
4643 Some(sw_profile) => {
4644 sw_profile.navigator.user_agent != page_stealth_profile.navigator.user_agent
4647 || sw_profile.navigator.platform
4648 != page_stealth_profile.navigator.platform
4649 }
4650 None => {
4651 true
4654 }
4655 }
4656 })
4657 .map(|h| h.id())
4658 .collect()
4659 }
4660}
4661
4662impl ServoDelegate for BaoServoDelegate {
4663 fn notify_error(&self, error: ServoError) {
4664 let error_str = format!("{error:?}");
4665 *self.last_error.borrow_mut() = Some(error_str.clone());
4666 if error_str.to_lowercase().contains("certificate")
4671 || error_str.to_lowercase().contains("tls")
4672 {
4673 if let Some(ref tx) = *self.console_log_tx.borrow() {
4674 let _ = tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
4675 event_id: 0,
4676 error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
4677 url: String::new(),
4678 }));
4679 }
4680 }
4681 }
4682
4683 fn show_console_message(&self, level: ConsoleLogLevel, message: String) {
4684 let level_str = match level {
4685 ConsoleLogLevel::Debug => "debug",
4686 ConsoleLogLevel::Log => "info",
4687 ConsoleLogLevel::Info => "info",
4688 ConsoleLogLevel::Warn => "warning",
4689 ConsoleLogLevel::Error => "error",
4690 ConsoleLogLevel::Trace => "verbose",
4691 ConsoleLogLevel::Dir => "info",
4692 };
4693 log::trace!("[servo] {message}");
4694
4695 let event_tx = self.event_tx.borrow();
4700 if let Some(ref tx) = *event_tx {
4701 let servo_level = match level {
4702 ConsoleLogLevel::Debug => ConsoleLevel::Debug,
4703 ConsoleLogLevel::Log => ConsoleLevel::Info,
4704 ConsoleLogLevel::Info => ConsoleLevel::Info,
4705 ConsoleLogLevel::Warn => ConsoleLevel::Warning,
4706 ConsoleLogLevel::Error => ConsoleLevel::Error,
4707 ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
4708 ConsoleLogLevel::Dir => ConsoleLevel::Info,
4709 };
4710 let _ = tx.send(ServoEvent::Console {
4711 target_id: "0".to_string(),
4712 level: servo_level,
4713 text: message,
4714 url: None,
4715 line: None,
4716 column: None,
4717 });
4718 } else if let Some(ref tx) = *self.console_log_tx.borrow() {
4719 let msg = match BaoEvent::from_console_text(&message) {
4720 Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
4721 _ => ConsoleMessage::Log {
4722 level: level_str.to_string(),
4723 text: message,
4724 },
4725 };
4726 let _ = tx.send(msg);
4727 }
4728 }
4729
4730 fn request_devtools_connection(&self, request: AllowOrDenyRequest) {
4731 request.allow();
4732 }
4733}
4734
4735pub struct BaoWebViewDelegate {
4736 state: Rc<RefCell<BaoWebViewState>>,
4737 viewport: PhysicalSize<u32>,
4738}
4739
4740impl BaoWebViewDelegate {
4741 pub fn new(state: Rc<RefCell<BaoWebViewState>>, viewport: PhysicalSize<u32>) -> Self {
4742 BaoWebViewDelegate { state, viewport }
4743 }
4744
4745 pub fn state(&self) -> &Rc<RefCell<BaoWebViewState>> {
4746 &self.state
4747 }
4748}
4749
4750impl WebViewDelegate for BaoWebViewDelegate {
4751 fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
4752 let screen_size =
4753 DeviceIntSize::new(self.viewport.width as i32, self.viewport.height as i32);
4754 Some(ScreenGeometry {
4755 size: screen_size,
4756 available_size: screen_size,
4757 window_rect: DeviceIntRect::from_origin_and_size(DeviceIntPoint::zero(), screen_size),
4758 })
4759 }
4760
4761 fn notify_url_changed(&self, _webview: WebView, url: url::Url) {
4762 let url_str = url.to_string();
4763 self.state.borrow_mut().url = Some(url);
4764 let event_tx = self.state.borrow().event_tx.clone();
4768 if let Some(ref tx) = event_tx {
4769 let _ = tx.send(ServoEvent::FrameNavigated {
4770 target_id: "0".to_string(),
4771 frame_id: "0".to_string(),
4772 url: url_str,
4773 name: None,
4774 });
4775 } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4776 let loader_id = format!("{:016x}", url_str.len() as u64);
4777 let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
4778 frame_id: "0".to_string(),
4779 url: url_str,
4780 loader_id,
4781 }));
4782 }
4783 }
4784
4785 fn notify_page_title_changed(&self, _webview: WebView, title: Option<String>) {
4786 self.state.borrow_mut().title = title;
4787 }
4788
4789 fn notify_load_status_changed(&self, _webview: WebView, status: LoadStatus) {
4790 self.state.borrow_mut().load_status = status;
4791 match status {
4792 LoadStatus::Started => {
4793 {
4799 let mut state = self.state.borrow_mut();
4800 if !state.active_workers.is_empty() {
4801 log::debug!(
4802 "[delegate] page navigation: terminating {} active workers",
4803 state.active_worker_count()
4804 );
4805 state.terminate_all_workers();
4806 }
4807 state.disconnect_shared_worker_ports();
4810 state.clear_controlling_service_worker();
4816 }
4817
4818 let event_tx = self.state.borrow().event_tx.clone();
4823 if let Some(ref tx) = event_tx {
4824 let _ = tx.send(ServoEvent::FrameStartedLoading {
4825 target_id: "0".to_string(),
4826 frame_id: "0".to_string(),
4827 });
4828 }
4829 }
4830 LoadStatus::Complete => {
4831 self.state.borrow_mut().dom_proxies_dirty = true;
4832
4833 self.state.borrow_mut().reap_terminated_workers();
4838
4839 let event_tx = self.state.borrow().event_tx.clone();
4843 if let Some(ref tx) = event_tx {
4844 let _ = tx.send(ServoEvent::FrameStoppedLoading {
4845 target_id: "0".to_string(),
4846 frame_id: "0".to_string(),
4847 });
4848 } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4849 let timestamp = std::time::SystemTime::now()
4850 .duration_since(std::time::UNIX_EPOCH)
4851 .unwrap_or_default()
4852 .as_secs_f64();
4853 let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageLoadEventFired {
4854 timestamp,
4855 }));
4856 }
4857 }
4858 LoadStatus::HeadParsed => {}
4859 }
4860 }
4861
4862 fn notify_new_frame_ready(&self, _webview: WebView) {
4863 self.state.borrow_mut().frame_ready = true;
4864 }
4865
4866 fn request_navigation(&self, _webview: WebView, request: NavigationRequest) {
4867 request.allow();
4868 }
4869
4870 fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
4871 request.allow();
4872 }
4873
4874 fn request_create_new(&self, _parent_webview: WebView, _request: CreateNewWebViewRequest) {}
4875
4876 fn show_console_message(&self, _webview: WebView, level: ConsoleLogLevel, message: String) {
4877 let level_str = match level {
4878 ConsoleLogLevel::Debug => "debug",
4879 ConsoleLogLevel::Log => "info",
4880 ConsoleLogLevel::Info => "info",
4881 ConsoleLogLevel::Warn => "warning",
4882 ConsoleLogLevel::Error => "error",
4883 ConsoleLogLevel::Trace => "verbose",
4884 ConsoleLogLevel::Dir => "info",
4885 };
4886 log::trace!("[webview] {message}");
4887
4888 let event_tx = self.state.borrow().event_tx.clone();
4892 if let Some(ref tx) = event_tx {
4893 let servo_level = match level {
4894 ConsoleLogLevel::Debug => ConsoleLevel::Debug,
4895 ConsoleLogLevel::Log => ConsoleLevel::Info,
4896 ConsoleLogLevel::Info => ConsoleLevel::Info,
4897 ConsoleLogLevel::Warn => ConsoleLevel::Warning,
4898 ConsoleLogLevel::Error => ConsoleLevel::Error,
4899 ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
4900 ConsoleLogLevel::Dir => ConsoleLevel::Info,
4901 };
4902 let _ = tx.send(ServoEvent::Console {
4903 target_id: "0".to_string(),
4904 level: servo_level,
4905 text: message,
4906 url: None,
4907 line: None,
4908 column: None,
4909 });
4910 } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4911 let msg = match BaoEvent::from_console_text(&message) {
4912 Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
4913 _ => ConsoleMessage::Log {
4914 level: level_str.to_string(),
4915 text: message,
4916 },
4917 };
4918 let _ = tx.send(msg);
4919 }
4920 }
4921
4922 fn show_embedder_control(&self, _webview: WebView, _control: EmbedderControl) {}
4923
4924 fn hide_embedder_control(&self, _webview: WebView, _id: EmbedderControlId) {}
4925
4926 fn notify_crashed(&self, _webview: WebView, reason: String, _backtrace: Option<String>) {
4927 log::error!("[webview] crashed: {reason}");
4928 }
4929}
4930
4931#[cfg(test)]
4932mod tests {
4933 use super::*;
4934
4935 #[test]
4939 fn test_webview_state_default() {
4940 let state = BaoWebViewState::default();
4941 assert!(state.url.is_none());
4942 assert!(state.title.is_none());
4943 assert!(matches!(state.load_status, LoadStatus::Started));
4944 assert!(!state.frame_ready);
4945 assert!(!state.dom_proxies_dirty);
4946 }
4947
4948 #[test]
4949 fn test_webview_state_url_mutate() {
4950 let mut state = BaoWebViewState::default();
4951 state.url = Some(url::Url::parse("https://example.com").unwrap());
4952 assert!(state.url.is_some());
4953 assert_eq!(state.url.unwrap().as_str(), "https://example.com/");
4954 }
4955
4956 #[test]
4957 fn test_webview_state_title_mutate() {
4958 let mut state = BaoWebViewState::default();
4959 state.title = Some("Test Page".to_string());
4960 assert_eq!(state.title.as_deref(), Some("Test Page"));
4961 }
4962
4963 #[test]
4964 fn test_webview_state_frame_ready_toggle() {
4965 let mut state = BaoWebViewState::default();
4966 assert!(!state.frame_ready);
4967 state.frame_ready = true;
4968 assert!(state.frame_ready);
4969 }
4970
4971 #[test]
4975 fn test_servo_delegate_new_no_error() {
4976 let delegate = BaoServoDelegate::new();
4977 assert!(delegate.last_error().is_none());
4978 }
4979
4980 #[test]
4981 fn test_servo_delegate_default_no_error() {
4982 let delegate = BaoServoDelegate::default();
4983 assert!(delegate.last_error().is_none());
4984 }
4985
4986 #[test]
4990 fn test_webview_delegate_new_with_state() {
4991 let state = Rc::new(RefCell::new(BaoWebViewState::default()));
4992 let viewport = PhysicalSize::new(1024, 768);
4993 let delegate = BaoWebViewDelegate::new(state, viewport);
4994 assert!(delegate.state().borrow().url.is_none());
4995 }
4996
4997 #[test]
4998 fn test_webview_delegate_state_rc_shared() {
4999 let state = Rc::new(RefCell::new(BaoWebViewState::default()));
5000 let viewport = PhysicalSize::new(800, 600);
5001 let delegate = BaoWebViewDelegate::new(Rc::clone(&state), viewport);
5002 state.borrow_mut().title = Some("External".to_string());
5004 assert_eq!(delegate.state().borrow().title.as_deref(), Some("External"));
5006 }
5007
5008 #[test]
5009 fn test_webview_delegate_viewport_size() {
5010 let state = Rc::new(RefCell::new(BaoWebViewState::default()));
5011 let viewport = PhysicalSize::new(1440, 900);
5012 let delegate = BaoWebViewDelegate::new(state, viewport);
5013 assert!(delegate.state().borrow().url.is_none());
5015 }
5016
5017 #[test]
5021 fn test_pool_stats_fields() {
5022 let stats = crate::page_pool::PoolStats {
5023 active: 3,
5024 idle: 1,
5025 total_created: 5,
5026 total_destroyed: 2,
5027 };
5028 assert_eq!(stats.active, 3);
5029 assert_eq!(stats.idle, 1);
5030 assert_eq!(stats.total_created, 5);
5031 assert_eq!(stats.total_destroyed, 2);
5032 }
5033
5034 #[test]
5038 fn test_dom_proxies_dirty_default_false() {
5039 let state = BaoWebViewState::default();
5040 assert!(!state.dom_proxies_dirty);
5041 }
5042
5043 #[test]
5044 fn test_dom_proxies_dirty_set_on_complete() {
5045 let mut state = BaoWebViewState::default();
5046 state.load_status = LoadStatus::Complete;
5047 state.dom_proxies_dirty = true;
5048 assert!(state.dom_proxies_dirty);
5049 }
5050
5051 #[test]
5052 fn test_dom_proxies_dirty_clear_after_refresh() {
5053 let mut state = BaoWebViewState::default();
5054 state.dom_proxies_dirty = true;
5055 state.dom_proxies_dirty = false;
5056 assert!(!state.dom_proxies_dirty);
5057 }
5058
5059 #[test]
5063 fn test_servo_delegate_console_log_channel_set_and_get() {
5064 let delegate = BaoServoDelegate::new();
5065 assert!(delegate.console_log_tx().is_none());
5066 let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5067 delegate.set_console_log_tx(tx);
5068 assert!(delegate.console_log_tx().is_some());
5069 }
5070
5071 #[test]
5072 fn test_servo_delegate_console_log_tx_clones() {
5073 let delegate = BaoServoDelegate::new();
5074 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5075 delegate.set_console_log_tx(tx);
5076 let cloned = delegate.console_log_tx().unwrap();
5078 cloned
5079 .send(ConsoleMessage::Log {
5080 level: "info".into(),
5081 text: "hello".into(),
5082 })
5083 .unwrap();
5084 let msg = rx.try_recv().unwrap();
5085 match msg {
5086 ConsoleMessage::Log { level, text } => {
5087 assert_eq!(level, "info");
5088 assert_eq!(text, "hello");
5089 }
5090 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5091 }
5092 }
5093
5094 #[test]
5095 fn test_webview_state_console_log_tx_propagation() {
5096 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5097 let mut state = BaoWebViewState::default();
5098 state.console_log_tx = Some(tx);
5099 if let Some(ref tx) = state.console_log_tx {
5101 tx.send(ConsoleMessage::Log {
5102 level: "warning".into(),
5103 text: "test message".into(),
5104 })
5105 .unwrap();
5106 }
5107 let msg = rx.try_recv().unwrap();
5108 match msg {
5109 ConsoleMessage::Log { level, text } => {
5110 assert_eq!(level, "warning");
5111 assert_eq!(text, "test message");
5112 }
5113 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5114 }
5115 }
5116
5117 #[test]
5118 fn test_webview_state_console_log_tx_default_none() {
5119 let state = BaoWebViewState::default();
5120 assert!(state.console_log_tx.is_none());
5121 }
5122
5123 #[test]
5124 fn test_console_log_all_level_mappings() {
5125 let delegate = BaoServoDelegate::new();
5126 let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5127 delegate.set_console_log_tx(tx);
5128
5129 let cases: Vec<(ConsoleLogLevel, &str)> = vec![
5132 (ConsoleLogLevel::Debug, "debug"),
5133 (ConsoleLogLevel::Log, "info"),
5134 (ConsoleLogLevel::Info, "info"),
5135 (ConsoleLogLevel::Warn, "warning"),
5136 (ConsoleLogLevel::Error, "error"),
5137 (ConsoleLogLevel::Trace, "verbose"),
5138 (ConsoleLogLevel::Dir, "info"),
5139 ];
5140 for (level, expected_str) in cases {
5141 let mapped = match level {
5142 ConsoleLogLevel::Debug => "debug",
5143 ConsoleLogLevel::Log => "info",
5144 ConsoleLogLevel::Info => "info",
5145 ConsoleLogLevel::Warn => "warning",
5146 ConsoleLogLevel::Error => "error",
5147 ConsoleLogLevel::Trace => "verbose",
5148 ConsoleLogLevel::Dir => "info",
5149 };
5150 assert_eq!(
5151 mapped, expected_str,
5152 "level {:?} should map to {}",
5153 level, expected_str
5154 );
5155 }
5156 }
5157
5158 #[test]
5159 fn test_webview_delegate_console_log_forwarding() {
5160 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5161 let state = Rc::new(RefCell::new(BaoWebViewState {
5162 console_log_tx: Some(tx),
5163 ..Default::default()
5164 }));
5165 let viewport = PhysicalSize::new(800, 600);
5166 let _delegate = BaoWebViewDelegate::new(state, viewport);
5167
5168 if let Some(ref tx) = _delegate.state().borrow().console_log_tx {
5170 tx.send(ConsoleMessage::Log {
5171 level: "error".into(),
5172 text: "crash!".into(),
5173 })
5174 .unwrap();
5175 }
5176 let msg = rx.try_recv().unwrap();
5177 match msg {
5178 ConsoleMessage::Log { level, text } => {
5179 assert_eq!(level, "error");
5180 assert_eq!(text, "crash!");
5181 }
5182 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5183 }
5184 }
5185
5186 #[test]
5190 fn test_notify_url_changed_emits_frame_navigated() {
5191 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5192 let state = Rc::new(RefCell::new(BaoWebViewState {
5193 console_log_tx: Some(tx),
5194 ..Default::default()
5195 }));
5196 let viewport = PhysicalSize::new(800, 600);
5197 let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
5198
5199 let url = url::Url::parse("https://example.com").unwrap();
5201 let url_str = url.to_string();
5202 let loader_id = format!("{:016x}", url_str.len() as u64);
5203 if let Some(ref tx) = state.borrow().console_log_tx {
5204 tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
5205 frame_id: "0".to_string(),
5206 url: url_str.clone(),
5207 loader_id: loader_id.clone(),
5208 }))
5209 .unwrap();
5210 }
5211
5212 let msg = rx.try_recv().unwrap();
5213 match msg {
5214 ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
5215 frame_id,
5216 url,
5217 loader_id: lid,
5218 }) => {
5219 assert_eq!(frame_id, "0");
5220 assert!(url.starts_with("https://example.com"));
5221 assert_eq!(lid, loader_id);
5222 }
5223 other => panic!("expected PageFrameNavigated, got {:?}", other),
5224 }
5225 }
5226
5227 #[test]
5231 fn test_notify_error_certificate_error_emits_security_event() {
5232 let delegate = BaoServoDelegate::new();
5233 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5234 delegate.set_console_log_tx(tx);
5235
5236 if let Some(ref tx) = *delegate.console_log_tx.borrow() {
5238 tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
5239 event_id: 0,
5240 error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
5241 url: String::new(),
5242 }))
5243 .unwrap();
5244 }
5245
5246 let msg = rx.try_recv().unwrap();
5247 match msg {
5248 ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
5249 event_id,
5250 error_type,
5251 url,
5252 }) => {
5253 assert_eq!(event_id, 0);
5254 assert_eq!(error_type, "net::ERR_CERT_AUTHORITY_INVALID");
5255 assert_eq!(url, "");
5256 }
5257 other => panic!("expected SecurityCertificateError, got {:?}", other),
5258 }
5259 }
5260
5261 #[test]
5265 fn test_servo_delegate_event_tx_set_and_get() {
5266 let delegate = BaoServoDelegate::new();
5267 assert!(delegate.event_tx().is_none());
5268 let (tx, _rx) = std::sync::mpsc::channel::<ServoEvent>();
5269 delegate.set_event_tx(tx);
5270 assert!(delegate.event_tx().is_some());
5271 }
5272
5273 #[test]
5274 fn test_servo_delegate_event_tx_sends_console_event() {
5275 let delegate = BaoServoDelegate::new();
5276 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5277 delegate.set_event_tx(tx);
5278
5279 if let Some(ref tx) = delegate.event_tx() {
5281 tx.send(ServoEvent::Console {
5282 target_id: "0".to_string(),
5283 level: ConsoleLevel::Info,
5284 text: "hello".to_string(),
5285 url: None,
5286 line: None,
5287 column: None,
5288 })
5289 .unwrap();
5290 }
5291
5292 let event = rx.try_recv().unwrap();
5293 match event {
5294 ServoEvent::Console { level, text, .. } => {
5295 assert_eq!(level, ConsoleLevel::Info);
5296 assert_eq!(text, "hello");
5297 }
5298 _ => panic!("expected Console event"),
5299 }
5300 }
5301
5302 #[test]
5303 fn test_webview_state_event_tx_default_none() {
5304 let state = BaoWebViewState::default();
5305 assert!(state.event_tx.is_none());
5306 }
5307
5308 #[test]
5309 fn test_webview_state_event_tx_propagation() {
5310 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5311 let mut state = BaoWebViewState::default();
5312 state.event_tx = Some(tx);
5313 if let Some(ref tx) = state.event_tx {
5315 tx.send(ServoEvent::FrameNavigated {
5316 target_id: "0".to_string(),
5317 frame_id: "0".to_string(),
5318 url: "https://example.com/".to_string(),
5319 name: None,
5320 })
5321 .unwrap();
5322 }
5323 let event = rx.try_recv().unwrap();
5324 match event {
5325 ServoEvent::FrameNavigated { url, .. } => {
5326 assert_eq!(url, "https://example.com/");
5327 }
5328 _ => panic!("expected FrameNavigated event"),
5329 }
5330 }
5331
5332 #[test]
5333 fn test_event_tx_console_level_mapping() {
5334 let cases: Vec<(ConsoleLogLevel, ConsoleLevel)> = vec![
5336 (ConsoleLogLevel::Debug, ConsoleLevel::Debug),
5337 (ConsoleLogLevel::Log, ConsoleLevel::Info),
5338 (ConsoleLogLevel::Info, ConsoleLevel::Info),
5339 (ConsoleLogLevel::Warn, ConsoleLevel::Warning),
5340 (ConsoleLogLevel::Error, ConsoleLevel::Error),
5341 (ConsoleLogLevel::Trace, ConsoleLevel::Verbose),
5342 ];
5343 for (servo_level, expected) in cases {
5344 let mapped = match servo_level {
5345 ConsoleLogLevel::Debug => ConsoleLevel::Debug,
5346 ConsoleLogLevel::Log => ConsoleLevel::Info,
5347 ConsoleLogLevel::Info => ConsoleLevel::Info,
5348 ConsoleLogLevel::Warn => ConsoleLevel::Warning,
5349 ConsoleLogLevel::Error => ConsoleLevel::Error,
5350 ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
5351 ConsoleLogLevel::Dir => ConsoleLevel::Info,
5352 };
5353 assert_eq!(
5354 mapped, expected,
5355 "servo {:?} should map to {:?}",
5356 servo_level, expected
5357 );
5358 }
5359 }
5360
5361 #[test]
5362 fn test_notify_load_started_emits_frame_started_loading() {
5363 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5366 let state = Rc::new(RefCell::new(BaoWebViewState {
5367 event_tx: Some(tx),
5368 ..Default::default()
5369 }));
5370 let viewport = PhysicalSize::new(800, 600);
5371 let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
5372
5373 if let Some(ref tx) = state.borrow().event_tx {
5375 tx.send(ServoEvent::FrameStartedLoading {
5376 target_id: "0".to_string(),
5377 frame_id: "0".to_string(),
5378 })
5379 .unwrap();
5380 }
5381
5382 let event = rx.try_recv().unwrap();
5383 match event {
5384 ServoEvent::FrameStartedLoading {
5385 target_id,
5386 frame_id,
5387 } => {
5388 assert_eq!(target_id, "0");
5389 assert_eq!(frame_id, "0");
5390 }
5391 _ => panic!("expected FrameStartedLoading event"),
5392 }
5393 }
5394
5395 #[test]
5399 fn test_worker_handle_new_is_running() {
5400 let handle = WorkerHandle::new("https://example.com/worker.js".to_string());
5401 assert_eq!(handle.script_url, "https://example.com/worker.js");
5402 assert!(!handle.is_closing());
5403 assert!(!handle.is_terminated());
5404 }
5405
5406 #[test]
5407 fn test_worker_handle_terminate_sets_closing() {
5408 let handle = WorkerHandle::new("worker.js".to_string());
5409 assert!(!handle.is_closing());
5410 handle.terminate();
5411 assert!(handle.is_closing());
5412 handle.terminate();
5414 assert!(handle.is_closing());
5415 }
5416
5417 #[test]
5418 fn test_worker_handle_mark_terminated() {
5419 let handle = WorkerHandle::new("worker.js".to_string());
5420 assert!(!handle.is_terminated());
5421 handle.mark_terminated();
5422 assert!(handle.is_terminated());
5423 }
5424
5425 #[test]
5426 fn test_worker_handle_terminate_then_terminated() {
5427 let handle = WorkerHandle::new("worker.js".to_string());
5428 handle.terminate();
5429 assert!(handle.is_closing());
5430 assert!(!handle.is_terminated());
5431 handle.mark_terminated();
5432 assert!(handle.is_terminated());
5433 }
5434
5435 #[test]
5436 fn test_worker_handle_clone_shares_state() {
5437 let handle = WorkerHandle::new("worker.js".to_string());
5438 let clone = handle.clone();
5439 handle.terminate();
5440 assert!(
5441 clone.is_closing(),
5442 "clone should see closing flag from original"
5443 );
5444 clone.mark_terminated();
5445 assert!(
5446 handle.is_terminated(),
5447 "original should see terminated flag from clone"
5448 );
5449 }
5450
5451 #[test]
5452 fn test_webview_state_active_workers_default_empty() {
5453 let state = BaoWebViewState::default();
5454 assert!(state.active_workers.is_empty());
5455 assert_eq!(state.active_worker_count(), 0);
5456 }
5457
5458 #[test]
5459 fn test_webview_state_track_worker() {
5460 let mut state = BaoWebViewState::default();
5461 let handle = WorkerHandle::new("worker1.js".to_string());
5462 state.track_worker(handle);
5463 assert_eq!(state.active_worker_count(), 1);
5464 assert_eq!(state.active_workers.len(), 1);
5465 assert_eq!(state.active_workers[0].handle().script_url, "worker1.js");
5466 }
5467
5468 #[test]
5469 fn test_webview_state_track_multiple_workers() {
5470 let mut state = BaoWebViewState::default();
5471 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5472 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5473 state.track_worker(WorkerHandle::new("worker3.js".to_string()));
5474 assert_eq!(state.active_worker_count(), 3);
5475 }
5476
5477 #[test]
5478 fn test_webview_state_terminate_all_workers() {
5479 let mut state = BaoWebViewState::default();
5480 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5481 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5482 assert!(!state.active_workers[0].handle().is_closing());
5483 assert!(!state.active_workers[1].handle().is_closing());
5484 state.terminate_all_workers();
5485 assert!(state.active_workers[0].handle().is_closing());
5486 assert!(state.active_workers[1].handle().is_closing());
5487 }
5488
5489 #[test]
5490 fn test_webview_state_reap_terminated_workers() {
5491 let mut state = BaoWebViewState::default();
5492 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5493 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5494 state.active_workers[0].handle().terminate();
5496 state.active_workers[0].handle().mark_terminated();
5497 assert_eq!(state.active_worker_count(), 1);
5498 state.reap_terminated_workers();
5499 assert_eq!(state.active_workers.len(), 1);
5500 assert_eq!(state.active_workers[0].handle().script_url, "worker2.js");
5501 }
5502
5503 #[test]
5504 fn test_webview_state_reap_all_terminated() {
5505 let mut state = BaoWebViewState::default();
5506 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5507 state.terminate_all_workers();
5509 state.reap_terminated_workers();
5511 assert!(state.active_workers.is_empty());
5512 assert_eq!(state.active_worker_count(), 0);
5513 }
5514
5515 #[test]
5516 fn test_worker_id_equality() {
5517 let id1 = WorkerId("worker1.js".to_string());
5518 let id2 = WorkerId("worker1.js".to_string());
5519 let id3 = WorkerId("worker2.js".to_string());
5520 assert_eq!(id1, id2);
5521 assert_ne!(id1, id3);
5522 }
5523
5524 #[test]
5525 fn test_worker_message_direction() {
5526 assert_eq!(
5527 WorkerMessageDirection::PageToWorker,
5528 WorkerMessageDirection::PageToWorker
5529 );
5530 assert_ne!(
5531 WorkerMessageDirection::PageToWorker,
5532 WorkerMessageDirection::WorkerToPage
5533 );
5534 }
5535
5536 #[test]
5537 fn test_worker_message_event_creation() {
5538 let event = WorkerMessageEvent {
5539 worker_id: WorkerId("worker1.js".to_string()),
5540 direction: WorkerMessageDirection::PageToWorker,
5541 };
5542 assert_eq!(event.worker_id.0, "worker1.js");
5543 assert_eq!(event.direction, WorkerMessageDirection::PageToWorker);
5544 }
5545
5546 #[test]
5547 fn test_webview_state_forward_worker_message_to_event_tx() {
5548 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5549 let state = BaoWebViewState {
5550 event_tx: Some(tx),
5551 ..Default::default()
5552 };
5553 let msg = WorkerMessageEvent {
5554 worker_id: WorkerId("worker1.js".to_string()),
5555 direction: WorkerMessageDirection::WorkerToPage,
5556 };
5557 state.forward_worker_message_event(msg);
5558 let event = rx.try_recv().unwrap();
5559 match event {
5560 ServoEvent::Console { level, text, .. } => {
5561 assert_eq!(level, ConsoleLevel::Debug);
5562 assert!(text.contains("worker→page"));
5563 assert!(text.contains("worker1.js"));
5564 }
5565 _ => panic!("expected Console event for worker message"),
5566 }
5567 }
5568
5569 #[test]
5570 fn test_webview_state_forward_worker_message_no_event_tx() {
5571 let state = BaoWebViewState::default();
5573 let msg = WorkerMessageEvent {
5574 worker_id: WorkerId("worker1.js".to_string()),
5575 direction: WorkerMessageDirection::PageToWorker,
5576 };
5577 state.forward_worker_message_event(msg);
5579 }
5580
5581 #[test]
5582 fn test_terminate_on_navigation_then_reap() {
5583 let mut state = BaoWebViewState::default();
5585 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5586 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5587 assert_eq!(state.active_worker_count(), 2);
5588
5589 state.terminate_all_workers();
5595 assert!(state.active_workers[0].handle().is_closing());
5596 assert!(state.active_workers[1].handle().is_closing());
5597 assert_eq!(state.active_worker_count(), 0);
5600
5601 state.reap_terminated_workers();
5603 assert!(state.active_workers.is_empty());
5604 }
5605
5606 #[test]
5610 fn test_worker_error_event_creation() {
5611 let event = WorkerErrorEvent {
5612 worker_id: WorkerId("worker1.js".to_string()),
5613 message: "Uncaught TypeError: x is not a function".to_string(),
5614 filename: "worker1.js".to_string(),
5615 lineno: 42,
5616 colno: 5,
5617 };
5618 assert_eq!(event.worker_id.0, "worker1.js");
5619 assert_eq!(event.message, "Uncaught TypeError: x is not a function");
5620 assert_eq!(event.filename, "worker1.js");
5621 assert_eq!(event.lineno, 42);
5622 assert_eq!(event.colno, 5);
5623 }
5624
5625 #[test]
5626 fn test_webview_state_forward_worker_error_to_event_tx() {
5627 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5628 let state = BaoWebViewState {
5629 event_tx: Some(tx),
5630 ..Default::default()
5631 };
5632 let error = WorkerErrorEvent {
5633 worker_id: WorkerId("worker1.js".to_string()),
5634 message: "Uncaught Error: boom".to_string(),
5635 filename: "worker1.js".to_string(),
5636 lineno: 10,
5637 colno: 3,
5638 };
5639 state.forward_worker_error_event(error);
5640 let event = rx.try_recv().unwrap();
5641 match event {
5642 ServoEvent::PageError {
5643 text,
5644 url,
5645 line,
5646 column,
5647 ..
5648 } => {
5649 assert!(text.contains("worker1.js"));
5650 assert!(text.contains("Uncaught Error: boom"));
5651 assert_eq!(url.as_deref(), Some("worker1.js"));
5652 assert_eq!(line, Some(10));
5653 assert_eq!(column, Some(3));
5654 }
5655 _ => panic!("expected PageError event for worker error"),
5656 }
5657 }
5658
5659 #[test]
5660 fn test_webview_state_forward_worker_error_no_event_tx() {
5661 let state = BaoWebViewState::default();
5662 let error = WorkerErrorEvent {
5663 worker_id: WorkerId("worker1.js".to_string()),
5664 message: "error".to_string(),
5665 filename: "worker1.js".to_string(),
5666 lineno: 1,
5667 colno: 1,
5668 };
5669 state.forward_worker_error_event(error);
5671 }
5672
5673 #[test]
5677 fn test_worker_teardown_path_equality() {
5678 assert_eq!(WorkerTeardownPath::Terminate, WorkerTeardownPath::Terminate);
5679 assert_eq!(WorkerTeardownPath::SelfClose, WorkerTeardownPath::SelfClose);
5680 assert_eq!(
5681 WorkerTeardownPath::PageUnload,
5682 WorkerTeardownPath::PageUnload
5683 );
5684 assert_ne!(WorkerTeardownPath::Terminate, WorkerTeardownPath::SelfClose);
5685 }
5686
5687 #[test]
5688 fn test_worker_lifecycle_state_running() {
5689 let handle = WorkerHandle::new("worker.js".to_string());
5690 let guard = AutoCloseWorker::new(handle);
5691 assert_eq!(guard.lifecycle_state(), WorkerLifecycleState::Running);
5692 }
5693
5694 #[test]
5695 fn test_worker_lifecycle_state_closing() {
5696 let handle = WorkerHandle::new("worker.js".to_string());
5697 let mut guard = AutoCloseWorker::new(handle);
5698 guard.terminate_via(WorkerTeardownPath::Terminate);
5699 assert_eq!(
5700 guard.lifecycle_state(),
5701 WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5702 );
5703 }
5704
5705 #[test]
5706 fn test_worker_lifecycle_state_terminated() {
5707 let handle = WorkerHandle::new("worker.js".to_string());
5708 let mut guard = AutoCloseWorker::new(handle);
5709 guard.terminate_via(WorkerTeardownPath::SelfClose);
5710 guard.handle().mark_terminated();
5711 assert_eq!(
5712 guard.lifecycle_state(),
5713 WorkerLifecycleState::Terminated(WorkerTeardownPath::SelfClose)
5714 );
5715 }
5716
5717 #[test]
5718 fn test_worker_lifecycle_states_snapshot() {
5719 let mut state = BaoWebViewState::default();
5720 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5721 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5722 let snapshot = state.worker_lifecycle_states();
5723 assert_eq!(snapshot.len(), 2);
5724 assert_eq!(snapshot[0].0, WorkerId("worker1.js".to_string()));
5725 assert_eq!(snapshot[0].1, WorkerLifecycleState::Running);
5726 assert_eq!(snapshot[1].0, WorkerId("worker2.js".to_string()));
5727 assert_eq!(snapshot[1].1, WorkerLifecycleState::Running);
5728 }
5729
5730 #[test]
5734 fn test_auto_close_worker_new_is_running() {
5735 let handle = WorkerHandle::new("worker.js".to_string());
5736 let guard = AutoCloseWorker::new(handle);
5737 assert!(!guard.handle().is_closing());
5738 assert!(!guard.handle().is_terminated());
5739 }
5740
5741 #[test]
5742 fn test_auto_close_worker_terminate_via() {
5743 let handle = WorkerHandle::new("worker.js".to_string());
5744 let mut guard = AutoCloseWorker::new(handle);
5745 guard.terminate_via(WorkerTeardownPath::Terminate);
5746 assert!(guard.handle().is_closing());
5747 assert_eq!(
5748 guard.lifecycle_state(),
5749 WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5750 );
5751 }
5752
5753 #[test]
5754 fn test_auto_close_worker_terminate_via_idempotent() {
5755 let handle = WorkerHandle::new("worker.js".to_string());
5756 let mut guard = AutoCloseWorker::new(handle);
5757 guard.terminate_via(WorkerTeardownPath::Terminate);
5758 guard.terminate_via(WorkerTeardownPath::SelfClose);
5759 assert_eq!(
5761 guard.lifecycle_state(),
5762 WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5763 );
5764 }
5765
5766 #[test]
5767 fn test_auto_close_worker_drop_terminates() {
5768 let handle = WorkerHandle::new("worker.js".to_string());
5769 let handle_clone = handle.clone();
5770 let guard = AutoCloseWorker::new(handle);
5771 assert!(!handle_clone.is_closing());
5772 drop(guard);
5773 assert!(handle_clone.is_closing());
5775 }
5776
5777 #[test]
5778 fn test_auto_close_worker_drop_already_closing() {
5779 let handle = WorkerHandle::new("worker.js".to_string());
5780 let handle_clone = handle.clone();
5781 let mut guard = AutoCloseWorker::new(handle);
5782 guard.terminate_via(WorkerTeardownPath::Terminate);
5783 drop(guard);
5784 assert!(handle_clone.is_closing());
5786 assert!(handle_clone.is_terminated());
5788 }
5789
5790 #[test]
5794 fn test_worker_handle_global_addr_default_zero() {
5795 let handle = WorkerHandle::new("worker.js".to_string());
5796 assert_eq!(handle.worker_global_addr(), 0);
5797 }
5798
5799 #[test]
5800 fn test_worker_handle_global_addr_set_and_get() {
5801 let handle = WorkerHandle::new("worker.js".to_string());
5802 handle.set_worker_global_addr(0xDEADBEEF);
5803 assert_eq!(handle.worker_global_addr(), 0xDEADBEEF);
5804 }
5805
5806 #[test]
5807 fn test_worker_handle_global_addr_arc_shared() {
5808 let handle = WorkerHandle::new("worker.js".to_string());
5809 let arc = handle.worker_global_addr_arc();
5810 arc.store(0xCAFEBABE_usize as u64, Ordering::Release);
5812 assert_eq!(handle.worker_global_addr(), 0xCAFEBABE);
5814 }
5815
5816 #[test]
5817 fn test_worker_handle_unregister_stealth_profile_no_addr() {
5818 let handle = WorkerHandle::new("worker.js".to_string());
5820 handle.unregister_stealth_profile();
5822 }
5823
5824 #[test]
5825 fn test_worker_handle_unregister_stealth_profile_with_addr() {
5826 let fake_addr = 0x12345678_usize;
5828 bao_stealth::engine_props::set_profile_for_global(
5829 fake_addr,
5830 &bao_stealth::StealthProfile::firefox_default(),
5831 );
5832 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
5834 let handle = WorkerHandle::new("worker.js".to_string());
5836 handle.set_worker_global_addr(fake_addr);
5837 handle.unregister_stealth_profile();
5838 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5840 bao_stealth::engine_props::clear_all_realm_profiles();
5842 }
5843
5844 #[test]
5845 fn test_teardown_result_crash_safe() {
5846 let result = WorkerTeardownResult {
5847 path: WorkerTeardownPath::Terminate,
5848 thread_joined: true,
5849 realm_profile_unregistered: true,
5850 closing_flag_set: true,
5851 never_registered: false,
5852 };
5853 assert!(result.is_crash_safe());
5854 }
5855
5856 #[test]
5857 fn test_teardown_result_not_crash_safe_no_join() {
5858 let result = WorkerTeardownResult {
5859 path: WorkerTeardownPath::PageUnload,
5860 thread_joined: false,
5861 realm_profile_unregistered: true,
5862 closing_flag_set: true,
5863 never_registered: false,
5864 };
5865 assert!(!result.is_crash_safe());
5866 }
5867
5868 #[test]
5869 fn test_teardown_result_not_crash_safe_no_closing() {
5870 let result = WorkerTeardownResult {
5871 path: WorkerTeardownPath::SelfClose,
5872 thread_joined: true,
5873 realm_profile_unregistered: true,
5874 closing_flag_set: false,
5875 never_registered: false,
5876 };
5877 assert!(!result.is_crash_safe());
5878 }
5879
5880 #[test]
5881 fn test_crash_safe_teardown_no_web_worker() {
5882 let handle = WorkerHandle::new("worker.js".to_string());
5884 let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::Terminate);
5885 assert!(result.closing_flag_set);
5886 assert!(result.thread_joined); assert!(!result.realm_profile_unregistered); assert!(handle.is_closing());
5889 assert!(handle.is_terminated());
5890 }
5891
5892 #[test]
5893 fn test_crash_safe_teardown_with_stealth_profile() {
5894 let fake_addr = 0xABCD0000_usize;
5896 bao_stealth::engine_props::set_profile_for_global(
5897 fake_addr,
5898 &bao_stealth::StealthProfile::firefox_default(),
5899 );
5900
5901 let handle = WorkerHandle::new("worker.js".to_string());
5902 handle.set_worker_global_addr(fake_addr);
5903
5904 let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::SelfClose);
5905 assert!(result.closing_flag_set);
5906 assert!(result.thread_joined);
5907 assert!(result.realm_profile_unregistered);
5908 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5910 assert!(handle.is_closing());
5911 assert!(handle.is_terminated());
5912 }
5913
5914 #[test]
5915 fn test_auto_close_worker_drop_unregisters_stealth_profile() {
5916 let fake_addr = 0xBEEF0000_usize;
5918 bao_stealth::engine_props::set_profile_for_global(
5919 fake_addr,
5920 &bao_stealth::StealthProfile::firefox_default(),
5921 );
5922
5923 let handle = WorkerHandle::new("worker.js".to_string());
5924 handle.set_worker_global_addr(fake_addr);
5925 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
5927
5928 let guard = AutoCloseWorker::new(handle);
5929 drop(guard);
5931 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5933 }
5934
5935 #[test]
5936 fn test_terminate_all_workers_unregisters_stealth_profiles() {
5937 let fake_addr1 = 0xAAAA0001_usize;
5939 let fake_addr2 = 0xAAAA0002_usize;
5940 bao_stealth::engine_props::set_profile_for_global(
5941 fake_addr1,
5942 &bao_stealth::StealthProfile::firefox_default(),
5943 );
5944 bao_stealth::engine_props::set_profile_for_global(
5945 fake_addr2,
5946 &bao_stealth::StealthProfile::firefox_default(),
5947 );
5948
5949 let mut state = BaoWebViewState::default();
5950 let h1 = WorkerHandle::new("worker1.js".to_string());
5951 h1.set_worker_global_addr(fake_addr1);
5952 let h2 = WorkerHandle::new("worker2.js".to_string());
5953 h2.set_worker_global_addr(fake_addr2);
5954 state.track_worker(h1);
5955 state.track_worker(h2);
5956
5957 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_some());
5959 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_some());
5960
5961 state.terminate_all_workers();
5963
5964 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_none());
5966 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_none());
5967 assert!(state.active_workers.iter().all(|g| g.handle().is_closing()));
5969 assert!(state
5970 .active_workers
5971 .iter()
5972 .all(|g| g.handle().is_terminated()));
5973 }
5974
5975 #[test]
5976 fn test_terminate_worker_via_path_terminate() {
5977 let fake_addr = 0xCCCC0001_usize;
5979 bao_stealth::engine_props::set_profile_for_global(
5980 fake_addr,
5981 &bao_stealth::StealthProfile::firefox_default(),
5982 );
5983
5984 let mut state = BaoWebViewState::default();
5985 let handle = WorkerHandle::new("worker.js".to_string());
5986 handle.set_worker_global_addr(fake_addr);
5987 let worker_id = WorkerId("worker.js".to_string());
5988 state.track_worker(handle.clone());
5989
5990 let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
5991 assert!(result.is_some());
5992 let result = result.unwrap();
5993 assert_eq!(result.path, WorkerTeardownPath::Terminate);
5994 assert!(result.closing_flag_set);
5995 assert!(result.thread_joined);
5996 assert!(result.realm_profile_unregistered);
5997 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5999 assert!(handle.is_closing());
6001 assert!(handle.is_terminated());
6002 }
6003
6004 #[test]
6005 fn test_terminate_worker_via_path_self_close() {
6006 let fake_addr = 0xDDDD0001_usize;
6008 bao_stealth::engine_props::set_profile_for_global(
6009 fake_addr,
6010 &bao_stealth::StealthProfile::firefox_default(),
6011 );
6012
6013 let mut state = BaoWebViewState::default();
6014 let handle = WorkerHandle::new("worker.js".to_string());
6015 handle.set_worker_global_addr(fake_addr);
6016 let worker_id = WorkerId("worker.js".to_string());
6017 state.track_worker(handle.clone());
6018
6019 let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::SelfClose);
6020 assert!(result.is_some());
6021 let result = result.unwrap();
6022 assert_eq!(result.path, WorkerTeardownPath::SelfClose);
6023 assert!(result.closing_flag_set);
6024 assert!(result.realm_profile_unregistered);
6025 }
6026
6027 #[test]
6028 fn test_terminate_worker_via_path_not_found() {
6029 let mut state = BaoWebViewState::default();
6030 let worker_id = WorkerId("nonexistent.js".to_string());
6031 let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
6032 assert!(result.is_none());
6033 }
6034
6035 #[test]
6036 fn test_three_paths_all_crash_safe() {
6037 for path in [
6039 WorkerTeardownPath::Terminate,
6040 WorkerTeardownPath::SelfClose,
6041 WorkerTeardownPath::PageUnload,
6042 ] {
6043 let fake_addr = 0x12340000_usize
6044 + match &path {
6045 WorkerTeardownPath::Terminate => 1,
6046 WorkerTeardownPath::SelfClose => 2,
6047 WorkerTeardownPath::PageUnload => 3,
6048 };
6049 bao_stealth::engine_props::set_profile_for_global(
6050 fake_addr,
6051 &bao_stealth::StealthProfile::firefox_default(),
6052 );
6053
6054 let handle = WorkerHandle::new("worker.js".to_string());
6055 handle.set_worker_global_addr(fake_addr);
6056 let result = crash_safe_teardown_worker(&handle, path.clone());
6057 assert!(
6058 result.closing_flag_set,
6059 "closing flag not set for {:?}",
6060 path
6061 );
6062 assert!(result.thread_joined, "thread not joined for {:?}", path);
6063 assert!(
6064 result.realm_profile_unregistered,
6065 "profile not unregistered for {:?}",
6066 path
6067 );
6068 assert!(result.is_crash_safe(), "not crash-safe for {:?}", path);
6069 assert!(handle.is_closing(), "handle not closing for {:?}", path);
6070 assert!(
6071 handle.is_terminated(),
6072 "handle not terminated for {:?}",
6073 path
6074 );
6075 assert!(
6076 bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none(),
6077 "profile not removed for {:?}",
6078 path
6079 );
6080 }
6081 }
6082
6083 #[test]
6084 fn test_track_worker_guard() {
6085 let handle = WorkerHandle::new("worker.js".to_string());
6086 let guard = AutoCloseWorker::new(handle);
6087 let mut state = BaoWebViewState::default();
6088 state.track_worker_guard(guard);
6089 assert_eq!(state.active_worker_count(), 1);
6090 }
6091
6092 #[test]
6096 fn test_worker_scope_config_default() {
6097 let config = WorkerScopeConfig::default();
6098 assert!(config.stealth_profile.is_none());
6099 assert!(config.user_agent.is_empty());
6100 assert!(config.platform.is_empty());
6101 assert!(config.hardware_concurrency > 0);
6102 assert_eq!(config.language, "en-US");
6103 assert!(!config.languages.is_empty());
6104 }
6105
6106 #[test]
6107 fn test_worker_scope_config_set_on_state() {
6108 let mut state = BaoWebViewState::default();
6109 let config = WorkerScopeConfig {
6110 stealth_profile: None,
6111 user_agent: "Bao/1.0".to_string(),
6112 platform: "Linux x86_64".to_string(),
6113 hardware_concurrency: 8,
6114 language: "zh-CN".to_string(),
6115 languages: vec!["zh-CN".to_string(), "zh".to_string(), "en".to_string()],
6116 };
6117 state.set_worker_scope_config(config);
6118 assert_eq!(state.worker_scope_config.user_agent, "Bao/1.0");
6119 assert_eq!(state.worker_scope_config.platform, "Linux x86_64");
6120 assert_eq!(state.worker_scope_config.hardware_concurrency, 8);
6121 assert_eq!(state.worker_scope_config.language, "zh-CN");
6122 assert_eq!(state.worker_scope_config.languages.len(), 3);
6123 }
6124
6125 #[test]
6126 fn test_webview_state_default_worker_scope_config() {
6127 let state = BaoWebViewState::default();
6128 assert!(state.worker_scope_config.stealth_profile.is_none());
6129 assert!(state.worker_scope_config.hardware_concurrency > 0);
6130 }
6131
6132 #[test]
6136 fn test_shared_worker_id_equality() {
6137 let id1 = SharedWorkerId {
6138 script_url: "sw.js".to_string(),
6139 name: "myworker".to_string(),
6140 };
6141 let id2 = SharedWorkerId {
6142 script_url: "sw.js".to_string(),
6143 name: "myworker".to_string(),
6144 };
6145 let id3 = SharedWorkerId {
6146 script_url: "sw.js".to_string(),
6147 name: "other".to_string(),
6148 };
6149 let id4 = SharedWorkerId {
6150 script_url: "other.js".to_string(),
6151 name: "myworker".to_string(),
6152 };
6153 assert_eq!(id1, id2);
6154 assert_ne!(id1, id3); assert_ne!(id1, id4); }
6157
6158 #[test]
6159 fn test_shared_worker_id_default_name() {
6160 let id = SharedWorkerId {
6161 script_url: "sw.js".to_string(),
6162 name: String::new(),
6163 };
6164 assert!(id.name.is_empty());
6165 }
6166
6167 #[test]
6168 fn test_shared_worker_handle_new_is_running() {
6169 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6170 assert_eq!(handle.script_url, "sw.js");
6171 assert_eq!(handle.name, "myname");
6172 assert!(!handle.is_closing());
6173 assert!(!handle.is_terminated());
6174 assert_eq!(handle.connected_page_count(), 0);
6175 }
6176
6177 #[test]
6178 fn test_shared_worker_handle_id() {
6179 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6180 let id = handle.id();
6181 assert_eq!(id.script_url, "sw.js");
6182 assert_eq!(id.name, "myname");
6183 }
6184
6185 #[test]
6186 fn test_shared_worker_handle_close() {
6187 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6188 assert!(!handle.is_closing());
6189 handle.close();
6190 assert!(handle.is_closing());
6191 handle.close();
6193 assert!(handle.is_closing());
6194 }
6195
6196 #[test]
6197 fn test_shared_worker_handle_mark_terminated() {
6198 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6199 assert!(!handle.is_terminated());
6200 handle.mark_terminated();
6201 assert!(handle.is_terminated());
6202 }
6203
6204 #[test]
6205 fn test_shared_worker_handle_connected_pages() {
6206 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6207 assert_eq!(handle.connected_page_count(), 0);
6208 handle.page_connected();
6209 assert_eq!(handle.connected_page_count(), 1);
6210 handle.page_connected();
6211 assert_eq!(handle.connected_page_count(), 2);
6212 handle.page_disconnected();
6213 assert_eq!(handle.connected_page_count(), 1);
6214 handle.page_disconnected();
6215 assert_eq!(handle.connected_page_count(), 0);
6216 }
6217
6218 #[test]
6219 fn test_shared_worker_handle_clone_shares_state() {
6220 let handle = SharedWorkerHandle::new("sw.js".to_string(), "name".to_string());
6221 let clone = handle.clone();
6222 handle.close();
6223 assert!(
6224 clone.is_closing(),
6225 "clone should see closing flag from original"
6226 );
6227 clone.mark_terminated();
6228 assert!(
6229 handle.is_terminated(),
6230 "original should see terminated flag from clone"
6231 );
6232 }
6233
6234 #[test]
6235 fn test_shared_worker_port_ref_increments_connected() {
6236 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6237 let port = SharedWorkerPortRef::new(handle.clone());
6238 assert_eq!(handle.connected_page_count(), 1);
6239 assert_eq!(port.handle().script_url, "sw.js");
6240 }
6241
6242 #[test]
6243 fn test_shared_worker_port_ref_drop_decrements_connected() {
6244 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6245 {
6246 let _port = SharedWorkerPortRef::new(handle.clone());
6247 assert_eq!(handle.connected_page_count(), 1);
6248 }
6249 assert_eq!(
6250 handle.connected_page_count(),
6251 0,
6252 "dropping port should decrement connected count"
6253 );
6254 }
6255
6256 #[test]
6257 fn test_shared_worker_port_ref_clone_increments_connected() {
6258 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6259 let port = SharedWorkerPortRef::new(handle.clone());
6260 assert_eq!(handle.connected_page_count(), 1);
6261 let _port2 = port.clone();
6262 assert_eq!(handle.connected_page_count(), 2);
6263 }
6264
6265 #[test]
6266 fn test_shared_worker_port_ref_multiple_pages() {
6267 let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
6268 let _port1 = SharedWorkerPortRef::new(handle.clone());
6269 let _port2 = SharedWorkerPortRef::new(handle.clone());
6270 assert_eq!(handle.connected_page_count(), 2);
6271 }
6272
6273 #[test]
6274 fn test_webview_state_track_shared_worker_port() {
6275 let mut state = BaoWebViewState::default();
6276 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6277 state.track_shared_worker_port(SharedWorkerPortRef::new(handle));
6278 assert_eq!(state.shared_worker_port_count(), 1);
6279 }
6280
6281 #[test]
6282 fn test_webview_state_disconnect_shared_worker_ports() {
6283 let mut state = BaoWebViewState::default();
6284 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6285 state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6286 state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6287 assert_eq!(state.shared_worker_port_count(), 2);
6288 assert_eq!(handle.connected_page_count(), 2);
6289 state.disconnect_shared_worker_ports();
6290 assert_eq!(state.shared_worker_port_count(), 0);
6291 assert_eq!(
6292 handle.connected_page_count(),
6293 0,
6294 "disconnect should drop ports and decrement counter"
6295 );
6296 }
6297
6298 #[test]
6299 fn test_webview_state_disconnect_shared_worker_ports_empty() {
6300 let mut state = BaoWebViewState::default();
6301 state.disconnect_shared_worker_ports();
6303 assert_eq!(state.shared_worker_port_count(), 0);
6304 }
6305
6306 #[test]
6307 fn test_delegate_register_shared_worker_new() {
6308 let delegate = BaoServoDelegate::new();
6309 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6310 let returned = delegate.register_shared_worker(handle);
6311 assert_eq!(returned.script_url, "sw.js");
6312 assert_eq!(returned.name, "myname");
6313 assert_eq!(delegate.shared_worker_count(), 1);
6314 }
6315
6316 #[test]
6317 fn test_delegate_register_shared_worker_dedup() {
6318 let delegate = BaoServoDelegate::new();
6319 let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6320 let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6321 delegate.register_shared_worker(handle1);
6322 let returned = delegate.register_shared_worker(handle2);
6323 assert_eq!(delegate.shared_worker_count(), 1);
6325 assert_eq!(returned.script_url, "sw.js");
6326 }
6327
6328 #[test]
6329 fn test_delegate_find_shared_worker() {
6330 let delegate = BaoServoDelegate::new();
6331 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6332 delegate.register_shared_worker(handle);
6333 let found = delegate.find_shared_worker("sw.js", "myname");
6334 assert!(found.is_some());
6335 assert_eq!(found.unwrap().script_url, "sw.js");
6336 assert!(delegate.find_shared_worker("other.js", "myname").is_none());
6337 assert!(delegate.find_shared_worker("sw.js", "other").is_none());
6338 }
6339
6340 #[test]
6341 fn test_delegate_reap_terminated_shared_workers() {
6342 let delegate = BaoServoDelegate::new();
6343 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6344 delegate.register_shared_worker(handle.clone());
6345 assert_eq!(delegate.shared_worker_count(), 1);
6346 handle.close();
6348 handle.mark_terminated();
6349 delegate.reap_terminated_shared_workers();
6350 assert_eq!(
6351 delegate.shared_worker_count(),
6352 0,
6353 "terminated shared worker with zero pages should be reaped"
6354 );
6355 }
6356
6357 #[test]
6358 fn test_delegate_reap_keeps_terminated_with_connected_pages() {
6359 let delegate = BaoServoDelegate::new();
6360 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6361 delegate.register_shared_worker(handle.clone());
6362 let _port = SharedWorkerPortRef::new(handle.clone());
6364 handle.close();
6365 handle.mark_terminated();
6366 delegate.reap_terminated_shared_workers();
6367 assert_eq!(
6368 delegate.shared_worker_count(),
6369 1,
6370 "terminated but still has connected pages — keep in registry"
6371 );
6372 }
6373
6374 #[test]
6375 fn test_shared_worker_connect_event_creation() {
6376 let event = SharedWorkerConnectEvent {
6377 shared_worker_id: SharedWorkerId {
6378 script_url: "sw.js".to_string(),
6379 name: "myname".to_string(),
6380 },
6381 page_url: "https://example.com/page1".to_string(),
6382 };
6383 assert_eq!(event.shared_worker_id.script_url, "sw.js");
6384 assert_eq!(event.shared_worker_id.name, "myname");
6385 assert_eq!(event.page_url, "https://example.com/page1");
6386 }
6387
6388 #[test]
6389 fn test_forward_shared_worker_connect_event() {
6390 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6391 let state = BaoWebViewState {
6392 event_tx: Some(tx),
6393 ..Default::default()
6394 };
6395 let event = SharedWorkerConnectEvent {
6396 shared_worker_id: SharedWorkerId {
6397 script_url: "sw.js".to_string(),
6398 name: "myname".to_string(),
6399 },
6400 page_url: "https://example.com".to_string(),
6401 };
6402 state.forward_shared_worker_connect_event(event);
6403 let recv = rx.try_recv().unwrap();
6404 match recv {
6405 ServoEvent::Console { level, text, .. } => {
6406 assert_eq!(level, ConsoleLevel::Debug);
6407 assert!(text.contains("sw.js"));
6408 assert!(text.contains("myname"));
6409 assert!(text.contains("https://example.com"));
6410 }
6411 _ => panic!("expected Console event for shared worker connect"),
6412 }
6413 }
6414
6415 #[test]
6416 fn test_forward_shared_worker_connect_event_no_tx() {
6417 let state = BaoWebViewState::default();
6418 let event = SharedWorkerConnectEvent {
6419 shared_worker_id: SharedWorkerId {
6420 script_url: "sw.js".to_string(),
6421 name: String::new(),
6422 },
6423 page_url: "https://example.com".to_string(),
6424 };
6425 state.forward_shared_worker_connect_event(event);
6427 }
6428
6429 #[test]
6430 fn test_shared_worker_scope_config_default() {
6431 let config = SharedWorkerScopeConfig::default();
6432 assert!(config.stealth_profile.is_none());
6433 assert!(config.user_agent.is_empty());
6434 assert!(config.platform.is_empty());
6435 assert!(config.hardware_concurrency > 0);
6436 assert_eq!(config.language, "en-US");
6437 assert!(!config.languages.is_empty());
6438 }
6439
6440 #[test]
6441 fn test_page_navigation_disconnects_shared_workers() {
6442 let mut state = BaoWebViewState::default();
6443 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6444 state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6445 assert_eq!(state.shared_worker_port_count(), 1);
6446 assert_eq!(handle.connected_page_count(), 1);
6447 state.disconnect_shared_worker_ports();
6449 assert_eq!(state.shared_worker_port_count(), 0);
6450 assert_eq!(handle.connected_page_count(), 0);
6451 }
6452
6453 #[test]
6454 fn test_shared_worker_cross_page_sharing() {
6455 let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
6457
6458 let mut state1 = BaoWebViewState::default();
6460 state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6461 assert_eq!(handle.connected_page_count(), 1);
6462
6463 let mut state2 = BaoWebViewState::default();
6465 state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6466 assert_eq!(handle.connected_page_count(), 2);
6467
6468 state1.disconnect_shared_worker_ports();
6470 assert_eq!(handle.connected_page_count(), 1);
6471
6472 assert_eq!(state2.shared_worker_port_count(), 1);
6474
6475 assert!(!handle.is_closing());
6477 assert!(!handle.is_terminated());
6478 }
6479
6480 #[test]
6484 fn test_structured_clone_payload_creation() {
6485 let payload = StructuredClonePayload {
6486 data: vec![1, 2, 3, 4, 5],
6487 transferable_count: 0,
6488 };
6489 assert_eq!(payload.data.len(), 5);
6490 assert_eq!(payload.transferable_count, 0);
6491 }
6492
6493 #[test]
6494 fn test_structured_clone_payload_with_transferables() {
6495 let payload = StructuredClonePayload {
6496 data: vec![0u8; 1024],
6497 transferable_count: 2,
6498 };
6499 assert_eq!(payload.data.len(), 1024);
6500 assert_eq!(payload.transferable_count, 2);
6501 }
6502
6503 #[test]
6504 fn test_structured_clone_payload_clone() {
6505 let payload = StructuredClonePayload {
6506 data: vec![42u8; 100],
6507 transferable_count: 1,
6508 };
6509 let cloned = payload.clone();
6510 assert_eq!(cloned.data, payload.data);
6511 assert_eq!(cloned.transferable_count, payload.transferable_count);
6512 }
6513
6514 #[test]
6515 fn test_worker_structured_message_metadata_only() {
6516 let msg = WorkerStructuredMessage::metadata_only(
6517 WorkerId("worker1.js".to_string()),
6518 WorkerMessageDirection::PageToWorker,
6519 );
6520 assert!(msg.payload.is_none());
6521 assert_eq!(msg.worker_id.0, "worker1.js");
6522 assert_eq!(msg.direction, WorkerMessageDirection::PageToWorker);
6523 assert!(msg.message_id > 0);
6524 }
6525
6526 #[test]
6527 fn test_worker_structured_message_with_payload() {
6528 let msg = WorkerStructuredMessage::with_payload(
6529 WorkerId("worker2.js".to_string()),
6530 WorkerMessageDirection::WorkerToPage,
6531 vec![1, 2, 3],
6532 1,
6533 );
6534 assert!(msg.payload.is_some());
6535 let payload = msg.payload.unwrap();
6536 assert_eq!(payload.data, vec![1, 2, 3]);
6537 assert_eq!(payload.transferable_count, 1);
6538 assert_eq!(msg.direction, WorkerMessageDirection::WorkerToPage);
6539 }
6540
6541 #[test]
6542 fn test_worker_structured_message_unique_ids() {
6543 let msg1 = WorkerStructuredMessage::metadata_only(
6544 WorkerId("w.js".to_string()),
6545 WorkerMessageDirection::PageToWorker,
6546 );
6547 let msg2 = WorkerStructuredMessage::metadata_only(
6548 WorkerId("w.js".to_string()),
6549 WorkerMessageDirection::PageToWorker,
6550 );
6551 assert_ne!(msg1.message_id, msg2.message_id);
6553 }
6554
6555 #[test]
6556 fn test_worker_channel_bridge_creation() {
6557 let worker_id = WorkerId("worker1.js".to_string());
6558 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id.clone());
6559 assert_eq!(bridge.worker_id, worker_id);
6560 assert_eq!(endpoints.worker_id, worker_id);
6561 assert!(endpoints.page_to_worker_rx.is_some());
6563 assert!(endpoints.worker_to_page_tx.is_some());
6564 }
6565
6566 #[test]
6567 fn test_worker_channel_bridge_page_to_worker() {
6568 let worker_id = WorkerId("worker1.js".to_string());
6569 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6570 let payload = StructuredClonePayload {
6572 data: vec![1, 2, 3],
6573 transferable_count: 0,
6574 };
6575 bridge.post_message_to_worker(payload).unwrap();
6576 let rx = endpoints.page_to_worker_rx.unwrap();
6578 let received = rx.try_recv().unwrap();
6579 assert_eq!(received.data, vec![1, 2, 3]);
6580 }
6581
6582 #[test]
6583 fn test_worker_channel_bridge_worker_to_page() {
6584 let worker_id = WorkerId("worker1.js".to_string());
6585 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6586 let msg = WorkerStructuredMessage::with_payload(
6588 WorkerId("worker1.js".to_string()),
6589 WorkerMessageDirection::WorkerToPage,
6590 vec![4, 5, 6],
6591 0,
6592 );
6593 let tx = endpoints.worker_to_page_tx.unwrap();
6594 tx.send(msg).unwrap();
6595 let result = bridge.try_recv_from_worker().unwrap();
6597 assert!(result.is_some());
6598 let received = result.unwrap();
6599 assert_eq!(received.payload.unwrap().data, vec![4, 5, 6]);
6600 }
6601
6602 #[test]
6603 fn test_worker_channel_bridge_drain() {
6604 let worker_id = WorkerId("worker1.js".to_string());
6605 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6606 let tx = endpoints.worker_to_page_tx.unwrap();
6607 for i in 0..3 {
6609 let msg = WorkerStructuredMessage::with_payload(
6610 WorkerId("worker1.js".to_string()),
6611 WorkerMessageDirection::WorkerToPage,
6612 vec![i],
6613 0,
6614 );
6615 tx.send(msg).unwrap();
6616 }
6617 let result = bridge.drain_worker_messages();
6619 assert_eq!(result.messages.len(), 3);
6620 assert!(!result.disconnected);
6621 let empty = bridge.drain_worker_messages();
6623 assert!(empty.messages.is_empty());
6624 assert!(!empty.disconnected);
6625 }
6626
6627 #[test]
6628 fn test_webview_state_worker_channel_registration() {
6629 let mut state = BaoWebViewState::default();
6630 let worker_id = WorkerId("worker1.js".to_string());
6631 let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id.clone());
6632 state.register_worker_channel(bridge);
6633 assert_eq!(state.worker_channel_count(), 1);
6634 assert!(state.worker_channel(&worker_id).is_some());
6635 }
6636
6637 #[test]
6638 fn test_webview_state_create_worker_channel() {
6639 let mut state = BaoWebViewState::default();
6640 let worker_id = WorkerId("worker1.js".to_string());
6641 let endpoints = state.create_worker_channel(worker_id.clone());
6642 assert_eq!(state.worker_channel_count(), 1);
6643 assert_eq!(endpoints.worker_id, worker_id);
6644 assert!(endpoints.page_to_worker_rx.is_some());
6645 assert!(endpoints.worker_to_page_tx.is_some());
6646 }
6647
6648 #[test]
6649 fn test_webview_state_post_to_worker() {
6650 let mut state = BaoWebViewState::default();
6651 let worker_id = WorkerId("worker1.js".to_string());
6652 let endpoints = state.create_worker_channel(worker_id.clone());
6653 let payload = StructuredClonePayload {
6654 data: vec![42],
6655 transferable_count: 0,
6656 };
6657 let result = state.post_to_worker(&worker_id, payload);
6659 assert!(result.is_ok());
6660 let rx = endpoints.page_to_worker_rx.unwrap();
6662 let received = rx.try_recv().unwrap();
6663 assert_eq!(received.data, vec![42]);
6664 let result = state.post_to_worker(
6666 &WorkerId("nonexistent.js".to_string()),
6667 StructuredClonePayload {
6668 data: vec![],
6669 transferable_count: 0,
6670 },
6671 );
6672 assert!(result.is_err());
6673 }
6674
6675 #[test]
6676 fn test_webview_state_drain_all_worker_messages() {
6677 let mut state = BaoWebViewState::default();
6678 let worker_id1 = WorkerId("worker1.js".to_string());
6679 let worker_id2 = WorkerId("worker2.js".to_string());
6680 let endpoints1 = state.create_worker_channel(worker_id1);
6681 let endpoints2 = state.create_worker_channel(worker_id2);
6682 let tx1 = endpoints1.worker_to_page_tx.unwrap();
6684 let tx2 = endpoints2.worker_to_page_tx.unwrap();
6685 tx1.send(WorkerStructuredMessage::metadata_only(
6686 WorkerId("worker1.js".to_string()),
6687 WorkerMessageDirection::WorkerToPage,
6688 ))
6689 .unwrap();
6690 tx2.send(WorkerStructuredMessage::metadata_only(
6691 WorkerId("worker2.js".to_string()),
6692 WorkerMessageDirection::WorkerToPage,
6693 ))
6694 .unwrap();
6695 let (messages, disconnected) = state.drain_all_worker_messages();
6697 assert_eq!(messages.len(), 2);
6698 assert!(disconnected.is_empty());
6699 }
6700
6701 #[test]
6702 fn test_webview_state_terminate_clears_channels() {
6703 let mut state = BaoWebViewState::default();
6704 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
6705 state.create_worker_channel(WorkerId("worker1.js".to_string()));
6706 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
6707 state.create_worker_channel(WorkerId("worker2.js".to_string()));
6708 assert_eq!(state.worker_channel_count(), 2);
6709 state.terminate_all_workers();
6711 assert_eq!(state.worker_channel_count(), 0);
6712 }
6713
6714 #[test]
6715 fn test_webview_state_reap_terminated_worker_channels() {
6716 let mut state = BaoWebViewState::default();
6717 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
6718 state.create_worker_channel(WorkerId("worker1.js".to_string()));
6719 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
6720 state.create_worker_channel(WorkerId("worker2.js".to_string()));
6721 state.active_workers[0].handle().terminate();
6723 state.active_workers[0].handle().mark_terminated();
6724 state.reap_terminated_workers();
6725 assert_eq!(state.worker_channel_count(), 1);
6727 assert!(state
6728 .worker_channel(&WorkerId("worker2.js".to_string()))
6729 .is_some());
6730 }
6731
6732 #[test]
6733 fn test_webview_state_remove_worker_channel() {
6734 let mut state = BaoWebViewState::default();
6735 let worker_id = WorkerId("worker1.js".to_string());
6736 state.create_worker_channel(worker_id.clone());
6737 assert_eq!(state.worker_channel_count(), 1);
6738 let removed = state.remove_worker_channel(&worker_id);
6739 assert!(removed.is_some());
6740 assert_eq!(state.worker_channel_count(), 0);
6741 }
6742
6743 #[test]
6744 fn test_forward_worker_structured_message_with_payload() {
6745 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6746 let state = BaoWebViewState {
6747 event_tx: Some(tx),
6748 ..Default::default()
6749 };
6750 let msg = WorkerStructuredMessage::with_payload(
6751 WorkerId("worker1.js".to_string()),
6752 WorkerMessageDirection::WorkerToPage,
6753 vec![1, 2, 3],
6754 1,
6755 );
6756 state.forward_worker_structured_message(&msg);
6757 let event = rx.try_recv().unwrap();
6758 match event {
6759 ServoEvent::Console { level, text, .. } => {
6760 assert_eq!(level, ConsoleLevel::Debug);
6761 assert!(text.contains("worker→page"));
6762 assert!(text.contains("worker1.js"));
6763 assert!(text.contains("3 bytes"));
6764 assert!(text.contains("1 transferable"));
6765 }
6766 _ => panic!("expected Console event for structured message"),
6767 }
6768 }
6769
6770 #[test]
6771 fn test_forward_worker_structured_message_metadata_only() {
6772 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6773 let state = BaoWebViewState {
6774 event_tx: Some(tx),
6775 ..Default::default()
6776 };
6777 let msg = WorkerStructuredMessage::metadata_only(
6778 WorkerId("worker1.js".to_string()),
6779 WorkerMessageDirection::PageToWorker,
6780 );
6781 state.forward_worker_structured_message(&msg);
6782 let event = rx.try_recv().unwrap();
6783 match event {
6784 ServoEvent::Console { text, .. } => {
6785 assert!(text.contains("metadata-only"));
6786 }
6787 _ => panic!("expected Console event"),
6788 }
6789 }
6790
6791 #[test]
6792 fn test_drain_and_forward_worker_messages() {
6793 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6794 let mut state = BaoWebViewState {
6795 event_tx: Some(tx),
6796 ..Default::default()
6797 };
6798 let endpoints = state.create_worker_channel(WorkerId("worker1.js".to_string()));
6799 let worker_tx = endpoints.worker_to_page_tx.unwrap();
6800 worker_tx
6801 .send(WorkerStructuredMessage::metadata_only(
6802 WorkerId("worker1.js".to_string()),
6803 WorkerMessageDirection::WorkerToPage,
6804 ))
6805 .unwrap();
6806 let disconnected = state.drain_and_forward_worker_messages();
6808 assert!(disconnected.is_empty());
6809 let event = rx.try_recv().unwrap();
6811 match event {
6812 ServoEvent::Console { text, .. } => {
6813 assert!(text.contains("worker→page"));
6814 }
6815 _ => panic!("expected Console event"),
6816 }
6817 }
6818
6819 #[test]
6820 fn test_worker_channel_bridge_disconnected() {
6821 let worker_id = WorkerId("worker1.js".to_string());
6822 let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id);
6823 let result = bridge.try_recv_from_worker();
6828 assert!(result.is_ok()); assert!(result.unwrap().is_none());
6830 }
6831
6832 #[test]
6836 fn test_worker_location_from_https_url() {
6837 let loc = WorkerLocation::from_url("https://example.com:8080/path?q=1#hash").unwrap();
6838 assert_eq!(loc.href, "https://example.com:8080/path?q=1#hash");
6839 assert_eq!(loc.protocol, "https:");
6840 assert_eq!(loc.host, "example.com:8080");
6841 assert_eq!(loc.hostname, "example.com");
6842 assert_eq!(loc.port, "8080");
6843 assert_eq!(loc.pathname, "/path");
6844 assert_eq!(loc.search, "?q=1");
6845 assert_eq!(loc.hash, "#hash");
6846 assert_eq!(loc.origin, "https://example.com:8080");
6847 }
6848
6849 #[test]
6850 fn test_worker_location_from_default_port() {
6851 let loc = WorkerLocation::from_url("https://example.com/path").unwrap();
6852 assert_eq!(loc.host, "example.com");
6853 assert_eq!(loc.port, "");
6854 assert_eq!(loc.origin, "https://example.com");
6855 }
6856
6857 #[test]
6858 fn test_worker_location_from_http_url() {
6859 let loc = WorkerLocation::from_url("http://localhost:3000/worker.js").unwrap();
6860 assert_eq!(loc.protocol, "http:");
6861 assert_eq!(loc.hostname, "localhost");
6862 assert_eq!(loc.port, "3000");
6863 assert_eq!(loc.pathname, "/worker.js");
6864 }
6865
6866 #[test]
6867 fn test_worker_location_from_url_no_query_no_hash() {
6868 let loc = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
6869 assert_eq!(loc.search, "");
6870 assert_eq!(loc.hash, "");
6871 }
6872
6873 #[test]
6874 fn test_worker_location_from_invalid_url() {
6875 assert!(WorkerLocation::from_url("not a url").is_none());
6876 }
6877
6878 #[test]
6879 fn test_worker_location_from_url_value() {
6880 let url = url::Url::parse("https://example.com/worker.js").unwrap();
6881 let loc = WorkerLocation::from_url_value(url);
6882 assert_eq!(loc.protocol, "https:");
6883 assert_eq!(loc.hostname, "example.com");
6884 assert_eq!(loc.pathname, "/worker.js");
6885 }
6886
6887 #[test]
6891 fn test_worker_navigator_default() {
6892 let nav = WorkerNavigator::default();
6893 assert!(nav.user_agent.is_empty());
6894 assert!(nav.platform.is_empty());
6895 assert!(nav.hardware_concurrency > 0);
6896 assert_eq!(nav.language, "en-US");
6897 assert!(!nav.languages.is_empty());
6898 assert!(nav.connection.is_none());
6899 assert!(!nav.cookie_enabled);
6900 assert_eq!(nav.max_touch_points, 0);
6901 assert_eq!(nav.product, "Gecko");
6902 assert_eq!(nav.app_code_name, "Mozilla");
6903 assert_eq!(nav.app_name, "Netscape");
6904 assert!(nav.app_version.is_empty());
6905 }
6906
6907 #[test]
6908 fn test_worker_navigator_from_scope_config() {
6909 let config = WorkerScopeConfig {
6910 stealth_profile: None,
6911 user_agent: "Bao/1.0".to_string(),
6912 platform: "Linux x86_64".to_string(),
6913 hardware_concurrency: 8,
6914 language: "zh-CN".to_string(),
6915 languages: vec!["zh-CN".to_string(), "zh".to_string()],
6916 };
6917 let nav = WorkerNavigator::from_scope_config(&config);
6918 assert_eq!(nav.user_agent, "Bao/1.0");
6919 assert_eq!(nav.platform, "Linux x86_64");
6920 assert_eq!(nav.hardware_concurrency, 8);
6921 assert_eq!(nav.language, "zh-CN");
6922 assert_eq!(nav.languages.len(), 2);
6923 assert_eq!(nav.app_version, "Bao/1.0"); assert_eq!(nav.product, "Gecko");
6925 assert_eq!(nav.app_code_name, "Mozilla");
6926 assert_eq!(nav.app_name, "Netscape");
6927 }
6928
6929 #[test]
6930 fn test_worker_navigator_from_shared_scope_config() {
6931 let config = SharedWorkerScopeConfig {
6932 stealth_profile: None,
6933 user_agent: "Bao/2.0".to_string(),
6934 platform: "MacOS".to_string(),
6935 hardware_concurrency: 4,
6936 language: "ja".to_string(),
6937 languages: vec!["ja".to_string(), "en".to_string()],
6938 };
6939 let nav = WorkerNavigator::from_scope_config(&config);
6940 assert_eq!(nav.user_agent, "Bao/2.0");
6941 assert_eq!(nav.platform, "MacOS");
6942 assert_eq!(nav.hardware_concurrency, 4);
6943 assert_eq!(nav.app_version, "Bao/2.0");
6944 }
6945
6946 #[test]
6947 fn test_worker_network_information() {
6948 let info = WorkerNetworkInformation {
6949 effective_type: "4g".to_string(),
6950 downlink: 10,
6951 rtt: 50,
6952 save_data: false,
6953 };
6954 assert_eq!(info.effective_type, "4g");
6955 assert_eq!(info.downlink, 10);
6956 assert_eq!(info.rtt, 50);
6957 assert!(!info.save_data);
6958 }
6959
6960 #[test]
6964 fn test_worker_global_scope_state_new() {
6965 let config = WorkerScopeConfig {
6966 stealth_profile: None,
6967 user_agent: "Bao/1.0".to_string(),
6968 platform: "Linux".to_string(),
6969 hardware_concurrency: 8,
6970 language: "en-US".to_string(),
6971 languages: vec!["en-US".to_string()],
6972 };
6973 let scope =
6974 WorkerGlobalScopeState::new("https://example.com/worker.js".to_string(), &config);
6975 assert_eq!(scope.worker_url, "https://example.com/worker.js");
6976 assert!(!scope.closing);
6977 assert!(scope.location.is_some());
6978 assert_eq!(scope.navigator.user_agent, "Bao/1.0");
6979 }
6980
6981 #[test]
6982 fn test_worker_global_scope_state_new_shared() {
6983 let config = SharedWorkerScopeConfig {
6984 stealth_profile: None,
6985 user_agent: "Bao/2.0".to_string(),
6986 platform: "MacOS".to_string(),
6987 hardware_concurrency: 4,
6988 language: "ja".to_string(),
6989 languages: vec!["ja".to_string()],
6990 };
6991 let scope =
6992 WorkerGlobalScopeState::new_shared("https://example.com/sw.js".to_string(), &config);
6993 assert_eq!(scope.worker_url, "https://example.com/sw.js");
6994 assert_eq!(scope.navigator.user_agent, "Bao/2.0");
6995 }
6996
6997 #[test]
6998 fn test_worker_global_scope_state_location_parsed() {
6999 let config = WorkerScopeConfig::default();
7000 let scope = WorkerGlobalScopeState::new(
7001 "https://example.com:8080/app/worker.js?debug=true#section".to_string(),
7002 &config,
7003 );
7004 let loc = scope.location.unwrap();
7005 assert_eq!(loc.hostname, "example.com");
7006 assert_eq!(loc.port, "8080");
7007 assert_eq!(loc.pathname, "/app/worker.js");
7008 assert_eq!(loc.search, "?debug=true");
7009 assert_eq!(loc.hash, "#section");
7010 }
7011
7012 #[test]
7013 fn test_worker_global_scope_state_invalid_url_no_location() {
7014 let config = WorkerScopeConfig::default();
7015 let scope = WorkerGlobalScopeState::new("not-a-url".to_string(), &config);
7016 assert!(scope.location.is_none());
7017 }
7018
7019 #[test]
7023 fn test_dedicated_worker_global_scope_state_new() {
7024 let worker_id = WorkerId("https://example.com/worker.js".to_string());
7025 let config = WorkerScopeConfig {
7026 stealth_profile: None,
7027 user_agent: "Bao/1.0".to_string(),
7028 platform: "Linux".to_string(),
7029 hardware_concurrency: 8,
7030 language: "en-US".to_string(),
7031 languages: vec!["en-US".to_string()],
7032 };
7033 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7034 assert_eq!(scope.worker_id, worker_id);
7035 assert!(!scope.has_onmessage);
7036 assert!(!scope.has_onerror);
7037 assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
7038 }
7039
7040 #[test]
7041 fn test_dedicated_worker_global_scope_state_location() {
7042 let worker_id = WorkerId("https://example.com/worker.js".to_string());
7043 let config = WorkerScopeConfig::default();
7044 let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7045 let loc = scope.location().unwrap();
7046 assert_eq!(loc.hostname, "example.com");
7047 assert_eq!(loc.pathname, "/worker.js");
7048 }
7049
7050 #[test]
7051 fn test_dedicated_worker_global_scope_state_navigator() {
7052 let worker_id = WorkerId("worker.js".to_string());
7053 let config = WorkerScopeConfig {
7054 stealth_profile: None,
7055 user_agent: "Bao/1.0".to_string(),
7056 platform: "Linux".to_string(),
7057 hardware_concurrency: 8,
7058 language: "zh-CN".to_string(),
7059 languages: vec!["zh-CN".to_string()],
7060 };
7061 let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7062 let nav = scope.navigator();
7063 assert_eq!(nav.user_agent, "Bao/1.0");
7064 assert_eq!(nav.hardware_concurrency, 8);
7065 }
7066
7067 #[test]
7068 fn test_dedicated_worker_global_scope_state_event_handlers() {
7069 let worker_id = WorkerId("worker.js".to_string());
7070 let config = WorkerScopeConfig::default();
7071 let mut scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7072 assert!(!scope.has_onmessage);
7073 assert!(!scope.has_onerror);
7074 scope.set_onmessage();
7075 assert!(scope.has_onmessage);
7076 assert!(!scope.has_onerror);
7077 scope.set_onerror();
7078 assert!(scope.has_onmessage);
7079 assert!(scope.has_onerror);
7080 }
7081
7082 #[test]
7086 fn test_webview_state_dedicated_worker_scope_registration() {
7087 let mut state = BaoWebViewState::default();
7088 let worker_id = WorkerId("worker1.js".to_string());
7089 let config = WorkerScopeConfig::default();
7090 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7091 state.register_dedicated_worker_scope(worker_id.clone(), scope);
7092 assert_eq!(state.dedicated_worker_scope_count(), 1);
7093 assert!(state.dedicated_worker_scope(&worker_id).is_some());
7094 }
7095
7096 #[test]
7097 fn test_webview_state_dedicated_worker_scope_get_mut() {
7098 let mut state = BaoWebViewState::default();
7099 let worker_id = WorkerId("worker1.js".to_string());
7100 let config = WorkerScopeConfig::default();
7101 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7102 state.register_dedicated_worker_scope(worker_id.clone(), scope);
7103 state
7105 .dedicated_worker_scope_mut(&worker_id)
7106 .unwrap()
7107 .set_onmessage();
7108 assert!(
7109 state
7110 .dedicated_worker_scope(&worker_id)
7111 .unwrap()
7112 .has_onmessage
7113 );
7114 }
7115
7116 #[test]
7117 fn test_webview_state_dedicated_worker_scope_remove() {
7118 let mut state = BaoWebViewState::default();
7119 let worker_id = WorkerId("worker1.js".to_string());
7120 let config = WorkerScopeConfig::default();
7121 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7122 state.register_dedicated_worker_scope(worker_id.clone(), scope);
7123 let removed = state.remove_dedicated_worker_scope(&worker_id);
7124 assert!(removed.is_some());
7125 assert_eq!(state.dedicated_worker_scope_count(), 0);
7126 }
7127
7128 #[test]
7129 fn test_webview_state_dedicated_worker_scopes_snapshot() {
7130 let mut state = BaoWebViewState::default();
7131 let config = WorkerScopeConfig::default();
7132 let id1 = WorkerId("worker1.js".to_string());
7133 let id2 = WorkerId("worker2.js".to_string());
7134 state.register_dedicated_worker_scope(
7135 id1,
7136 DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7137 );
7138 state.register_dedicated_worker_scope(
7139 id2,
7140 DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
7141 );
7142 let scopes = state.dedicated_worker_scopes();
7143 assert_eq!(scopes.len(), 2);
7144 }
7145
7146 #[test]
7147 fn test_webview_state_terminate_clears_dedicated_worker_scopes() {
7148 let mut state = BaoWebViewState::default();
7149 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7150 let config = WorkerScopeConfig::default();
7151 state.register_dedicated_worker_scope(
7152 WorkerId("worker1.js".to_string()),
7153 DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7154 );
7155 assert_eq!(state.dedicated_worker_scope_count(), 1);
7156 state.terminate_all_workers();
7157 assert_eq!(state.dedicated_worker_scope_count(), 0);
7158 }
7159
7160 #[test]
7161 fn test_webview_state_reap_terminated_dedicated_worker_scopes() {
7162 let mut state = BaoWebViewState::default();
7163 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7164 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
7165 let config = WorkerScopeConfig::default();
7166 state.register_dedicated_worker_scope(
7167 WorkerId("worker1.js".to_string()),
7168 DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7169 );
7170 state.register_dedicated_worker_scope(
7171 WorkerId("worker2.js".to_string()),
7172 DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
7173 );
7174 state.active_workers[0].handle().terminate();
7176 state.active_workers[0].handle().mark_terminated();
7177 state.reap_terminated_workers();
7178 assert_eq!(state.dedicated_worker_scope_count(), 1);
7180 assert!(state
7181 .dedicated_worker_scope(&WorkerId("worker2.js".to_string()))
7182 .is_some());
7183 }
7184
7185 #[test]
7186 fn test_worker_location_equality() {
7187 let loc1 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
7188 let loc2 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
7189 assert_eq!(loc1, loc2);
7190 }
7191
7192 #[test]
7196 fn test_worker_script_source_inline() {
7197 let source = WorkerScriptSource::Inline("var x = 1;".to_string());
7198 assert_eq!(source, WorkerScriptSource::Inline("var x = 1;".to_string()));
7199 assert_ne!(source, WorkerScriptSource::Inline("var y = 2;".to_string()));
7200 }
7201
7202 #[test]
7203 fn test_worker_script_source_url() {
7204 let source = WorkerScriptSource::Url("https://example.com/worker.js".to_string());
7205 assert_eq!(
7206 source,
7207 WorkerScriptSource::Url("https://example.com/worker.js".to_string())
7208 );
7209 assert_ne!(
7210 source,
7211 WorkerScriptSource::Url("https://other.com/worker.js".to_string())
7212 );
7213 }
7214
7215 #[test]
7216 fn test_worker_script_load_result() {
7217 let result = WorkerScriptLoadResult {
7218 source: "self.onmessage = function(e) {}".to_string(),
7219 final_url: "https://example.com/worker.js".to_string(),
7220 mime_type: Some("text/javascript".to_string()),
7221 };
7222 assert_eq!(result.source, "self.onmessage = function(e) {}");
7223 assert_eq!(result.final_url, "https://example.com/worker.js");
7224 assert_eq!(result.mime_type.as_deref(), Some("text/javascript"));
7225 }
7226
7227 #[test]
7228 fn test_worker_script_load_error_network() {
7229 let err = WorkerScriptLoadError::NetworkError("404 Not Found".to_string());
7230 assert_eq!(
7231 err,
7232 WorkerScriptLoadError::NetworkError("404 Not Found".to_string())
7233 );
7234 }
7235
7236 #[test]
7237 fn test_worker_script_load_error_invalid_mime() {
7238 let err = WorkerScriptLoadError::InvalidMimeType {
7239 received: "text/html".to_string(),
7240 url: "https://example.com/worker.js".to_string(),
7241 };
7242 match err {
7243 WorkerScriptLoadError::InvalidMimeType { received, url } => {
7244 assert_eq!(received, "text/html");
7245 assert_eq!(url, "https://example.com/worker.js");
7246 }
7247 _ => panic!("expected InvalidMimeType"),
7248 }
7249 }
7250
7251 #[test]
7252 fn test_worker_script_load_error_utf8() {
7253 let err = WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string());
7254 assert_eq!(
7255 err,
7256 WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string())
7257 );
7258 }
7259
7260 #[test]
7261 fn test_worker_script_load_error_invalid_url() {
7262 let err = WorkerScriptLoadError::InvalidUrl("bad url".to_string());
7263 assert_eq!(
7264 err,
7265 WorkerScriptLoadError::InvalidUrl("bad url".to_string())
7266 );
7267 }
7268
7269 #[test]
7270 fn test_worker_script_load_error_cancelled() {
7271 let err = WorkerScriptLoadError::Cancelled;
7272 assert_eq!(err, WorkerScriptLoadError::Cancelled);
7273 }
7274
7275 #[test]
7276 fn test_worker_script_type_default_classic() {
7277 assert_eq!(WorkerScriptType::default(), WorkerScriptType::Classic);
7278 }
7279
7280 #[test]
7281 fn test_worker_script_type_equality() {
7282 assert_eq!(WorkerScriptType::Classic, WorkerScriptType::Classic);
7283 assert_eq!(WorkerScriptType::Module, WorkerScriptType::Module);
7284 assert_ne!(WorkerScriptType::Classic, WorkerScriptType::Module);
7285 }
7286
7287 #[test]
7288 fn test_is_javascript_mime_type_valid() {
7289 assert!(is_javascript_mime_type("text/javascript"));
7290 assert!(is_javascript_mime_type("application/javascript"));
7291 assert!(is_javascript_mime_type("application/ecmascript"));
7292 assert!(is_javascript_mime_type("application/x-javascript"));
7293 assert!(is_javascript_mime_type("text/ecmascript"));
7294 assert!(is_javascript_mime_type("text/x-javascript"));
7295 assert!(is_javascript_mime_type("text/jscript"));
7296 assert!(is_javascript_mime_type("text/livescript"));
7297 }
7298
7299 #[test]
7300 fn test_is_javascript_mime_type_case_insensitive() {
7301 assert!(is_javascript_mime_type("Text/JavaScript"));
7302 assert!(is_javascript_mime_type("APPLICATION/JAVASCRIPT"));
7303 assert!(is_javascript_mime_type("text/JavaScript"));
7304 }
7305
7306 #[test]
7307 fn test_is_javascript_mime_type_with_charset() {
7308 assert!(is_javascript_mime_type("text/javascript; charset=utf-8"));
7310 assert!(is_javascript_mime_type(
7311 "application/javascript;charset=utf-8"
7312 ));
7313 }
7314
7315 #[test]
7316 fn test_is_javascript_mime_type_invalid() {
7317 assert!(!is_javascript_mime_type("text/html"));
7318 assert!(!is_javascript_mime_type("application/json"));
7319 assert!(!is_javascript_mime_type("text/plain"));
7320 assert!(!is_javascript_mime_type("application/octet-stream"));
7321 assert!(!is_javascript_mime_type("text/css"));
7322 }
7323
7324 #[test]
7325 fn test_worker_script_loader_inline() {
7326 let loader =
7327 WorkerScriptLoader::inline("var x = 1;".to_string(), WorkerScriptType::Classic);
7328 assert!(loader.script_url().is_none());
7329 assert!(!loader.requires_fetch());
7330 let resolved = loader.resolve().unwrap();
7331 assert_eq!(
7332 resolved,
7333 WorkerScriptSource::Inline("var x = 1;".to_string())
7334 );
7335 }
7336
7337 #[test]
7338 fn test_worker_script_loader_url_https() {
7339 let loader = WorkerScriptLoader::url(
7340 "https://example.com/worker.js".to_string(),
7341 WorkerScriptType::Classic,
7342 );
7343 assert_eq!(loader.script_url(), Some("https://example.com/worker.js"));
7344 assert!(loader.requires_fetch());
7345 let resolved = loader.resolve().unwrap();
7346 assert_eq!(
7347 resolved,
7348 WorkerScriptSource::Url("https://example.com/worker.js".to_string())
7349 );
7350 }
7351
7352 #[test]
7353 fn test_worker_script_loader_url_http() {
7354 let loader = WorkerScriptLoader::url(
7355 "http://localhost:3000/worker.js".to_string(),
7356 WorkerScriptType::Module,
7357 );
7358 assert!(loader.requires_fetch());
7359 assert_eq!(loader.script_type, WorkerScriptType::Module);
7360 }
7361
7362 #[test]
7363 fn test_worker_script_loader_url_invalid() {
7364 let loader = WorkerScriptLoader::url("not a url".to_string(), WorkerScriptType::Classic);
7365 let result = loader.resolve();
7366 assert!(result.is_err());
7367 match result.unwrap_err() {
7368 WorkerScriptLoadError::InvalidUrl(msg) => {
7369 assert!(msg.contains("Invalid Worker script URL"));
7370 }
7371 _ => panic!("expected InvalidUrl error"),
7372 }
7373 }
7374
7375 #[test]
7376 fn test_worker_script_loader_url_unsupported_scheme() {
7377 let loader = WorkerScriptLoader::url(
7378 "ftp://example.com/worker.js".to_string(),
7379 WorkerScriptType::Classic,
7380 );
7381 let result = loader.resolve();
7382 assert!(result.is_err());
7383 match result.unwrap_err() {
7384 WorkerScriptLoadError::InvalidUrl(msg) => {
7385 assert!(msg.contains("Unsupported") || msg.contains("ftp"));
7386 }
7387 _ => panic!("expected InvalidUrl error"),
7388 }
7389 }
7390
7391 #[test]
7392 fn test_worker_script_loader_data_url_text() {
7393 let loader = WorkerScriptLoader::url(
7394 "data:text/javascript,self.postMessage('hello')".to_string(),
7395 WorkerScriptType::Classic,
7396 );
7397 let resolved = loader.resolve().unwrap();
7398 match resolved {
7399 WorkerScriptSource::Inline(script) => {
7400 assert_eq!(script, "self.postMessage('hello')");
7401 }
7402 WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
7403 }
7404 }
7405
7406 #[test]
7407 fn test_worker_script_loader_data_url_base64() {
7408 let loader = WorkerScriptLoader::url(
7410 "data:text/javascript;base64,dmFyIHggPSAxOw==".to_string(),
7411 WorkerScriptType::Classic,
7412 );
7413 let resolved = loader.resolve().unwrap();
7414 match resolved {
7415 WorkerScriptSource::Inline(script) => {
7416 assert_eq!(script, "var x = 1;");
7417 }
7418 WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
7419 }
7420 }
7421
7422 #[test]
7423 fn test_worker_script_loader_data_url_invalid_base64() {
7424 let loader = WorkerScriptLoader::url(
7425 "data:text/javascript;base64,!!!invalid!!!".to_string(),
7426 WorkerScriptType::Classic,
7427 );
7428 let result = loader.resolve();
7429 assert!(result.is_err());
7430 }
7431
7432 #[test]
7433 fn test_worker_script_loader_data_url_missing_comma() {
7434 let loader = WorkerScriptLoader::url(
7435 "data:text/javascript".to_string(),
7436 WorkerScriptType::Classic,
7437 );
7438 let result = loader.resolve();
7439 assert!(result.is_err());
7440 match result.unwrap_err() {
7441 WorkerScriptLoadError::InvalidUrl(msg) => {
7442 assert!(msg.contains("comma separator"));
7443 }
7444 _ => panic!("expected InvalidUrl error"),
7445 }
7446 }
7447
7448 #[test]
7449 fn test_worker_script_loader_blob_url_passthrough() {
7450 let loader = WorkerScriptLoader::url(
7451 "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string(),
7452 WorkerScriptType::Classic,
7453 );
7454 let resolved = loader.resolve().unwrap();
7455 assert_eq!(
7456 resolved,
7457 WorkerScriptSource::Url(
7458 "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string()
7459 )
7460 );
7461 }
7462
7463 #[test]
7464 fn test_worker_script_loader_from_source() {
7465 let loader = WorkerScriptLoader::from_source(
7466 WorkerScriptSource::Inline("code".to_string()),
7467 WorkerScriptType::Module,
7468 );
7469 assert_eq!(loader.script_type, WorkerScriptType::Module);
7470 assert!(loader.script_url().is_none());
7471 }
7472
7473 #[test]
7474 fn test_worker_script_loader_validate_mime_type_valid() {
7475 assert!(WorkerScriptLoader::validate_mime_type(
7476 "text/javascript",
7477 "https://example.com/worker.js"
7478 )
7479 .is_ok());
7480 assert!(WorkerScriptLoader::validate_mime_type(
7481 "application/javascript",
7482 "https://example.com/worker.js"
7483 )
7484 .is_ok());
7485 }
7486
7487 #[test]
7488 fn test_worker_script_loader_validate_mime_type_invalid() {
7489 let result =
7490 WorkerScriptLoader::validate_mime_type("text/html", "https://example.com/worker.js");
7491 assert!(result.is_err());
7492 match result.unwrap_err() {
7493 WorkerScriptLoadError::InvalidMimeType { received, url } => {
7494 assert_eq!(received, "text/html");
7495 assert_eq!(url, "https://example.com/worker.js");
7496 }
7497 _ => panic!("expected InvalidMimeType error"),
7498 }
7499 }
7500
7501 #[test]
7502 fn test_worker_script_load_state_transitions() {
7503 let mut state = WorkerScriptLoadState::Pending;
7504 assert!(state.is_loading());
7505 assert!(!state.is_ready());
7506 assert!(!state.is_failed());
7507
7508 state = WorkerScriptLoadState::Fetching;
7509 assert!(state.is_loading());
7510
7511 state = WorkerScriptLoadState::Validating;
7512 assert!(state.is_loading());
7513
7514 state = WorkerScriptLoadState::Decoding;
7515 assert!(state.is_loading());
7516
7517 state = WorkerScriptLoadState::Compiling;
7518 assert!(state.is_loading());
7519
7520 state = WorkerScriptLoadState::Ready;
7521 assert!(!state.is_loading());
7522 assert!(state.is_ready());
7523
7524 state = WorkerScriptLoadState::Failed(WorkerScriptLoadError::NetworkError(
7525 "timeout".to_string(),
7526 ));
7527 assert!(!state.is_loading());
7528 assert!(state.is_failed());
7529 }
7530
7531 #[test]
7532 fn test_webview_state_worker_script_load_state_registration() {
7533 let mut state = BaoWebViewState::default();
7534 let worker_id = WorkerId("worker1.js".to_string());
7535 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7536 assert_eq!(state.worker_script_load_state_count(), 1);
7537 assert!(state.worker_script_load_state(&worker_id).is_some());
7538 assert_eq!(
7539 state.worker_script_load_state(&worker_id).unwrap(),
7540 &WorkerScriptLoadState::Pending
7541 );
7542 }
7543
7544 #[test]
7545 fn test_webview_state_worker_script_load_state_update() {
7546 let mut state = BaoWebViewState::default();
7547 let worker_id = WorkerId("worker1.js".to_string());
7548 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7549 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
7550 assert_eq!(
7551 state.worker_script_load_state(&worker_id).unwrap(),
7552 &WorkerScriptLoadState::Fetching
7553 );
7554 }
7555
7556 #[test]
7557 fn test_webview_state_worker_script_load_state_remove() {
7558 let mut state = BaoWebViewState::default();
7559 let worker_id = WorkerId("worker1.js".to_string());
7560 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Ready);
7561 let removed = state.remove_worker_script_load_state(&worker_id);
7562 assert!(removed.is_some());
7563 assert_eq!(removed.unwrap(), WorkerScriptLoadState::Ready);
7564 assert_eq!(state.worker_script_load_state_count(), 0);
7565 }
7566
7567 #[test]
7568 fn test_webview_state_terminate_clears_script_load_states() {
7569 let mut state = BaoWebViewState::default();
7570 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7571 state.register_worker_script_load_state(
7572 WorkerId("worker1.js".to_string()),
7573 WorkerScriptLoadState::Fetching,
7574 );
7575 assert_eq!(state.worker_script_load_state_count(), 1);
7576 state.terminate_all_workers();
7577 assert_eq!(state.worker_script_load_state_count(), 0);
7578 }
7579
7580 #[test]
7581 fn test_webview_state_reap_terminated_worker_script_load_states() {
7582 let mut state = BaoWebViewState::default();
7583 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7584 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
7585 state.register_worker_script_load_state(
7586 WorkerId("worker1.js".to_string()),
7587 WorkerScriptLoadState::Ready,
7588 );
7589 state.register_worker_script_load_state(
7590 WorkerId("worker2.js".to_string()),
7591 WorkerScriptLoadState::Fetching,
7592 );
7593 state.active_workers[0].handle().terminate();
7595 state.active_workers[0].handle().mark_terminated();
7596 state.reap_terminated_workers();
7597 assert_eq!(state.worker_script_load_state_count(), 1);
7599 assert!(state
7600 .worker_script_load_state(&WorkerId("worker2.js".to_string()))
7601 .is_some());
7602 }
7603
7604 #[test]
7605 fn test_worker_script_loader_file_url() {
7606 let temp_dir = std::env::temp_dir();
7608 let temp_file = temp_dir.join("bao_test_worker_script.js");
7609 std::fs::write(&temp_file, "var x = 42;").unwrap();
7610
7611 let file_url = format!("file://{}", temp_file.display());
7612 let loader = WorkerScriptLoader::url(file_url, WorkerScriptType::Classic);
7613 let resolved = loader.resolve().unwrap();
7614 match resolved {
7615 WorkerScriptSource::Inline(script) => {
7616 assert_eq!(script, "var x = 42;");
7617 }
7618 WorkerScriptSource::Url(_) => panic!("expected inline source from file: URL"),
7619 }
7620
7621 let _ = std::fs::remove_file(&temp_file);
7623 }
7624
7625 #[test]
7626 fn test_worker_script_loader_file_url_not_found() {
7627 let loader = WorkerScriptLoader::url(
7628 "file:///nonexistent/path/worker.js".to_string(),
7629 WorkerScriptType::Classic,
7630 );
7631 let result = loader.resolve();
7632 assert!(result.is_err());
7633 match result.unwrap_err() {
7634 WorkerScriptLoadError::NetworkError(msg) => {
7635 assert!(msg.contains("Failed to read") || msg.contains("No such file"));
7636 }
7637 _ => panic!("expected NetworkError for missing file"),
7638 }
7639 }
7640
7641 #[test]
7642 fn test_worker_script_loader_full_pipeline_states() {
7643 let mut state = BaoWebViewState::default();
7645 let worker_id = WorkerId("https://example.com/worker.js".to_string());
7646
7647 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7649 assert!(state
7650 .worker_script_load_state(&worker_id)
7651 .unwrap()
7652 .is_loading());
7653
7654 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
7656 assert!(matches!(
7657 state.worker_script_load_state(&worker_id).unwrap(),
7658 WorkerScriptLoadState::Fetching
7659 ));
7660
7661 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Validating);
7663
7664 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Decoding);
7666
7667 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Compiling);
7669
7670 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Ready);
7672 assert!(state
7673 .worker_script_load_state(&worker_id)
7674 .unwrap()
7675 .is_ready());
7676 }
7677
7678 #[test]
7679 fn test_worker_script_loader_pipeline_failure() {
7680 let mut state = BaoWebViewState::default();
7681 let worker_id = WorkerId("https://example.com/bad-worker.js".to_string());
7682
7683 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7684
7685 state.update_worker_script_load_state(
7687 &worker_id,
7688 WorkerScriptLoadState::Failed(WorkerScriptLoadError::InvalidMimeType {
7689 received: "text/html".to_string(),
7690 url: "https://example.com/bad-worker.js".to_string(),
7691 }),
7692 );
7693 assert!(state
7694 .worker_script_load_state(&worker_id)
7695 .unwrap()
7696 .is_failed());
7697 }
7698
7699 #[test]
7703 fn test_worker_scope_config_from_stealth_profile_chrome() {
7704 let profile = bao_stealth::StealthProfile::chrome_default();
7705 let config = WorkerScopeConfig::from(&profile);
7706
7707 assert!(
7708 config.stealth_profile.is_some(),
7709 "stealth_profile must be Some"
7710 );
7711 assert_eq!(config.user_agent, profile.navigator.user_agent);
7712 assert_eq!(config.platform, profile.navigator.platform);
7713 assert_eq!(
7714 config.hardware_concurrency,
7715 profile.navigator.hardware_concurrency as usize
7716 );
7717 assert_eq!(config.language, profile.navigator.language);
7718 assert_eq!(config.languages, profile.navigator.languages);
7719 assert!(
7720 config.user_agent.contains("Chrome"),
7721 "Chrome profile UA must contain Chrome"
7722 );
7723 }
7724
7725 #[test]
7726 fn test_worker_scope_config_from_stealth_profile_firefox() {
7727 let profile = bao_stealth::StealthProfile::firefox_default();
7728 let config = WorkerScopeConfig::from(&profile);
7729
7730 assert!(
7731 config.stealth_profile.is_some(),
7732 "stealth_profile must be Some"
7733 );
7734 assert_eq!(config.user_agent, profile.navigator.user_agent);
7735 assert_eq!(config.platform, profile.navigator.platform);
7736 assert_eq!(
7737 config.hardware_concurrency,
7738 profile.navigator.hardware_concurrency as usize
7739 );
7740 assert_eq!(config.language, profile.navigator.language);
7741 assert_eq!(config.languages, profile.navigator.languages);
7742 assert!(
7743 config.user_agent.contains("Firefox"),
7744 "Firefox profile UA must contain Firefox"
7745 );
7746 }
7747
7748 #[test]
7749 fn test_shared_worker_scope_config_from_stealth_profile() {
7750 let profile = bao_stealth::StealthProfile::chrome_default();
7751 let config = SharedWorkerScopeConfig::from(&profile);
7752
7753 assert!(
7754 config.stealth_profile.is_some(),
7755 "stealth_profile must be Some"
7756 );
7757 assert_eq!(config.user_agent, profile.navigator.user_agent);
7758 assert_eq!(config.platform, profile.navigator.platform);
7759 assert_eq!(
7760 config.hardware_concurrency,
7761 profile.navigator.hardware_concurrency as usize
7762 );
7763 assert_eq!(config.language, profile.navigator.language);
7764 assert_eq!(config.languages, profile.navigator.languages);
7765 }
7766
7767 #[test]
7768 fn test_worker_scope_config_from_stealth_profile_carries_canvas_webgl_audio() {
7769 let profile = bao_stealth::StealthProfile::chrome_default();
7772 let config = WorkerScopeConfig::from(&profile);
7773 let worker_profile = config.stealth_profile.unwrap();
7774
7775 assert_eq!(
7776 worker_profile.canvas.seed(),
7777 profile.canvas.seed(),
7778 "Canvas seed must match"
7779 );
7780 assert!(
7781 (worker_profile.canvas.noise_amplitude() - profile.canvas.noise_amplitude()).abs()
7782 < f64::EPSILON,
7783 "Canvas amplitude must match"
7784 );
7785 assert_eq!(
7786 worker_profile.audio.seed(),
7787 profile.audio.seed(),
7788 "Audio seed must match"
7789 );
7790 assert_eq!(
7791 worker_profile.webgl.vendor, profile.webgl.vendor,
7792 "WebGL vendor must match"
7793 );
7794 assert_eq!(
7795 worker_profile.webgl.renderer, profile.webgl.renderer,
7796 "WebGL renderer must match"
7797 );
7798 }
7799
7800 #[test]
7801 fn test_worker_scope_config_from_different_profiles_produces_different_configs() {
7802 let chrome = bao_stealth::StealthProfile::chrome_default();
7804 let firefox = bao_stealth::StealthProfile::firefox_default();
7805 let chrome_config = WorkerScopeConfig::from(&chrome);
7806 let firefox_config = WorkerScopeConfig::from(&firefox);
7807
7808 assert_ne!(chrome_config.user_agent, firefox_config.user_agent);
7809 assert_ne!(
7810 chrome_config.stealth_profile.unwrap().canvas.seed(),
7811 firefox_config.stealth_profile.unwrap().canvas.seed()
7812 );
7813 }
7814
7815 #[test]
7819 fn test_shared_worker_global_scope_state_new() {
7820 let id = SharedWorkerId {
7821 script_url: "sw.js".to_string(),
7822 name: "myworker".to_string(),
7823 };
7824 let config = SharedWorkerScopeConfig {
7825 stealth_profile: None,
7826 user_agent: "Bao/1.0".to_string(),
7827 platform: "Linux".to_string(),
7828 hardware_concurrency: 8,
7829 language: "en-US".to_string(),
7830 languages: vec!["en-US".to_string()],
7831 };
7832 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
7833 assert_eq!(scope.shared_worker_id, id);
7834 assert!(!scope.has_onconnect);
7835 assert_eq!(scope.connect_count, 0);
7836 assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
7837 }
7838
7839 #[test]
7840 fn test_shared_worker_global_scope_state_location() {
7841 let id = SharedWorkerId {
7842 script_url: "https://example.com/sw.js".to_string(),
7843 name: String::new(),
7844 };
7845 let config = SharedWorkerScopeConfig::default();
7846 let scope = SharedWorkerGlobalScopeState::new(id, &config);
7847 let loc = scope.location().unwrap();
7848 assert_eq!(loc.hostname, "example.com");
7849 assert_eq!(loc.pathname, "/sw.js");
7850 }
7851
7852 #[test]
7853 fn test_shared_worker_global_scope_state_navigator() {
7854 let id = SharedWorkerId {
7855 script_url: "sw.js".to_string(),
7856 name: "test".to_string(),
7857 };
7858 let config = SharedWorkerScopeConfig {
7859 stealth_profile: None,
7860 user_agent: "Bao/2.0".to_string(),
7861 platform: "MacOS".to_string(),
7862 hardware_concurrency: 4,
7863 language: "ja".to_string(),
7864 languages: vec!["ja".to_string()],
7865 };
7866 let scope = SharedWorkerGlobalScopeState::new(id, &config);
7867 let nav = scope.navigator();
7868 assert_eq!(nav.user_agent, "Bao/2.0");
7869 assert_eq!(nav.hardware_concurrency, 4);
7870 }
7871
7872 #[test]
7873 fn test_shared_worker_global_scope_state_onconnect() {
7874 let id = SharedWorkerId {
7875 script_url: "sw.js".to_string(),
7876 name: String::new(),
7877 };
7878 let config = SharedWorkerScopeConfig::default();
7879 let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
7880 assert!(!scope.has_onconnect);
7881 scope.set_onconnect();
7882 assert!(scope.has_onconnect);
7883 }
7884
7885 #[test]
7886 fn test_shared_worker_global_scope_state_connect_count() {
7887 let id = SharedWorkerId {
7888 script_url: "sw.js".to_string(),
7889 name: String::new(),
7890 };
7891 let config = SharedWorkerScopeConfig::default();
7892 let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
7893 assert_eq!(scope.connect_count, 0);
7894 scope.page_connected();
7895 assert_eq!(scope.connect_count, 1);
7896 scope.page_connected();
7897 assert_eq!(scope.connect_count, 2);
7898 }
7899
7900 #[test]
7904 fn test_shared_worker_port_channel_creation() {
7905 let id = SharedWorkerId {
7906 script_url: "sw.js".to_string(),
7907 name: "test".to_string(),
7908 };
7909 let (port, endpoints) = SharedWorkerPortChannel::new(id.clone());
7910 assert_eq!(port.shared_worker_id, id);
7911 assert_eq!(endpoints.shared_worker_id, id);
7912 assert!(endpoints.page_to_worker_rx.is_some());
7913 assert!(endpoints.worker_to_page_tx.is_some());
7914 }
7915
7916 #[test]
7917 fn test_shared_worker_port_channel_page_to_worker() {
7918 let id = SharedWorkerId {
7919 script_url: "sw.js".to_string(),
7920 name: String::new(),
7921 };
7922 let (port, endpoints) = SharedWorkerPortChannel::new(id);
7923 let payload = StructuredClonePayload {
7924 data: vec![1, 2, 3],
7925 transferable_count: 0,
7926 };
7927 port.post_message_to_worker(payload).unwrap();
7928 let rx = endpoints.page_to_worker_rx.unwrap();
7929 let received = rx.try_recv().unwrap();
7930 assert_eq!(received.data, vec![1, 2, 3]);
7931 }
7932
7933 #[test]
7934 fn test_shared_worker_port_channel_worker_to_page() {
7935 let id = SharedWorkerId {
7936 script_url: "sw.js".to_string(),
7937 name: String::new(),
7938 };
7939 let (port, endpoints) = SharedWorkerPortChannel::new(id);
7940 let msg = WorkerStructuredMessage::with_payload(
7941 WorkerId("sw.js".to_string()),
7942 WorkerMessageDirection::WorkerToPage,
7943 vec![4, 5, 6],
7944 0,
7945 );
7946 let tx = endpoints.worker_to_page_tx.unwrap();
7947 tx.send(msg).unwrap();
7948 let result = port.try_recv_from_worker().unwrap();
7949 assert!(result.is_some());
7950 assert_eq!(result.unwrap().payload.unwrap().data, vec![4, 5, 6]);
7951 }
7952
7953 #[test]
7954 fn test_shared_worker_port_channel_drain() {
7955 let id = SharedWorkerId {
7956 script_url: "sw.js".to_string(),
7957 name: String::new(),
7958 };
7959 let (port, endpoints) = SharedWorkerPortChannel::new(id);
7960 let tx = endpoints.worker_to_page_tx.unwrap();
7961 for i in 0..3 {
7962 let msg = WorkerStructuredMessage::with_payload(
7963 WorkerId("sw.js".to_string()),
7964 WorkerMessageDirection::WorkerToPage,
7965 vec![i],
7966 0,
7967 );
7968 tx.send(msg).unwrap();
7969 }
7970 let result = port.drain_worker_messages();
7971 assert_eq!(result.messages.len(), 3);
7972 assert!(!result.disconnected);
7973 let empty = port.drain_worker_messages();
7974 assert!(empty.messages.is_empty());
7975 assert!(!empty.disconnected);
7976 }
7977
7978 #[test]
7982 fn test_shared_worker_channel_bridge_new() {
7983 let id = SharedWorkerId {
7984 script_url: "sw.js".to_string(),
7985 name: "test".to_string(),
7986 };
7987 let bridge = SharedWorkerChannelBridge::new(id.clone());
7988 assert_eq!(bridge.shared_worker_id, id);
7989 assert_eq!(bridge.port_count(), 0);
7990 }
7991
7992 #[test]
7993 fn test_shared_worker_channel_bridge_add_port() {
7994 let id = SharedWorkerId {
7995 script_url: "sw.js".to_string(),
7996 name: "test".to_string(),
7997 };
7998 let mut bridge = SharedWorkerChannelBridge::new(id.clone());
7999 let endpoints = bridge.add_port();
8000 assert_eq!(bridge.port_count(), 1);
8001 assert_eq!(endpoints.shared_worker_id, id);
8002 assert!(endpoints.page_to_worker_rx.is_some());
8003 assert!(endpoints.worker_to_page_tx.is_some());
8004 }
8005
8006 #[test]
8007 fn test_shared_worker_channel_bridge_multiple_ports() {
8008 let id = SharedWorkerId {
8009 script_url: "sw.js".to_string(),
8010 name: "test".to_string(),
8011 };
8012 let mut bridge = SharedWorkerChannelBridge::new(id);
8013 bridge.add_port(); bridge.add_port(); bridge.add_port(); assert_eq!(bridge.port_count(), 3);
8017 }
8018
8019 #[test]
8020 fn test_shared_worker_channel_bridge_drain_all() {
8021 let id = SharedWorkerId {
8022 script_url: "sw.js".to_string(),
8023 name: "test".to_string(),
8024 };
8025 let mut bridge = SharedWorkerChannelBridge::new(id);
8026 let endpoints1 = bridge.add_port();
8027 let endpoints2 = bridge.add_port();
8028 let tx1 = endpoints1.worker_to_page_tx.unwrap();
8030 let tx2 = endpoints2.worker_to_page_tx.unwrap();
8031 tx1.send(WorkerStructuredMessage::metadata_only(
8032 WorkerId("sw.js".to_string()),
8033 WorkerMessageDirection::WorkerToPage,
8034 ))
8035 .unwrap();
8036 tx2.send(WorkerStructuredMessage::metadata_only(
8037 WorkerId("sw.js".to_string()),
8038 WorkerMessageDirection::WorkerToPage,
8039 ))
8040 .unwrap();
8041 let (messages, disconnected) = bridge.drain_all_worker_messages();
8042 assert_eq!(messages.len(), 2);
8043 assert!(disconnected.is_empty());
8044 }
8045
8046 #[test]
8047 fn test_shared_worker_channel_bridge_post_to_worker() {
8048 let id = SharedWorkerId {
8049 script_url: "sw.js".to_string(),
8050 name: "test".to_string(),
8051 };
8052 let mut bridge = SharedWorkerChannelBridge::new(id);
8053 let endpoints = bridge.add_port();
8054 let payload = StructuredClonePayload {
8055 data: vec![42],
8056 transferable_count: 0,
8057 };
8058 bridge.post_to_worker_from_port(0, payload).unwrap();
8059 let rx = endpoints.page_to_worker_rx.unwrap();
8060 let received = rx.try_recv().unwrap();
8061 assert_eq!(received.data, vec![42]);
8062 }
8063
8064 #[test]
8065 fn test_shared_worker_channel_bridge_post_invalid_port() {
8066 let id = SharedWorkerId {
8067 script_url: "sw.js".to_string(),
8068 name: "test".to_string(),
8069 };
8070 let mut bridge = SharedWorkerChannelBridge::new(id);
8071 bridge.add_port();
8072 let payload = StructuredClonePayload {
8073 data: vec![],
8074 transferable_count: 0,
8075 };
8076 let result = bridge.post_to_worker_from_port(99, payload);
8077 assert!(result.is_err());
8078 }
8079
8080 #[test]
8084 fn test_webview_state_shared_worker_channel_registration() {
8085 let mut state = BaoWebViewState::default();
8086 let id = SharedWorkerId {
8087 script_url: "sw.js".to_string(),
8088 name: "test".to_string(),
8089 };
8090 let bridge = SharedWorkerChannelBridge::new(id.clone());
8091 state.register_shared_worker_channel(bridge);
8092 assert!(state.shared_worker_channel(&id).is_some());
8093 assert_eq!(state.shared_worker_channel_count(), 0); }
8095
8096 #[test]
8097 fn test_webview_state_create_shared_worker_channel() {
8098 let mut state = BaoWebViewState::default();
8099 let id = SharedWorkerId {
8100 script_url: "sw.js".to_string(),
8101 name: "test".to_string(),
8102 };
8103 state.create_shared_worker_channel(id.clone());
8104 assert!(state.shared_worker_channel(&id).is_some());
8105 }
8106
8107 #[test]
8108 fn test_webview_state_add_shared_worker_port() {
8109 let mut state = BaoWebViewState::default();
8110 let id = SharedWorkerId {
8111 script_url: "sw.js".to_string(),
8112 name: "test".to_string(),
8113 };
8114 let endpoints = state.add_shared_worker_port(id.clone());
8115 assert_eq!(state.shared_worker_channel_count(), 1);
8116 assert_eq!(endpoints.shared_worker_id, id);
8117 assert!(endpoints.page_to_worker_rx.is_some());
8118 assert!(endpoints.worker_to_page_tx.is_some());
8119 }
8120
8121 #[test]
8122 fn test_webview_state_add_shared_worker_port_multiple() {
8123 let mut state = BaoWebViewState::default();
8124 let id = SharedWorkerId {
8125 script_url: "sw.js".to_string(),
8126 name: "test".to_string(),
8127 };
8128 state.add_shared_worker_port(id.clone());
8129 state.add_shared_worker_port(id.clone());
8130 assert_eq!(state.shared_worker_channel_count(), 2); }
8132
8133 #[test]
8134 fn test_webview_state_drain_all_shared_worker_messages() {
8135 let mut state = BaoWebViewState::default();
8136 let id = SharedWorkerId {
8137 script_url: "sw.js".to_string(),
8138 name: "test".to_string(),
8139 };
8140 let endpoints = state.add_shared_worker_port(id);
8141 let tx = endpoints.worker_to_page_tx.unwrap();
8142 tx.send(WorkerStructuredMessage::metadata_only(
8143 WorkerId("sw.js".to_string()),
8144 WorkerMessageDirection::WorkerToPage,
8145 ))
8146 .unwrap();
8147 let (messages, disconnected) = state.drain_all_shared_worker_messages();
8148 assert_eq!(messages.len(), 1);
8149 assert!(disconnected.is_empty());
8150 }
8151
8152 #[test]
8153 fn test_webview_state_drain_and_forward_shared_worker_messages() {
8154 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
8155 let mut state = BaoWebViewState {
8156 event_tx: Some(tx),
8157 ..Default::default()
8158 };
8159 let id = SharedWorkerId {
8160 script_url: "sw.js".to_string(),
8161 name: "test".to_string(),
8162 };
8163 let endpoints = state.add_shared_worker_port(id);
8164 let worker_tx = endpoints.worker_to_page_tx.unwrap();
8165 worker_tx
8166 .send(WorkerStructuredMessage::metadata_only(
8167 WorkerId("sw.js".to_string()),
8168 WorkerMessageDirection::WorkerToPage,
8169 ))
8170 .unwrap();
8171 state.drain_and_forward_shared_worker_messages();
8172 let event = rx.try_recv().unwrap();
8173 match event {
8174 ServoEvent::Console { text, .. } => {
8175 assert!(text.contains("worker→page"));
8176 }
8177 _ => panic!("expected Console event for shared worker message"),
8178 }
8179 }
8180
8181 #[test]
8182 fn test_webview_state_disconnect_shared_worker_clears_channels() {
8183 let mut state = BaoWebViewState::default();
8184 let id = SharedWorkerId {
8185 script_url: "sw.js".to_string(),
8186 name: "test".to_string(),
8187 };
8188 state.track_shared_worker_port(SharedWorkerPortRef::new(SharedWorkerHandle::new(
8189 "sw.js".to_string(),
8190 "test".to_string(),
8191 )));
8192 state.add_shared_worker_port(id.clone());
8193 assert_eq!(state.shared_worker_port_count(), 1);
8194 assert_eq!(state.shared_worker_channel_count(), 1);
8195 state.disconnect_shared_worker_ports();
8196 assert_eq!(state.shared_worker_port_count(), 0);
8197 assert_eq!(state.shared_worker_channel_count(), 0);
8198 }
8199
8200 #[test]
8201 fn test_webview_state_shared_worker_scope_registration() {
8202 let mut state = BaoWebViewState::default();
8203 let id = SharedWorkerId {
8204 script_url: "sw.js".to_string(),
8205 name: "test".to_string(),
8206 };
8207 let config = SharedWorkerScopeConfig::default();
8208 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8209 state.register_shared_worker_scope(id.clone(), scope);
8210 assert_eq!(state.shared_worker_scope_count(), 1);
8211 assert!(state.shared_worker_scope(&id).is_some());
8212 }
8213
8214 #[test]
8215 fn test_webview_state_shared_worker_scope_get_mut() {
8216 let mut state = BaoWebViewState::default();
8217 let id = SharedWorkerId {
8218 script_url: "sw.js".to_string(),
8219 name: "test".to_string(),
8220 };
8221 let config = SharedWorkerScopeConfig::default();
8222 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8223 state.register_shared_worker_scope(id.clone(), scope);
8224 state.shared_worker_scope_mut(&id).unwrap().set_onconnect();
8225 assert!(state.shared_worker_scope(&id).unwrap().has_onconnect);
8226 }
8227
8228 #[test]
8229 fn test_webview_state_shared_worker_scope_remove() {
8230 let mut state = BaoWebViewState::default();
8231 let id = SharedWorkerId {
8232 script_url: "sw.js".to_string(),
8233 name: "test".to_string(),
8234 };
8235 let config = SharedWorkerScopeConfig::default();
8236 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8237 state.register_shared_worker_scope(id.clone(), scope);
8238 let removed = state.remove_shared_worker_scope(&id);
8239 assert!(removed.is_some());
8240 assert_eq!(state.shared_worker_scope_count(), 0);
8241 }
8242
8243 #[test]
8244 fn test_webview_state_shared_worker_scopes_snapshot() {
8245 let mut state = BaoWebViewState::default();
8246 let id1 = SharedWorkerId {
8247 script_url: "sw1.js".to_string(),
8248 name: "a".to_string(),
8249 };
8250 let id2 = SharedWorkerId {
8251 script_url: "sw2.js".to_string(),
8252 name: "b".to_string(),
8253 };
8254 let config = SharedWorkerScopeConfig::default();
8255 state.register_shared_worker_scope(
8256 id1,
8257 SharedWorkerGlobalScopeState::new(
8258 SharedWorkerId {
8259 script_url: "sw1.js".to_string(),
8260 name: "a".to_string(),
8261 },
8262 &config,
8263 ),
8264 );
8265 state.register_shared_worker_scope(
8266 id2,
8267 SharedWorkerGlobalScopeState::new(
8268 SharedWorkerId {
8269 script_url: "sw2.js".to_string(),
8270 name: "b".to_string(),
8271 },
8272 &config,
8273 ),
8274 );
8275 let scopes = state.shared_worker_scopes();
8276 assert_eq!(scopes.len(), 2);
8277 }
8278
8279 #[test]
8280 fn test_webview_state_disconnect_shared_worker_clears_scopes() {
8281 let mut state = BaoWebViewState::default();
8282 let id = SharedWorkerId {
8283 script_url: "sw.js".to_string(),
8284 name: "test".to_string(),
8285 };
8286 let config = SharedWorkerScopeConfig::default();
8287 state.register_shared_worker_scope(
8288 id,
8289 SharedWorkerGlobalScopeState::new(
8290 SharedWorkerId {
8291 script_url: "sw.js".to_string(),
8292 name: "test".to_string(),
8293 },
8294 &config,
8295 ),
8296 );
8297 assert_eq!(state.shared_worker_scope_count(), 1);
8298 state.disconnect_shared_worker_ports();
8299 assert_eq!(state.shared_worker_scope_count(), 0);
8300 }
8301
8302 #[test]
8303 fn test_webview_state_set_shared_worker_scope_config() {
8304 let mut state = BaoWebViewState::default();
8305 let id = SharedWorkerId {
8306 script_url: "sw.js".to_string(),
8307 name: "test".to_string(),
8308 };
8309 let config = SharedWorkerScopeConfig::default();
8310 state.register_shared_worker_scope(
8311 id.clone(),
8312 SharedWorkerGlobalScopeState::new(id.clone(), &config),
8313 );
8314 assert!(state
8315 .shared_worker_scope(&id)
8316 .unwrap()
8317 .navigator()
8318 .user_agent
8319 .is_empty());
8320 let new_config = SharedWorkerScopeConfig {
8321 stealth_profile: None,
8322 user_agent: "Bao/1.0".to_string(),
8323 platform: "Linux".to_string(),
8324 hardware_concurrency: 8,
8325 language: "en-US".to_string(),
8326 languages: vec!["en-US".to_string()],
8327 };
8328 state.set_shared_worker_scope_config(&id, &new_config);
8329 assert_eq!(
8330 state
8331 .shared_worker_scope(&id)
8332 .unwrap()
8333 .navigator()
8334 .user_agent,
8335 "Bao/1.0"
8336 );
8337 }
8338
8339 #[test]
8343 fn test_delegate_route_shared_worker_new() {
8344 let delegate = BaoServoDelegate::new();
8345 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8346 let (returned, is_new) = delegate.route_shared_worker(handle);
8347 assert!(is_new);
8348 assert_eq!(returned.script_url, "sw.js");
8349 assert_eq!(delegate.shared_worker_count(), 1);
8350 }
8351
8352 #[test]
8353 fn test_delegate_route_shared_worker_existing() {
8354 let delegate = BaoServoDelegate::new();
8355 let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8356 let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8357 delegate.route_shared_worker(handle1);
8358 let (_, is_new) = delegate.route_shared_worker(handle2);
8359 assert!(
8360 !is_new,
8361 "same (url, name) should return existing, not create new"
8362 );
8363 assert_eq!(delegate.shared_worker_count(), 1);
8364 }
8365
8366 #[test]
8367 fn test_delegate_get_or_create_shared_worker_new() {
8368 let delegate = BaoServoDelegate::new();
8369 let (handle, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
8370 assert!(is_new);
8371 assert_eq!(handle.script_url, "sw.js");
8372 assert_eq!(handle.name, "myname");
8373 }
8374
8375 #[test]
8376 fn test_delegate_get_or_create_shared_worker_existing() {
8377 let delegate = BaoServoDelegate::new();
8378 delegate.get_or_create_shared_worker("sw.js", "myname");
8379 let (_, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
8380 assert!(!is_new);
8381 assert_eq!(delegate.shared_worker_count(), 1);
8382 }
8383
8384 #[test]
8385 fn test_delegate_unregister_shared_worker() {
8386 let delegate = BaoServoDelegate::new();
8387 let id = SharedWorkerId {
8388 script_url: "sw.js".to_string(),
8389 name: "myname".to_string(),
8390 };
8391 delegate.get_or_create_shared_worker("sw.js", "myname");
8392 assert_eq!(delegate.shared_worker_count(), 1);
8393 let removed = delegate.unregister_shared_worker(&id);
8394 assert!(removed);
8395 assert_eq!(delegate.shared_worker_count(), 0);
8396 }
8397
8398 #[test]
8399 fn test_delegate_unregister_nonexistent_shared_worker() {
8400 let delegate = BaoServoDelegate::new();
8401 let id = SharedWorkerId {
8402 script_url: "sw.js".to_string(),
8403 name: "nonexistent".to_string(),
8404 };
8405 let removed = delegate.unregister_shared_worker(&id);
8406 assert!(!removed);
8407 }
8408
8409 #[test]
8410 fn test_delegate_all_shared_workers() {
8411 let delegate = BaoServoDelegate::new();
8412 delegate.get_or_create_shared_worker("sw1.js", "a");
8413 delegate.get_or_create_shared_worker("sw2.js", "b");
8414 let all = delegate.all_shared_workers();
8415 assert_eq!(all.len(), 2);
8416 }
8417
8418 #[test]
8419 fn test_shared_worker_cross_page_routing_full_lifecycle() {
8420 let delegate = BaoServoDelegate::new();
8423
8424 let (handle, is_new) = delegate.route_shared_worker(SharedWorkerHandle::new(
8426 "sw.js".to_string(),
8427 "shared".to_string(),
8428 ));
8429 assert!(is_new);
8430 assert_eq!(handle.connected_page_count(), 0);
8431
8432 let mut state1 = BaoWebViewState::default();
8434 let id = SharedWorkerId {
8435 script_url: "sw.js".to_string(),
8436 name: "shared".to_string(),
8437 };
8438 let config = SharedWorkerScopeConfig::default();
8439 state1.register_shared_worker_scope(
8440 id.clone(),
8441 SharedWorkerGlobalScopeState::new(id.clone(), &config),
8442 );
8443 state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
8444 assert_eq!(handle.connected_page_count(), 1);
8446 let endpoints1 = state1.add_shared_worker_port(id.clone());
8448
8449 let mut state2 = BaoWebViewState::default();
8451 state2.register_shared_worker_scope(
8452 id.clone(),
8453 SharedWorkerGlobalScopeState::new(id.clone(), &config),
8454 );
8455 state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
8456 let endpoints2 = state2.add_shared_worker_port(id.clone());
8457 assert_eq!(handle.connected_page_count(), 2);
8458
8459 let payload1 = StructuredClonePayload {
8461 data: vec![1],
8462 transferable_count: 0,
8463 };
8464 state1
8465 .post_to_worker_via_shared_port(&id, 0, payload1)
8466 .unwrap();
8467 let payload2 = StructuredClonePayload {
8468 data: vec![2],
8469 transferable_count: 0,
8470 };
8471 state2
8472 .post_to_worker_via_shared_port(&id, 0, payload2)
8473 .unwrap();
8474
8475 let rx1 = endpoints1.page_to_worker_rx.unwrap();
8477 let rx2 = endpoints2.page_to_worker_rx.unwrap();
8478 assert_eq!(rx1.try_recv().unwrap().data, vec![1]);
8479 assert_eq!(rx2.try_recv().unwrap().data, vec![2]);
8480
8481 let tx1 = endpoints1.worker_to_page_tx.unwrap();
8483 let tx2 = endpoints2.worker_to_page_tx.unwrap();
8484 tx1.send(WorkerStructuredMessage::metadata_only(
8485 WorkerId("sw.js".to_string()),
8486 WorkerMessageDirection::WorkerToPage,
8487 ))
8488 .unwrap();
8489 tx2.send(WorkerStructuredMessage::metadata_only(
8490 WorkerId("sw.js".to_string()),
8491 WorkerMessageDirection::WorkerToPage,
8492 ))
8493 .unwrap();
8494
8495 let (msgs1, disc1) = state1.drain_all_shared_worker_messages();
8497 assert_eq!(msgs1.len(), 1);
8498 assert!(disc1.is_empty());
8499 let (msgs2, disc2) = state2.drain_all_shared_worker_messages();
8501 assert_eq!(msgs2.len(), 1);
8502 assert!(disc2.is_empty());
8503
8504 state1.disconnect_shared_worker_ports();
8506 assert_eq!(handle.connected_page_count(), 1);
8508 assert!(!handle.is_closing());
8509
8510 assert_eq!(state2.shared_worker_port_count(), 1);
8512
8513 handle.close();
8515 handle.mark_terminated();
8516 assert!(handle.is_closing());
8517 assert!(handle.is_terminated());
8518
8519 state2.disconnect_shared_worker_ports();
8522 assert_eq!(handle.connected_page_count(), 0);
8523 delegate.reap_terminated_shared_workers();
8524 assert_eq!(delegate.shared_worker_count(), 0);
8525 }
8526
8527 #[test]
8532 fn test_service_worker_registration_id_equality() {
8533 let id1 = ServiceWorkerRegistrationId {
8534 script_url: "sw.js".to_string(),
8535 scope: "/".to_string(),
8536 };
8537 let id2 = ServiceWorkerRegistrationId {
8538 script_url: "sw.js".to_string(),
8539 scope: "/".to_string(),
8540 };
8541 let id3 = ServiceWorkerRegistrationId {
8542 script_url: "sw.js".to_string(),
8543 scope: "/app/".to_string(),
8544 };
8545 assert_eq!(id1, id2);
8546 assert_ne!(id1, id3);
8547 }
8548
8549 #[test]
8550 fn test_service_worker_handle_lifecycle() {
8551 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8552 assert!(!handle.is_closing());
8553 assert!(!handle.is_terminated());
8554 assert_eq!(
8555 handle.registration_state(),
8556 ServiceWorkerRegistrationState::Installing
8557 );
8558 assert!(!handle.is_intercepting_fetch());
8559 }
8560
8561 #[test]
8562 fn test_service_worker_handle_state_transitions() {
8563 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8564 handle.transition_state(ServiceWorkerRegistrationState::Installed);
8566 assert_eq!(
8567 handle.registration_state(),
8568 ServiceWorkerRegistrationState::Installed
8569 );
8570
8571 handle.transition_state(ServiceWorkerRegistrationState::Activating);
8573 assert_eq!(
8574 handle.registration_state(),
8575 ServiceWorkerRegistrationState::Activating
8576 );
8577
8578 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8580 handle.enable_fetch_interception();
8581 assert_eq!(
8582 handle.registration_state(),
8583 ServiceWorkerRegistrationState::Activated
8584 );
8585 assert!(handle.is_intercepting_fetch());
8586 assert_eq!(
8587 handle.fetch_intercept_mode(),
8588 ServiceWorkerFetchInterceptMode::Intercepting
8589 );
8590 }
8591
8592 #[test]
8593 fn test_service_worker_handle_terminate_disables_interception() {
8594 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8595 handle.enable_fetch_interception();
8596 assert!(handle.is_intercepting_fetch());
8597
8598 handle.terminate();
8600 assert!(handle.is_closing());
8601 assert!(!handle.is_intercepting_fetch());
8602 assert_eq!(
8603 handle.fetch_intercept_mode(),
8604 ServiceWorkerFetchInterceptMode::None
8605 );
8606 }
8607
8608 #[test]
8609 fn test_service_worker_scope_config_from_stealth_profile() {
8610 let profile = bao_stealth::StealthProfile::chrome_default();
8611 let config = ServiceWorkerScopeConfig::from(&profile);
8612 assert!(config.stealth_profile.is_some());
8613 assert_eq!(config.user_agent, profile.navigator.user_agent);
8614 assert_eq!(config.platform, profile.navigator.platform);
8615 assert_eq!(
8616 config.hardware_concurrency,
8617 profile.navigator.hardware_concurrency as usize
8618 );
8619 assert_eq!(config.language, profile.navigator.language);
8620 }
8621
8622 #[test]
8623 fn test_service_worker_global_scope_state() {
8624 let reg_id = ServiceWorkerRegistrationId {
8625 script_url: "sw.js".to_string(),
8626 scope: "/app/".to_string(),
8627 };
8628 let config = ServiceWorkerScopeConfig::default();
8629 let scope = ServiceWorkerGlobalScopeState::new(reg_id.clone(), &config);
8630
8631 assert!(!scope.has_fetch_handler);
8632 assert!(!scope.has_activate_handler);
8633 assert!(!scope.has_install_handler);
8634 assert!(!scope.has_message_handler);
8635 assert_eq!(scope.scope_url, "/app/");
8636 assert!(scope.is_url_in_scope("/app/page1"));
8637 assert!(scope.is_url_in_scope("/app/sub/page2"));
8638 assert!(!scope.is_url_in_scope("/other/page"));
8639 }
8640
8641 #[test]
8642 fn test_service_worker_global_scope_fetch_handler() {
8643 let reg_id = ServiceWorkerRegistrationId {
8644 script_url: "sw.js".to_string(),
8645 scope: "/".to_string(),
8646 };
8647 let config = ServiceWorkerScopeConfig::default();
8648 let mut scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8649
8650 scope.set_fetch_handler();
8651 assert!(scope.has_fetch_handler);
8652 assert!(scope.is_url_in_scope("/anything"));
8653 }
8654
8655 #[test]
8656 fn test_webview_state_service_worker_control() {
8657 let mut state = BaoWebViewState::default();
8658 assert!(!state.is_controlled_by_service_worker());
8659
8660 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8661 state.set_controlling_service_worker(handle);
8662 assert!(state.is_controlled_by_service_worker());
8663
8664 state.clear_controlling_service_worker();
8665 assert!(!state.is_controlled_by_service_worker());
8666 }
8667
8668 #[test]
8669 fn test_webview_state_service_worker_scope_matching() {
8670 let mut state = BaoWebViewState::default();
8671 assert!(!state.is_url_in_service_worker_scope("/app/page1"));
8672
8673 let reg_id = ServiceWorkerRegistrationId {
8674 script_url: "sw.js".to_string(),
8675 scope: "/app/".to_string(),
8676 };
8677 let config = ServiceWorkerScopeConfig::default();
8678 let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8679 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/app/".to_string(), None);
8680 state.set_controlling_service_worker(handle);
8681 state.register_service_worker_scope(scope);
8682
8683 assert!(state.is_url_in_service_worker_scope("/app/page1"));
8684 assert!(state.is_url_in_service_worker_scope("/app/sub/page2"));
8685 assert!(!state.is_url_in_service_worker_scope("/other/page"));
8686 }
8687
8688 #[test]
8689 fn test_delegate_service_worker_registration() {
8690 let delegate = BaoServoDelegate::new();
8691 assert_eq!(delegate.service_worker_count(), 0);
8692
8693 let (handle, is_new) = delegate.get_or_create_service_worker("sw.js", "/", None);
8694 assert!(is_new);
8695 assert_eq!(delegate.service_worker_count(), 1);
8696
8697 let (handle2, is_new2) = delegate.get_or_create_service_worker("sw.js", "/", None);
8699 assert!(!is_new2);
8700 assert_eq!(delegate.service_worker_count(), 1);
8701 }
8702
8703 #[test]
8704 fn test_delegate_find_service_worker_for_url() {
8705 let delegate = BaoServoDelegate::new();
8706
8707 let handle = delegate
8709 .get_or_create_service_worker("sw.js", "/app/", None)
8710 .0;
8711
8712 assert!(delegate.find_service_worker_for_url("/app/page1").is_none());
8714
8715 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8717 handle.enable_fetch_interception();
8718
8719 let found = delegate.find_service_worker_for_url("/app/page1");
8721 assert!(found.is_some());
8722 assert_eq!(found.unwrap().script_url, "sw.js");
8723
8724 assert!(delegate
8726 .find_service_worker_for_url("/other/page")
8727 .is_none());
8728 }
8729
8730 #[test]
8731 fn test_delegate_service_worker_unregistration() {
8732 let delegate = BaoServoDelegate::new();
8733 let (handle, _) = delegate.get_or_create_service_worker("sw.js", "/", None);
8734 assert_eq!(delegate.service_worker_count(), 1);
8735
8736 let id = handle.id();
8737 assert!(delegate.unregister_service_worker(&id));
8738 assert_eq!(delegate.service_worker_count(), 0);
8739
8740 assert!(!delegate.unregister_service_worker(&id));
8742 }
8743
8744 #[test]
8745 fn test_delegate_service_worker_stealth_consistency_no_violations() {
8746 let delegate = BaoServoDelegate::new();
8747 let profile = bao_stealth::StealthProfile::chrome_default();
8748
8749 let handle =
8751 ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), Some(profile.clone()));
8752 delegate.register_service_worker(handle);
8753 let all = delegate.all_service_workers();
8755 all[0].transition_state(ServiceWorkerRegistrationState::Activated);
8756 all[0].enable_fetch_interception();
8757
8758 let violations = delegate.verify_service_worker_stealth_consistency(&profile);
8759 assert!(violations.is_empty());
8760 }
8761
8762 #[test]
8763 fn test_delegate_service_worker_stealth_consistency_violation_no_profile() {
8764 let delegate = BaoServoDelegate::new();
8765 let profile = bao_stealth::StealthProfile::chrome_default();
8766
8767 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8769 delegate.register_service_worker(handle);
8770 let all = delegate.all_service_workers();
8771 all[0].transition_state(ServiceWorkerRegistrationState::Activated);
8772 all[0].enable_fetch_interception();
8773
8774 let violations = delegate.verify_service_worker_stealth_consistency(&profile);
8775 assert_eq!(violations.len(), 1);
8776 }
8777
8778 #[test]
8779 fn test_service_worker_persistent_lifecycle_across_page_navigation() {
8780 let delegate = BaoServoDelegate::new();
8784 let profile = bao_stealth::StealthProfile::chrome_default();
8785
8786 let (handle, is_new) =
8788 delegate.get_or_create_service_worker("sw.js", "/", Some(profile.clone()));
8789 assert!(is_new);
8790 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8791 handle.enable_fetch_interception();
8792
8793 let mut page_state = BaoWebViewState::default();
8795 page_state.set_controlling_service_worker(handle.clone());
8796 let reg_id = ServiceWorkerRegistrationId {
8797 script_url: "sw.js".to_string(),
8798 scope: "/".to_string(),
8799 };
8800 let config = ServiceWorkerScopeConfig::from(&profile);
8801 page_state
8802 .register_service_worker_scope(ServiceWorkerGlobalScopeState::new(reg_id, &config));
8803 assert!(page_state.is_controlled_by_service_worker());
8804
8805 page_state.clear_controlling_service_worker();
8807 assert!(!page_state.is_controlled_by_service_worker());
8808
8809 assert_eq!(delegate.service_worker_count(), 1);
8811 assert!(delegate.find_service_worker_for_url("/page2").is_some());
8812
8813 let mut page2_state = BaoWebViewState::default();
8815 page2_state.set_controlling_service_worker(handle.clone());
8816 assert!(page2_state.is_controlled_by_service_worker());
8817 }
8818
8819 #[test]
8820 fn test_service_worker_fetch_intercept_mode() {
8821 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8822 assert_eq!(
8823 handle.fetch_intercept_mode(),
8824 ServiceWorkerFetchInterceptMode::None
8825 );
8826
8827 handle.enable_fetch_interception();
8828 assert_eq!(
8829 handle.fetch_intercept_mode(),
8830 ServiceWorkerFetchInterceptMode::Intercepting
8831 );
8832
8833 handle.disable_fetch_interception();
8834 assert_eq!(
8835 handle.fetch_intercept_mode(),
8836 ServiceWorkerFetchInterceptMode::None
8837 );
8838 }
8839
8840 #[test]
8841 fn test_service_worker_registration_state_all_transitions() {
8842 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8843 assert_eq!(
8844 handle.registration_state(),
8845 ServiceWorkerRegistrationState::Installing
8846 );
8847
8848 handle.transition_state(ServiceWorkerRegistrationState::Installed);
8849 assert_eq!(
8850 handle.registration_state(),
8851 ServiceWorkerRegistrationState::Installed
8852 );
8853
8854 handle.transition_state(ServiceWorkerRegistrationState::Activating);
8855 assert_eq!(
8856 handle.registration_state(),
8857 ServiceWorkerRegistrationState::Activating
8858 );
8859
8860 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8861 assert_eq!(
8862 handle.registration_state(),
8863 ServiceWorkerRegistrationState::Activated
8864 );
8865
8866 handle.transition_state(ServiceWorkerRegistrationState::Redundant);
8867 assert_eq!(
8868 handle.registration_state(),
8869 ServiceWorkerRegistrationState::Redundant
8870 );
8871 }
8872
8873 #[test]
8874 fn test_service_worker_navigator_from_scope_config() {
8875 let config = ServiceWorkerScopeConfig {
8876 stealth_profile: None,
8877 user_agent: "Mozilla/5.0 Test".to_string(),
8878 platform: "Linux x86_64".to_string(),
8879 hardware_concurrency: 4,
8880 language: "zh-CN".to_string(),
8881 languages: vec!["zh-CN".to_string(), "zh".to_string()],
8882 registering_page_url: "https://example.com/".to_string(),
8883 };
8884 let nav = WorkerNavigator::from_scope_config(&config);
8885 assert_eq!(nav.user_agent, "Mozilla/5.0 Test");
8886 assert_eq!(nav.platform, "Linux x86_64");
8887 assert_eq!(nav.hardware_concurrency, 4);
8888 assert_eq!(nav.language, "zh-CN");
8889 assert_eq!(nav.languages, vec!["zh-CN".to_string(), "zh".to_string()]);
8890 }
8891
8892 #[test]
8893 fn test_webview_state_service_worker_scope_config() {
8894 let mut state = BaoWebViewState::default();
8895 let reg_id = ServiceWorkerRegistrationId {
8896 script_url: "sw.js".to_string(),
8897 scope: "/".to_string(),
8898 };
8899 let config = ServiceWorkerScopeConfig::default();
8900 let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8901 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8902 state.set_controlling_service_worker(handle);
8903 state.register_service_worker_scope(scope);
8904
8905 let new_config = ServiceWorkerScopeConfig {
8906 user_agent: "Updated Agent".to_string(),
8907 ..ServiceWorkerScopeConfig::default()
8908 };
8909 state.set_service_worker_scope_config(&new_config);
8910 assert_eq!(
8911 state.service_worker_scope().unwrap().navigator().user_agent,
8912 "Updated Agent"
8913 );
8914 }
8915}