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 match self.worker_to_page_rx.try_recv() {
417 Ok(msg) => Ok(Some(msg)),
418 Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
419 Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(()),
420 }
421 }
422
423 pub fn drain_worker_messages(&self) -> WorkerDrainResult {
435 let mut messages = Vec::new();
436 let mut disconnected = false;
437 loop {
438 match self.worker_to_page_rx.try_recv() {
439 Ok(msg) => messages.push(msg),
440 Err(std::sync::mpsc::TryRecvError::Empty) => break,
441 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
442 disconnected = true;
443 break;
444 }
445 }
446 }
447 WorkerDrainResult {
448 messages,
449 disconnected,
450 }
451 }
452}
453
454#[derive(Debug)]
464pub struct WorkerDrainResult {
465 pub messages: Vec<WorkerStructuredMessage>,
467 pub disconnected: bool,
469}
470
471pub struct WorkerChannelEndpoints {
489 pub worker_id: WorkerId,
491 pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
494 pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
497}
498
499#[derive(Debug, Clone)]
524pub struct SharedWorkerGlobalScopeState {
525 pub scope: WorkerGlobalScopeState,
528 pub shared_worker_id: SharedWorkerId,
532 pub has_onconnect: bool,
536 pub connect_count: usize,
540}
541
542impl SharedWorkerGlobalScopeState {
543 pub fn new(shared_worker_id: SharedWorkerId, config: &SharedWorkerScopeConfig) -> Self {
547 let worker_url = shared_worker_id.script_url.clone();
548 SharedWorkerGlobalScopeState {
549 scope: WorkerGlobalScopeState::new_shared(worker_url, config),
550 shared_worker_id,
551 has_onconnect: false,
552 connect_count: 0,
553 }
554 }
555
556 pub fn location(&self) -> Option<&WorkerLocation> {
560 self.scope.location.as_ref()
561 }
562
563 pub fn navigator(&self) -> &WorkerNavigator {
567 &self.scope.navigator
568 }
569
570 pub fn set_onconnect(&mut self) {
574 self.has_onconnect = true;
575 }
576
577 pub fn page_connected(&mut self) {
581 self.connect_count += 1;
582 }
583}
584
585#[derive(Debug)]
611pub struct SharedWorkerPortChannel {
612 pub shared_worker_id: SharedWorkerId,
615 pub page_to_worker_tx: Sender<StructuredClonePayload>,
618 pub worker_to_page_rx: Receiver<WorkerStructuredMessage>,
621}
622
623impl SharedWorkerPortChannel {
624 pub fn new(shared_worker_id: SharedWorkerId) -> (Self, SharedWorkerPortEndpoints) {
631 let (page_to_worker_tx, page_to_worker_rx) =
632 std::sync::mpsc::channel::<StructuredClonePayload>();
633 let (worker_to_page_tx, worker_to_page_rx) =
634 std::sync::mpsc::channel::<WorkerStructuredMessage>();
635
636 let port = SharedWorkerPortChannel {
637 shared_worker_id: shared_worker_id.clone(),
638 page_to_worker_tx,
639 worker_to_page_rx,
640 };
641
642 let endpoints = SharedWorkerPortEndpoints {
643 shared_worker_id,
644 page_to_worker_rx: Some(page_to_worker_rx),
645 worker_to_page_tx: Some(worker_to_page_tx),
646 };
647
648 (port, endpoints)
649 }
650
651 pub fn post_message_to_worker(
655 &self,
656 payload: StructuredClonePayload,
657 ) -> Result<(), std::sync::mpsc::SendError<StructuredClonePayload>> {
658 self.page_to_worker_tx.send(payload)
659 }
660
661 pub fn try_recv_from_worker(&self) -> Result<Option<WorkerStructuredMessage>, ()> {
665 match self.worker_to_page_rx.try_recv() {
666 Ok(msg) => Ok(Some(msg)),
667 Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None),
668 Err(std::sync::mpsc::TryRecvError::Disconnected) => Err(()),
669 }
670 }
671
672 pub fn drain_worker_messages(&self) -> WorkerDrainResult {
678 let mut messages = Vec::new();
679 let mut disconnected = false;
680 loop {
681 match self.worker_to_page_rx.try_recv() {
682 Ok(msg) => messages.push(msg),
683 Err(std::sync::mpsc::TryRecvError::Empty) => break,
684 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
685 disconnected = true;
686 break;
687 }
688 }
689 }
690 WorkerDrainResult {
691 messages,
692 disconnected,
693 }
694 }
695}
696
697#[derive(Debug)]
704pub struct SharedWorkerPortEndpoints {
705 pub shared_worker_id: SharedWorkerId,
707 pub page_to_worker_rx: Option<Receiver<StructuredClonePayload>>,
710 pub worker_to_page_tx: Option<Sender<WorkerStructuredMessage>>,
713}
714
715pub struct SharedWorkerChannelBridge {
724 pub shared_worker_id: SharedWorkerId,
727 pub port_channels: Vec<SharedWorkerPortChannel>,
731}
732
733impl SharedWorkerChannelBridge {
734 pub fn new(shared_worker_id: SharedWorkerId) -> Self {
738 SharedWorkerChannelBridge {
739 shared_worker_id,
740 port_channels: Vec::new(),
741 }
742 }
743
744 pub fn add_port(&mut self) -> SharedWorkerPortEndpoints {
750 let (port, endpoints) = SharedWorkerPortChannel::new(self.shared_worker_id.clone());
751 self.port_channels.push(port);
752 endpoints
753 }
754
755 pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
764 let mut all_messages = Vec::new();
765 let mut disconnected = Vec::new();
766 for port in &self.port_channels {
767 let result = port.drain_worker_messages();
768 all_messages.extend(result.messages);
769 if result.disconnected {
770 disconnected.push(port.shared_worker_id.clone());
771 }
772 }
773 (all_messages, disconnected)
774 }
775
776 pub fn remove_disconnected_ports(&mut self) {
780 self.port_channels.retain(|port| {
781 match port.try_recv_from_worker() {
784 Ok(_) => true, Err(()) => false, }
787 });
788 }
789
790 pub fn port_count(&self) -> usize {
794 self.port_channels.len()
795 }
796
797 pub fn post_to_worker_from_port(
801 &self,
802 port_index: usize,
803 payload: StructuredClonePayload,
804 ) -> Result<(), String> {
805 match self.port_channels.get(port_index) {
806 Some(port) => port
807 .post_message_to_worker(payload)
808 .map_err(|e| format!("SharedWorker port channel closed: {}", e)),
809 None => Err(format!(
810 "Invalid port index {} for SharedWorker",
811 port_index
812 )),
813 }
814 }
815}
816
817#[derive(Debug, Clone, PartialEq, Eq, Hash)]
846pub struct SharedWorkerId {
847 pub script_url: String,
849 pub name: String,
851}
852
853#[derive(Debug, Clone)]
860pub struct SharedWorkerHandle {
861 pub script_url: String,
863 pub name: String,
865 pub closing: Arc<AtomicBool>,
867 pub terminated: Arc<AtomicBool>,
869 pub connected_pages: Arc<std::sync::atomic::AtomicUsize>,
872}
873
874impl SharedWorkerHandle {
875 pub fn new(script_url: String, name: String) -> Self {
879 SharedWorkerHandle {
880 script_url,
881 name,
882 closing: Arc::new(AtomicBool::new(false)),
883 terminated: Arc::new(AtomicBool::new(false)),
884 connected_pages: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
885 }
886 }
887
888 pub fn id(&self) -> SharedWorkerId {
892 SharedWorkerId {
893 script_url: self.script_url.clone(),
894 name: self.name.clone(),
895 }
896 }
897
898 pub fn is_closing(&self) -> bool {
902 self.closing.load(Ordering::Acquire)
903 }
904
905 pub fn is_terminated(&self) -> bool {
909 self.terminated.load(Ordering::Acquire)
910 }
911
912 pub fn connected_page_count(&self) -> usize {
916 self.connected_pages.load(Ordering::Acquire)
917 }
918
919 pub fn close(&self) {
925 self.closing.store(true, Ordering::Release);
926 }
927
928 pub fn mark_terminated(&self) {
932 self.terminated.store(true, Ordering::Release);
933 }
934
935 pub fn page_connected(&self) {
939 self.connected_pages.fetch_add(1, Ordering::AcqRel);
940 }
941
942 pub fn page_disconnected(&self) -> usize {
947 self.connected_pages.fetch_sub(1, Ordering::AcqRel)
948 }
949}
950
951#[derive(Debug, Clone)]
959pub struct SharedWorkerConnectEvent {
960 pub shared_worker_id: SharedWorkerId,
962 pub page_url: String,
964}
965
966#[derive(Debug, Clone)]
976pub struct SharedWorkerScopeConfig {
977 pub stealth_profile: Option<bao_stealth::StealthProfile>,
981 pub user_agent: String,
984 pub platform: String,
987 pub hardware_concurrency: usize,
990 pub language: String,
993 pub languages: Vec<String>,
996}
997
998impl Default for SharedWorkerScopeConfig {
999 fn default() -> Self {
1000 SharedWorkerScopeConfig {
1001 stealth_profile: None,
1002 user_agent: String::new(),
1003 platform: String::new(),
1004 hardware_concurrency: std::thread::available_parallelism()
1005 .map(|n| n.get())
1006 .unwrap_or(1),
1007 language: "en-US".to_string(),
1008 languages: vec!["en-US".to_string(), "en".to_string()],
1009 }
1010 }
1011}
1012
1013#[derive(Debug)]
1021pub struct SharedWorkerPortRef {
1022 handle: SharedWorkerHandle,
1024}
1025
1026impl SharedWorkerPortRef {
1027 pub fn new(handle: SharedWorkerHandle) -> Self {
1031 handle.page_connected();
1032 SharedWorkerPortRef { handle }
1033 }
1034
1035 pub fn handle(&self) -> &SharedWorkerHandle {
1037 &self.handle
1038 }
1039}
1040
1041impl Drop for SharedWorkerPortRef {
1042 fn drop(&mut self) {
1043 self.handle.page_disconnected();
1048 }
1049}
1050
1051impl Clone for SharedWorkerPortRef {
1052 fn clone(&self) -> Self {
1053 self.handle.page_connected();
1055 SharedWorkerPortRef {
1056 handle: self.handle.clone(),
1057 }
1058 }
1059}
1060
1061#[derive(Debug, Clone, PartialEq, Eq)]
1075pub enum WorkerTeardownPath {
1076 Terminate,
1080 SelfClose,
1084 PageUnload,
1088}
1089
1090#[derive(Debug, Clone, PartialEq, Eq)]
1095pub enum WorkerLifecycleState {
1096 Running,
1098 Closing(WorkerTeardownPath),
1101 Terminated(WorkerTeardownPath),
1103 Failed,
1105}
1106
1107#[derive(Debug, Clone, PartialEq, Eq)]
1140pub struct WorkerTeardownResult {
1141 pub path: WorkerTeardownPath,
1143 pub thread_joined: bool,
1146 pub realm_profile_unregistered: bool,
1149 pub closing_flag_set: bool,
1151 pub never_registered: bool,
1157}
1158
1159impl WorkerTeardownResult {
1160 pub fn is_crash_safe(&self) -> bool {
1177 self.closing_flag_set
1178 && self.thread_joined
1179 && (self.realm_profile_unregistered || self.never_registered)
1180 }
1181}
1182
1183const WORKER_TEARDOWN_TIMEOUT_MS: u64 = 5000;
1188
1189pub fn crash_safe_teardown_worker(
1214 handle: &WorkerHandle,
1215 path: WorkerTeardownPath,
1216) -> WorkerTeardownResult {
1217 let was_already_closing = handle.is_closing();
1222 handle.terminate();
1223
1224 let realm_unregistered = if handle.worker_global_addr() != 0 {
1229 handle.unregister_stealth_profile();
1230 true
1231 } else {
1232 false
1235 };
1236
1237 let thread_joined = true;
1245
1246 handle.mark_terminated();
1249
1250 if !was_already_closing {
1251 log::debug!(
1252 "[bao] crash-safe teardown: worker '{}' via {:?}, joined={}, realm_unreg={}",
1253 handle.script_url,
1254 path,
1255 thread_joined,
1256 realm_unregistered,
1257 );
1258 }
1259
1260 WorkerTeardownResult {
1261 path,
1262 thread_joined,
1263 realm_profile_unregistered: realm_unregistered,
1264 closing_flag_set: true,
1265 never_registered: handle.worker_global_addr() == 0,
1266 }
1267}
1268
1269#[derive(Debug, Clone, PartialEq, Eq)]
1292pub struct WorkerLocation {
1293 pub href: String,
1296 pub protocol: String,
1299 pub host: String,
1302 pub hostname: String,
1305 pub port: String,
1308 pub pathname: String,
1311 pub search: String,
1314 pub hash: String,
1317 pub origin: String,
1320}
1321
1322impl WorkerLocation {
1323 pub fn from_url(url_str: &str) -> Option<Self> {
1329 let parsed = url::Url::parse(url_str).ok()?;
1330 let scheme = parsed.scheme();
1331 let host = parsed.host_str().unwrap_or("");
1332 let port = parsed.port();
1333 let default_port_for_scheme = match scheme {
1334 "http" => Some(80),
1335 "https" => Some(443),
1336 _ => None,
1337 };
1338 let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
1339 let host_with_port = if is_default_port {
1340 host.to_string()
1341 } else {
1342 format!("{}:{}", host, port.unwrap())
1343 };
1344 let origin = if scheme == "http" || scheme == "https" {
1345 if is_default_port {
1346 format!("{}://{}", scheme, host)
1347 } else {
1348 format!("{}://{}:{}", scheme, host, port.unwrap())
1349 }
1350 } else {
1351 "null".to_string()
1352 };
1353
1354 Some(WorkerLocation {
1355 href: url_str.to_string(),
1356 protocol: format!("{}:", scheme),
1357 host: host_with_port,
1358 hostname: host.to_string(),
1359 port: port.map_or(String::new(), |p| p.to_string()),
1360 pathname: parsed.path().to_string(),
1361 search: parsed.query().map_or(String::new(), |q| format!("?{}", q)),
1362 hash: parsed
1363 .fragment()
1364 .map_or(String::new(), |f| format!("#{}", f)),
1365 origin,
1366 })
1367 }
1368
1369 pub fn from_url_value(url: url::Url) -> Self {
1374 let scheme = url.scheme();
1375 let host = url.host_str().unwrap_or("");
1376 let port = url.port();
1377 let default_port_for_scheme = match scheme {
1378 "http" => Some(80),
1379 "https" => Some(443),
1380 _ => None,
1381 };
1382 let is_default_port = port.map_or(true, |p| Some(p) == default_port_for_scheme);
1383 let host_with_port = if is_default_port {
1384 host.to_string()
1385 } else {
1386 format!("{}:{}", host, port.unwrap())
1387 };
1388 let origin = if scheme == "http" || scheme == "https" {
1389 if is_default_port {
1390 format!("{}://{}", scheme, host)
1391 } else {
1392 format!("{}://{}:{}", scheme, host, port.unwrap())
1393 }
1394 } else {
1395 "null".to_string()
1396 };
1397 let href = url.to_string();
1398
1399 WorkerLocation {
1400 href,
1401 protocol: format!("{}:", scheme),
1402 host: host_with_port,
1403 hostname: host.to_string(),
1404 port: port.map_or(String::new(), |p| p.to_string()),
1405 pathname: url.path().to_string(),
1406 search: url.query().map_or(String::new(), |q| format!("?{}", q)),
1407 hash: url.fragment().map_or(String::new(), |f| format!("#{}", f)),
1408 origin,
1409 }
1410 }
1411}
1412
1413#[derive(Debug, Clone)]
1427pub struct WorkerNavigator {
1428 pub user_agent: String,
1431 pub platform: String,
1434 pub hardware_concurrency: usize,
1437 pub language: String,
1440 pub languages: Vec<String>,
1443 pub connection: Option<WorkerNetworkInformation>,
1446 pub cookie_enabled: bool,
1449 pub max_touch_points: u32,
1452 pub product: String,
1455 pub app_code_name: String,
1458 pub app_name: String,
1461 pub app_version: String,
1464}
1465
1466#[derive(Debug, Clone, PartialEq, Eq)]
1472pub struct WorkerNetworkInformation {
1473 pub effective_type: String,
1475 pub downlink: u64,
1477 pub rtt: u64,
1479 pub save_data: bool,
1481}
1482
1483impl WorkerNavigator {
1484 pub fn from_scope_config(config: &WorkerScopeConfig) -> Self {
1491 WorkerNavigator {
1492 user_agent: config.user_agent.clone(),
1493 platform: config.platform.clone(),
1494 hardware_concurrency: config.hardware_concurrency,
1495 language: config.language.clone(),
1496 languages: config.languages.clone(),
1497 connection: None,
1498 cookie_enabled: false,
1499 max_touch_points: 0,
1500 product: "Gecko".to_string(),
1501 app_code_name: "Mozilla".to_string(),
1502 app_name: "Netscape".to_string(),
1503 app_version: config.user_agent.clone(),
1504 }
1505 }
1506
1507 pub fn from_shared_scope_config(config: &SharedWorkerScopeConfig) -> Self {
1511 WorkerNavigator {
1512 user_agent: config.user_agent.clone(),
1513 platform: config.platform.clone(),
1514 hardware_concurrency: config.hardware_concurrency,
1515 language: config.language.clone(),
1516 languages: config.languages.clone(),
1517 connection: None,
1518 cookie_enabled: false,
1519 max_touch_points: 0,
1520 product: "Gecko".to_string(),
1521 app_code_name: "Mozilla".to_string(),
1522 app_name: "Netscape".to_string(),
1523 app_version: config.user_agent.clone(),
1524 }
1525 }
1526}
1527
1528impl Default for WorkerNavigator {
1529 fn default() -> Self {
1530 WorkerNavigator {
1531 user_agent: String::new(),
1532 platform: String::new(),
1533 hardware_concurrency: std::thread::available_parallelism()
1534 .map(|n| n.get())
1535 .unwrap_or(1),
1536 language: "en-US".to_string(),
1537 languages: vec!["en-US".to_string(), "en".to_string()],
1538 connection: None,
1539 cookie_enabled: false,
1540 max_touch_points: 0,
1541 product: "Gecko".to_string(),
1542 app_code_name: "Mozilla".to_string(),
1543 app_name: "Netscape".to_string(),
1544 app_version: String::new(),
1545 }
1546 }
1547}
1548
1549#[derive(Debug, Clone)]
1564pub struct WorkerGlobalScopeState {
1565 pub worker_url: String,
1568 pub closing: bool,
1571 pub location: Option<WorkerLocation>,
1574 pub navigator: WorkerNavigator,
1577}
1578
1579impl WorkerGlobalScopeState {
1580 pub fn new(worker_url: String, config: &WorkerScopeConfig) -> Self {
1584 WorkerGlobalScopeState {
1585 location: WorkerLocation::from_url(&worker_url),
1586 navigator: WorkerNavigator::from_scope_config(config),
1587 worker_url,
1588 closing: false,
1589 }
1590 }
1591
1592 pub fn new_shared(worker_url: String, config: &SharedWorkerScopeConfig) -> Self {
1596 WorkerGlobalScopeState {
1597 location: WorkerLocation::from_url(&worker_url),
1598 navigator: WorkerNavigator::from_shared_scope_config(config),
1599 worker_url,
1600 closing: false,
1601 }
1602 }
1603}
1604
1605#[derive(Debug, Clone)]
1624pub struct DedicatedWorkerGlobalScopeState {
1625 pub scope: WorkerGlobalScopeState,
1628 pub worker_id: WorkerId,
1632 pub has_onmessage: bool,
1636 pub has_onerror: bool,
1640}
1641
1642impl DedicatedWorkerGlobalScopeState {
1643 pub fn new(worker_id: WorkerId, config: &WorkerScopeConfig) -> Self {
1647 let worker_url = worker_id.0.clone();
1648 DedicatedWorkerGlobalScopeState {
1649 scope: WorkerGlobalScopeState::new(worker_url, config),
1650 worker_id,
1651 has_onmessage: false,
1652 has_onerror: false,
1653 }
1654 }
1655
1656 pub fn location(&self) -> Option<&WorkerLocation> {
1660 self.scope.location.as_ref()
1661 }
1662
1663 pub fn navigator(&self) -> &WorkerNavigator {
1667 &self.scope.navigator
1668 }
1669
1670 pub fn set_onmessage(&mut self) {
1674 self.has_onmessage = true;
1675 }
1676
1677 pub fn set_onerror(&mut self) {
1681 self.has_onerror = true;
1682 }
1683}
1684
1685#[derive(Debug, Clone)]
1705pub struct WorkerScopeConfig {
1706 pub stealth_profile: Option<bao_stealth::StealthProfile>,
1711 pub user_agent: String,
1714 pub platform: String,
1717 pub hardware_concurrency: usize,
1720 pub language: String,
1723 pub languages: Vec<String>,
1726}
1727
1728impl Default for WorkerScopeConfig {
1729 fn default() -> Self {
1730 WorkerScopeConfig {
1731 stealth_profile: None,
1732 user_agent: String::new(),
1733 platform: String::new(),
1734 hardware_concurrency: std::thread::available_parallelism()
1735 .map(|n| n.get())
1736 .unwrap_or(1),
1737 language: "en-US".to_string(),
1738 languages: vec!["en-US".to_string(), "en".to_string()],
1739 }
1740 }
1741}
1742
1743impl From<&bao_stealth::StealthProfile> for WorkerScopeConfig {
1750 fn from(profile: &bao_stealth::StealthProfile) -> Self {
1756 WorkerScopeConfig {
1757 stealth_profile: Some(profile.clone()),
1758 user_agent: profile.navigator.user_agent.clone(),
1759 platform: profile.navigator.platform.clone(),
1760 hardware_concurrency: profile.navigator.hardware_concurrency as usize,
1761 language: profile.navigator.language.clone(),
1762 languages: profile.navigator.languages.clone(),
1763 }
1764 }
1765}
1766
1767impl From<&bao_stealth::StealthProfile> for SharedWorkerScopeConfig {
1768 fn from(profile: &bao_stealth::StealthProfile) -> Self {
1773 SharedWorkerScopeConfig {
1774 stealth_profile: Some(profile.clone()),
1775 user_agent: profile.navigator.user_agent.clone(),
1776 platform: profile.navigator.platform.clone(),
1777 hardware_concurrency: profile.navigator.hardware_concurrency as usize,
1778 language: profile.navigator.language.clone(),
1779 languages: profile.navigator.languages.clone(),
1780 }
1781 }
1782}
1783
1784pub struct AutoCloseWorker {
1802 handle: WorkerHandle,
1803 teardown_path: WorkerTeardownPath,
1807}
1808
1809impl AutoCloseWorker {
1810 pub fn new(handle: WorkerHandle) -> Self {
1814 AutoCloseWorker {
1815 handle,
1816 teardown_path: WorkerTeardownPath::PageUnload,
1817 }
1818 }
1819
1820 pub fn lifecycle_state(&self) -> WorkerLifecycleState {
1824 if self.handle.is_terminated() {
1825 WorkerLifecycleState::Terminated(self.teardown_path.clone())
1826 } else if self.handle.is_closing() {
1827 WorkerLifecycleState::Closing(self.teardown_path.clone())
1828 } else {
1829 WorkerLifecycleState::Running
1830 }
1831 }
1832
1833 pub fn terminate_via(&mut self, path: WorkerTeardownPath) {
1838 if !self.handle.is_closing() {
1839 self.teardown_path = path;
1840 self.handle.terminate();
1841 }
1842 }
1843
1844 pub fn handle(&self) -> &WorkerHandle {
1846 &self.handle
1847 }
1848}
1849
1850impl Drop for AutoCloseWorker {
1851 fn drop(&mut self) {
1852 if !self.handle.is_closing() {
1877 self.teardown_path = WorkerTeardownPath::PageUnload;
1878 self.handle.terminate();
1879 }
1880 self.handle.unregister_stealth_profile();
1883 self.handle.mark_terminated();
1888 }
1889}
1890
1891#[derive(Debug, Clone, PartialEq, Eq)]
1938pub enum WorkerScriptSource {
1939 Inline(String),
1943 Url(String),
1947}
1948
1949#[derive(Debug, Clone, PartialEq, Eq)]
1953pub struct WorkerScriptLoadResult {
1954 pub source: String,
1957 pub final_url: String,
1960 pub mime_type: Option<String>,
1963}
1964
1965#[derive(Debug, Clone, PartialEq, Eq)]
1969pub enum WorkerScriptLoadError {
1970 NetworkError(String),
1973 InvalidMimeType {
1977 received: String,
1979 url: String,
1981 },
1982 Utf8DecodeError(String),
1986 InvalidUrl(String),
1989 Cancelled,
1992}
1993
1994#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2000pub enum WorkerScriptType {
2001 Classic,
2004 Module,
2007}
2008
2009impl Default for WorkerScriptType {
2010 fn default() -> Self {
2011 WorkerScriptType::Classic
2012 }
2013}
2014
2015const JAVASCRIPT_MIME_TYPES: &[&str] = &[
2024 "application/ecmascript",
2025 "application/javascript",
2026 "application/x-ecmascript",
2027 "application/x-javascript",
2028 "text/ecmascript",
2029 "text/javascript",
2030 "text/javascript1.0",
2031 "text/javascript1.1",
2032 "text/javascript1.2",
2033 "text/javascript1.3",
2034 "text/javascript1.4",
2035 "text/javascript1.5",
2036 "text/jscript",
2037 "text/livescript",
2038 "text/x-ecmascript",
2039 "text/x-javascript",
2040];
2041
2042pub fn is_javascript_mime_type(mime: &str) -> bool {
2050 let base_type = mime.split(';').next().unwrap_or(mime).trim();
2052 JAVASCRIPT_MIME_TYPES
2053 .iter()
2054 .any(|&valid| valid.eq_ignore_ascii_case(base_type))
2055}
2056
2057#[derive(Debug, Clone)]
2071pub struct WorkerScriptLoader {
2072 pub source: WorkerScriptSource,
2075 pub script_type: WorkerScriptType,
2078}
2079
2080impl WorkerScriptLoader {
2081 pub fn inline(script: String, script_type: WorkerScriptType) -> Self {
2085 WorkerScriptLoader {
2086 source: WorkerScriptSource::Inline(script),
2087 script_type,
2088 }
2089 }
2090
2091 pub fn url(url: String, script_type: WorkerScriptType) -> Self {
2095 WorkerScriptLoader {
2096 source: WorkerScriptSource::Url(url),
2097 script_type,
2098 }
2099 }
2100
2101 pub fn from_source(source: WorkerScriptSource, script_type: WorkerScriptType) -> Self {
2105 WorkerScriptLoader {
2106 source,
2107 script_type,
2108 }
2109 }
2110
2111 pub fn resolve(&self) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2124 match &self.source {
2125 WorkerScriptSource::Inline(content) => {
2126 Ok(WorkerScriptSource::Inline(content.clone()))
2128 }
2129 WorkerScriptSource::Url(url_str) => {
2130 let parsed = url::Url::parse(url_str).map_err(|e| {
2132 WorkerScriptLoadError::InvalidUrl(format!(
2133 "Invalid Worker script URL '{}': {}",
2134 url_str, e
2135 ))
2136 })?;
2137
2138 if parsed.scheme() == "data" {
2140 return Self::resolve_data_url(&parsed);
2141 }
2142
2143 if parsed.scheme() == "blob" {
2147 return Ok(WorkerScriptSource::Url(url_str.clone()));
2148 }
2149
2150 if parsed.scheme() == "http" || parsed.scheme() == "https" {
2153 return Ok(WorkerScriptSource::Url(url_str.clone()));
2154 }
2155
2156 if parsed.scheme() == "file" {
2158 return Self::resolve_file_url(&parsed);
2159 }
2160
2161 Err(WorkerScriptLoadError::InvalidUrl(format!(
2162 "Unsupported Worker script URL scheme '{}'",
2163 parsed.scheme()
2164 )))
2165 }
2166 }
2167 }
2168
2169 fn resolve_data_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2177 let path = parsed.path();
2179 let comma_pos = path.find(',').ok_or_else(|| {
2181 WorkerScriptLoadError::InvalidUrl("data: URL missing comma separator".to_string())
2182 })?;
2183
2184 let metadata = &path[..comma_pos];
2185 let data = &path[comma_pos + 1..];
2186
2187 let (mime_part, is_base64) = if metadata.ends_with(";base64") {
2189 (&metadata[..metadata.len() - 7], true)
2190 } else if metadata.is_empty() {
2191 ("text/plain", false)
2192 } else {
2193 (metadata, false)
2194 };
2195
2196 if !mime_part.is_empty() && !is_javascript_mime_type(mime_part) {
2201 log::warn!(
2204 "[WorkerScriptLoader] data: URL has non-JS MIME type '{}', loading anyway",
2205 mime_part
2206 );
2207 }
2208
2209 let content = if is_base64 {
2211 use base64::Engine;
2212 base64::engine::general_purpose::STANDARD
2213 .decode(data)
2214 .map_err(|e| {
2215 WorkerScriptLoadError::Utf8DecodeError(format!(
2216 "Failed to decode base64 data: URL: {}",
2217 e
2218 ))
2219 })?
2220 } else {
2221 decode_percent_encoded(data)?
2224 };
2225
2226 let script = String::from_utf8(content).map_err(|e| {
2227 WorkerScriptLoadError::Utf8DecodeError(format!(
2228 "data: URL content is not valid UTF-8: {}",
2229 e
2230 ))
2231 })?;
2232
2233 Ok(WorkerScriptSource::Inline(script))
2234 }
2235
2236 fn resolve_file_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
2243 let path = parsed.to_file_path().map_err(|_| {
2244 WorkerScriptLoadError::InvalidUrl(format!(
2245 "Cannot convert file: URL to path: {}",
2246 parsed
2247 ))
2248 })?;
2249
2250 let content = std::fs::read_to_string(&path).map_err(|e| {
2251 WorkerScriptLoadError::NetworkError(format!(
2252 "Failed to read Worker script file '{}': {}",
2253 path.display(),
2254 e
2255 ))
2256 })?;
2257
2258 Ok(WorkerScriptSource::Inline(content))
2259 }
2260
2261 pub fn validate_mime_type(mime_type: &str, url: &str) -> Result<(), WorkerScriptLoadError> {
2272 if is_javascript_mime_type(mime_type) {
2273 Ok(())
2274 } else {
2275 Err(WorkerScriptLoadError::InvalidMimeType {
2276 received: mime_type.to_string(),
2277 url: url.to_string(),
2278 })
2279 }
2280 }
2281
2282 pub fn script_url(&self) -> Option<&str> {
2286 match &self.source {
2287 WorkerScriptSource::Url(url) => Some(url),
2288 WorkerScriptSource::Inline(_) => None,
2289 }
2290 }
2291
2292 pub fn requires_fetch(&self) -> bool {
2296 matches!(&self.source, WorkerScriptSource::Url(url)
2297 if url.starts_with("http://") || url.starts_with("https://"))
2298 }
2299
2300 pub fn load<F>(
2328 &self,
2329 stealth_profile: &Option<bao_stealth::StealthProfile>,
2330 mut state_callback: F,
2331 ) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError>
2332 where
2333 F: FnMut(WorkerScriptLoadState),
2334 {
2335 state_callback(WorkerScriptLoadState::Pending);
2338 let resolved = self.resolve()?;
2339
2340 let (source, final_url, mime_type) = match resolved {
2341 WorkerScriptSource::Inline(content) => {
2342 state_callback(WorkerScriptLoadState::Ready);
2345 return Ok(WorkerScriptLoadResult {
2346 source: content,
2347 final_url: self.script_url().unwrap_or("inline").to_string(),
2348 mime_type: None,
2349 });
2350 }
2351 WorkerScriptSource::Url(url_str) => {
2352 state_callback(WorkerScriptLoadState::Fetching);
2355
2356 let response = fetch_worker_script(&url_str, stealth_profile)
2357 .map_err(|e| WorkerScriptLoadError::NetworkError(e))?;
2358
2359 state_callback(WorkerScriptLoadState::Validating);
2362
2363 let ct = response
2365 .headers
2366 .iter()
2367 .find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
2368 .map(|(_, v)| v.to_string());
2369
2370 if let Some(ref content_type) = ct {
2371 Self::validate_mime_type(content_type, &url_str)?;
2374 }
2375 if ct.is_none() {
2381 log::warn!(
2382 "[WorkerScriptLoader] no Content-Type header for '{}', loading anyway",
2383 url_str
2384 );
2385 }
2386
2387 state_callback(WorkerScriptLoadState::Decoding);
2390
2391 let source = String::from_utf8(response.body.to_vec()).map_err(|e| {
2392 WorkerScriptLoadError::Utf8DecodeError(format!(
2393 "Worker script response body is not valid UTF-8: {}",
2394 e
2395 ))
2396 })?;
2397
2398 (source, url_str, ct)
2399 }
2400 };
2401
2402 state_callback(WorkerScriptLoadState::Compiling);
2407
2408 state_callback(WorkerScriptLoadState::Ready);
2411
2412 Ok(WorkerScriptLoadResult {
2413 source,
2414 final_url,
2415 mime_type,
2416 })
2417 }
2418
2419 pub fn load_simple(
2426 &self,
2427 stealth_profile: &Option<bao_stealth::StealthProfile>,
2428 ) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError> {
2429 self.load(stealth_profile, |_| {})
2430 }
2431
2432 pub fn is_module(&self) -> bool {
2439 matches!(self.script_type, WorkerScriptType::Module)
2440 }
2441}
2442
2443fn fetch_worker_script(
2459 url: &str,
2460 stealth_profile: &Option<bao_stealth::StealthProfile>,
2461) -> Result<WorkerScriptFetchResponse, String> {
2462 use bun_http::Method;
2463 use bun_runtime::stealth_http::stealth_http_request;
2464
2465 let result = stealth_http_request(
2472 stealth_profile,
2473 Method::GET,
2474 url,
2475 &[], None, )
2478 .map_err(|e| format!("Failed to fetch Worker script from '{}': {}", url, e))?;
2479
2480 Ok(WorkerScriptFetchResponse {
2482 status_code: result.status_code,
2483 headers: result
2484 .headers
2485 .into_iter()
2486 .map(|(k, v)| (k.to_string(), v.to_string()))
2487 .collect(),
2488 body: result.body.to_vec(),
2489 })
2490}
2491
2492struct WorkerScriptFetchResponse {
2499 status_code: u32,
2501 headers: Vec<(String, String)>,
2503 body: Vec<u8>,
2505}
2506#[derive(Debug, Clone, PartialEq, Eq)]
2519pub enum WorkerScriptLoadState {
2520 Pending,
2523 Fetching,
2526 Validating,
2529 Decoding,
2532 Compiling,
2535 Ready,
2538 Failed(WorkerScriptLoadError),
2541}
2542
2543impl WorkerScriptLoadState {
2544 pub fn is_ready(&self) -> bool {
2548 matches!(self, WorkerScriptLoadState::Ready)
2549 }
2550
2551 pub fn is_failed(&self) -> bool {
2555 matches!(self, WorkerScriptLoadState::Failed(_))
2556 }
2557
2558 pub fn is_loading(&self) -> bool {
2562 !self.is_ready() && !self.is_failed()
2563 }
2564}
2565
2566fn decode_percent_encoded(data: &str) -> Result<Vec<u8>, WorkerScriptLoadError> {
2573 let mut bytes = Vec::with_capacity(data.len());
2574 let mut chars = data.chars();
2575 while let Some(c) = chars.next() {
2576 if c == '%' {
2577 let hex: String = chars.by_ref().take(2).collect();
2579 if hex.len() != 2 {
2580 return Err(WorkerScriptLoadError::Utf8DecodeError(
2581 "Incomplete percent-encoding in data: URL".to_string(),
2582 ));
2583 }
2584 let byte = u8::from_str_radix(&hex, 16).map_err(|e| {
2585 WorkerScriptLoadError::Utf8DecodeError(format!(
2586 "Invalid percent-encoding '%{}' in data: URL: {}",
2587 hex, e
2588 ))
2589 })?;
2590 bytes.push(byte);
2591 } else if c == '+' {
2592 bytes.push(b' ');
2594 } else {
2595 let mut buf = [0u8; 4];
2597 bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
2598 }
2599 }
2600 String::from_utf8(bytes.clone()).map_err(|e| {
2602 WorkerScriptLoadError::Utf8DecodeError(format!(
2603 "data: URL content is not valid UTF-8: {}",
2604 e
2605 ))
2606 })?;
2607 Ok(bytes)
2608}
2609
2610#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2649pub struct ServiceWorkerRegistrationId {
2650 pub script_url: String,
2652 pub scope: String,
2654}
2655
2656#[derive(Debug, Clone, PartialEq, Eq)]
2663pub enum ServiceWorkerRegistrationState {
2664 Idle,
2667 Installing,
2670 Installed,
2673 Activating,
2676 Activated,
2679 Redundant,
2682}
2683
2684#[derive(Debug, Clone, PartialEq, Eq)]
2692pub enum ServiceWorkerFetchInterceptMode {
2693 None,
2696 Intercepting,
2701}
2702
2703#[derive(Debug, Clone)]
2710pub struct ServiceWorkerHandle {
2711 pub script_url: String,
2713 pub scope: String,
2715 pub closing: Arc<AtomicBool>,
2718 pub terminated: Arc<AtomicBool>,
2720 pub state: Arc<std::sync::Mutex<ServiceWorkerRegistrationState>>,
2722 pub fetch_intercept_mode: Arc<std::sync::Mutex<ServiceWorkerFetchInterceptMode>>,
2724 pub stealth_profile: Option<bao_stealth::StealthProfile>,
2731}
2732
2733impl ServiceWorkerHandle {
2734 pub fn new(
2738 script_url: String,
2739 scope: String,
2740 stealth_profile: Option<bao_stealth::StealthProfile>,
2741 ) -> Self {
2742 ServiceWorkerHandle {
2743 script_url,
2744 scope,
2745 closing: Arc::new(AtomicBool::new(false)),
2746 terminated: Arc::new(AtomicBool::new(false)),
2747 state: Arc::new(std::sync::Mutex::new(
2748 ServiceWorkerRegistrationState::Installing,
2749 )),
2750 fetch_intercept_mode: Arc::new(std::sync::Mutex::new(
2751 ServiceWorkerFetchInterceptMode::None,
2752 )),
2753 stealth_profile,
2754 }
2755 }
2756
2757 pub fn id(&self) -> ServiceWorkerRegistrationId {
2761 ServiceWorkerRegistrationId {
2762 script_url: self.script_url.clone(),
2763 scope: self.scope.clone(),
2764 }
2765 }
2766
2767 pub fn is_closing(&self) -> bool {
2771 self.closing.load(Ordering::Acquire)
2772 }
2773
2774 pub fn is_terminated(&self) -> bool {
2778 self.terminated.load(Ordering::Acquire)
2779 }
2780
2781 pub fn registration_state(&self) -> ServiceWorkerRegistrationState {
2785 self.state
2786 .lock()
2787 .expect("ServiceWorkerHandle state lock poisoned")
2788 .clone()
2789 }
2790
2791 pub fn fetch_intercept_mode(&self) -> ServiceWorkerFetchInterceptMode {
2795 self.fetch_intercept_mode
2796 .lock()
2797 .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned")
2798 .clone()
2799 }
2800
2801 pub fn is_intercepting_fetch(&self) -> bool {
2805 matches!(
2806 self.fetch_intercept_mode(),
2807 ServiceWorkerFetchInterceptMode::Intercepting
2808 )
2809 }
2810
2811 pub fn transition_state(&self, new_state: ServiceWorkerRegistrationState) {
2817 let mut state = self
2818 .state
2819 .lock()
2820 .expect("ServiceWorkerHandle state lock poisoned");
2821 *state = new_state;
2822 }
2823
2824 pub fn enable_fetch_interception(&self) {
2832 let mut mode = self
2833 .fetch_intercept_mode
2834 .lock()
2835 .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
2836 *mode = ServiceWorkerFetchInterceptMode::Intercepting;
2837 }
2838
2839 pub fn disable_fetch_interception(&self) {
2846 let mut mode = self
2847 .fetch_intercept_mode
2848 .lock()
2849 .expect("ServiceWorkerHandle fetch_intercept_mode lock poisoned");
2850 *mode = ServiceWorkerFetchInterceptMode::None;
2851 }
2852
2853 pub fn terminate(&self) {
2858 self.closing.store(true, Ordering::Release);
2859 self.disable_fetch_interception();
2863 }
2864
2865 pub fn mark_terminated(&self) {
2869 self.terminated.store(true, Ordering::Release);
2870 }
2871}
2872
2873#[derive(Debug, Clone)]
2882pub struct ServiceWorkerRegistrationTracking {
2883 pub registration_id: ServiceWorkerRegistrationId,
2886 pub state: ServiceWorkerRegistrationState,
2889 pub fetch_intercept_active: bool,
2892 pub registering_page_url: String,
2896 pub has_fetch_handler: bool,
2900}
2901
2902#[derive(Debug, Clone)]
2912pub struct ServiceWorkerFetchEvent {
2913 pub registration_id: ServiceWorkerRegistrationId,
2915 pub request_url: String,
2917 pub method: String,
2919 pub stealth_profile_applied: bool,
2926}
2927
2928#[derive(Debug, Clone)]
2937pub struct ServiceWorkerGlobalScopeState {
2938 pub scope: WorkerGlobalScopeState,
2941 pub registration_id: ServiceWorkerRegistrationId,
2944 pub has_fetch_handler: bool,
2948 pub has_activate_handler: bool,
2951 pub has_install_handler: bool,
2954 pub has_message_handler: bool,
2957 pub scope_url: String,
2960}
2961
2962#[derive(Debug, Clone)]
2977pub struct ServiceWorkerScopeConfig {
2978 pub stealth_profile: Option<bao_stealth::StealthProfile>,
2983 pub user_agent: String,
2986 pub platform: String,
2989 pub hardware_concurrency: usize,
2992 pub language: String,
2995 pub languages: Vec<String>,
2998 pub registering_page_url: String,
3002}
3003
3004impl Default for ServiceWorkerScopeConfig {
3005 fn default() -> Self {
3006 ServiceWorkerScopeConfig {
3007 stealth_profile: None,
3008 user_agent: String::new(),
3009 platform: String::new(),
3010 hardware_concurrency: std::thread::available_parallelism()
3011 .map(|n| n.get())
3012 .unwrap_or(1),
3013 language: "en-US".to_string(),
3014 languages: vec!["en-US".to_string(), "en".to_string()],
3015 registering_page_url: String::new(),
3016 }
3017 }
3018}
3019
3020impl From<&bao_stealth::StealthProfile> for ServiceWorkerScopeConfig {
3021 fn from(profile: &bao_stealth::StealthProfile) -> Self {
3027 ServiceWorkerScopeConfig {
3028 stealth_profile: Some(profile.clone()),
3029 user_agent: profile.navigator.user_agent.clone(),
3030 platform: profile.navigator.platform.clone(),
3031 hardware_concurrency: profile.navigator.hardware_concurrency as usize,
3032 language: profile.navigator.language.clone(),
3033 languages: profile.navigator.languages.clone(),
3034 registering_page_url: String::new(),
3035 }
3036 }
3037}
3038
3039impl WorkerNavigator {
3040 pub fn from_service_scope_config(config: &ServiceWorkerScopeConfig) -> Self {
3044 WorkerNavigator {
3045 user_agent: config.user_agent.clone(),
3046 platform: config.platform.clone(),
3047 hardware_concurrency: config.hardware_concurrency,
3048 language: config.language.clone(),
3049 languages: config.languages.clone(),
3050 connection: None,
3051 cookie_enabled: false,
3052 max_touch_points: 0,
3053 product: "Gecko".to_string(),
3054 app_code_name: "Mozilla".to_string(),
3055 app_name: "Netscape".to_string(),
3056 app_version: config.user_agent.clone(),
3057 }
3058 }
3059}
3060
3061impl WorkerGlobalScopeState {
3062 pub fn new_service(worker_url: String, config: &ServiceWorkerScopeConfig) -> Self {
3066 WorkerGlobalScopeState {
3067 location: WorkerLocation::from_url(&worker_url),
3068 navigator: WorkerNavigator::from_service_scope_config(config),
3069 worker_url,
3070 closing: false,
3071 }
3072 }
3073}
3074
3075impl ServiceWorkerGlobalScopeState {
3076 pub fn new(
3080 registration_id: ServiceWorkerRegistrationId,
3081 config: &ServiceWorkerScopeConfig,
3082 ) -> Self {
3083 let worker_url = registration_id.script_url.clone();
3084 let scope_url = registration_id.scope.clone();
3085 ServiceWorkerGlobalScopeState {
3086 scope: WorkerGlobalScopeState::new_service(worker_url, config),
3087 registration_id,
3088 has_fetch_handler: false,
3089 has_activate_handler: false,
3090 has_install_handler: false,
3091 has_message_handler: false,
3092 scope_url,
3093 }
3094 }
3095
3096 pub fn location(&self) -> Option<&WorkerLocation> {
3100 self.scope.location.as_ref()
3101 }
3102
3103 pub fn navigator(&self) -> &WorkerNavigator {
3107 &self.scope.navigator
3108 }
3109
3110 pub fn set_fetch_handler(&mut self) {
3115 self.has_fetch_handler = true;
3116 }
3117
3118 pub fn set_activate_handler(&mut self) {
3122 self.has_activate_handler = true;
3123 }
3124
3125 pub fn set_install_handler(&mut self) {
3129 self.has_install_handler = true;
3130 }
3131
3132 pub fn set_message_handler(&mut self) {
3136 self.has_message_handler = true;
3137 }
3138
3139 pub fn is_url_in_scope(&self, url: &str) -> bool {
3146 url.starts_with(&self.scope_url)
3147 }
3148}
3149
3150pub struct BaoWebViewState {
3151 pub url: Option<url::Url>,
3152 pub title: Option<String>,
3153 pub load_status: LoadStatus,
3154 pub frame_ready: bool,
3155 pub dom_proxies_dirty: bool,
3158 pub console_log_tx: Option<std::sync::mpsc::Sender<ConsoleMessage>>,
3160 pub event_tx: Option<Sender<ServoEvent>>,
3164 active_workers: Vec<AutoCloseWorker>,
3170 pub worker_scope_config: WorkerScopeConfig,
3175 shared_worker_ports: Vec<SharedWorkerPortRef>,
3180 shared_worker_channels: HashMap<SharedWorkerId, SharedWorkerChannelBridge>,
3185 shared_worker_scopes: HashMap<SharedWorkerId, SharedWorkerGlobalScopeState>,
3191 worker_channels: HashMap<WorkerId, WorkerChannelBridge>,
3196 dedicated_worker_scopes: HashMap<WorkerId, DedicatedWorkerGlobalScopeState>,
3202 worker_script_load_states: HashMap<WorkerId, WorkerScriptLoadState>,
3207 web_workers: HashMap<WorkerId, WorkerHandle>,
3213 controlled_service_worker: Option<ServiceWorkerHandle>,
3219 service_worker_scope: Option<ServiceWorkerGlobalScopeState>,
3224}
3225
3226impl Default for BaoWebViewState {
3227 fn default() -> Self {
3228 BaoWebViewState {
3229 url: None,
3230 title: None,
3231 load_status: LoadStatus::Started,
3232 frame_ready: false,
3233 dom_proxies_dirty: false,
3234 console_log_tx: None,
3235 event_tx: None,
3236 active_workers: Vec::new(),
3237 worker_scope_config: WorkerScopeConfig::default(),
3238 shared_worker_ports: Vec::new(),
3239 shared_worker_channels: HashMap::new(),
3240 shared_worker_scopes: HashMap::new(),
3241 worker_channels: HashMap::new(),
3242 dedicated_worker_scopes: HashMap::new(),
3243 worker_script_load_states: HashMap::new(),
3244 web_workers: HashMap::new(),
3245 controlled_service_worker: None,
3246 service_worker_scope: None,
3247 }
3248 }
3249}
3250
3251impl BaoWebViewState {
3252 pub fn track_worker(&mut self, handle: WorkerHandle) {
3262 self.active_workers.push(AutoCloseWorker::new(handle));
3263 }
3264
3265 pub fn track_worker_guard(&mut self, guard: AutoCloseWorker) {
3269 self.active_workers.push(guard);
3270 }
3271
3272 pub fn terminate_all_workers(&mut self) {
3297 for guard in &mut self.active_workers {
3300 guard.terminate_via(WorkerTeardownPath::PageUnload);
3301 guard.handle().unregister_stealth_profile();
3306 }
3307 self.worker_channels.clear();
3311 self.dedicated_worker_scopes.clear();
3314 self.worker_script_load_states.clear();
3317 self.web_workers.clear();
3325 for guard in &self.active_workers {
3331 guard.handle().mark_terminated();
3332 }
3333 }
3334
3335 pub fn reap_terminated_workers(&mut self) {
3344 self.active_workers.retain(|g| !g.handle().is_terminated());
3345 self.reap_terminated_worker_channels();
3346 self.reap_terminated_worker_script_load_states();
3347 let active_ids: std::collections::HashSet<WorkerId> = self
3350 .active_workers
3351 .iter()
3352 .map(|g| WorkerId(g.handle().script_url.clone()))
3353 .collect();
3354 self.dedicated_worker_scopes
3355 .retain(|id, _| active_ids.contains(id));
3356 self.web_workers.retain(|id, _| active_ids.contains(id));
3360 }
3361
3362 pub fn active_worker_count(&self) -> usize {
3366 self.active_workers
3367 .iter()
3368 .filter(|g| !g.handle().is_terminated())
3369 .count()
3370 }
3371
3372 pub fn terminate_worker_via_path(
3389 &mut self,
3390 worker_id: &WorkerId,
3391 path: WorkerTeardownPath,
3392 ) -> Option<WorkerTeardownResult> {
3393 let guard_idx = self
3395 .active_workers
3396 .iter()
3397 .position(|g| &WorkerId(g.handle().script_url.clone()) == worker_id)?;
3398
3399 let guard = &mut self.active_workers[guard_idx];
3400
3401 guard.terminate_via(path.clone());
3403
3404 let realm_unregistered = if guard.handle().worker_global_addr() != 0 {
3407 guard.handle().unregister_stealth_profile();
3408 true
3409 } else {
3410 false
3411 };
3412
3413 guard.handle().mark_terminated();
3415
3416 let thread_joined = if self.web_workers.contains_key(worker_id) {
3419 self.web_workers.remove(worker_id);
3423 true
3424 } else {
3425 true
3427 };
3428
3429 let never_registered = guard.handle().worker_global_addr() == 0;
3432
3433 self.worker_channels.remove(worker_id);
3435 self.dedicated_worker_scopes.remove(worker_id);
3436 self.worker_script_load_states.remove(worker_id);
3437
3438 Some(WorkerTeardownResult {
3439 path,
3440 thread_joined,
3441 realm_profile_unregistered: realm_unregistered,
3442 closing_flag_set: true,
3443 never_registered,
3444 })
3445 }
3446
3447 pub fn register_dedicated_worker_scope(
3454 &mut self,
3455 worker_id: WorkerId,
3456 scope: DedicatedWorkerGlobalScopeState,
3457 ) {
3458 self.dedicated_worker_scopes.insert(worker_id, scope);
3459 }
3460
3461 pub fn register_web_worker(&mut self, worker_id: WorkerId, handle: WorkerHandle) {
3470 self.web_workers.insert(worker_id, handle);
3471 }
3472
3473 pub fn web_worker(&self, worker_id: &WorkerId) -> Option<&WorkerHandle> {
3477 self.web_workers.get(worker_id)
3478 }
3479
3480 pub fn dedicated_worker_scope(
3484 &self,
3485 worker_id: &WorkerId,
3486 ) -> Option<&DedicatedWorkerGlobalScopeState> {
3487 self.dedicated_worker_scopes.get(worker_id)
3488 }
3489
3490 pub fn dedicated_worker_scope_mut(
3494 &mut self,
3495 worker_id: &WorkerId,
3496 ) -> Option<&mut DedicatedWorkerGlobalScopeState> {
3497 self.dedicated_worker_scopes.get_mut(worker_id)
3498 }
3499
3500 pub fn remove_dedicated_worker_scope(
3504 &mut self,
3505 worker_id: &WorkerId,
3506 ) -> Option<DedicatedWorkerGlobalScopeState> {
3507 self.dedicated_worker_scopes.remove(worker_id)
3508 }
3509
3510 pub fn dedicated_worker_scope_count(&self) -> usize {
3514 self.dedicated_worker_scopes.len()
3515 }
3516
3517 pub fn dedicated_worker_scopes(&self) -> Vec<&DedicatedWorkerGlobalScopeState> {
3524 self.dedicated_worker_scopes.values().collect()
3525 }
3526
3527 pub fn dedicated_worker_scope_by_url(
3532 &self,
3533 url: &str,
3534 ) -> Option<&DedicatedWorkerGlobalScopeState> {
3535 self.dedicated_worker_scopes
3536 .values()
3537 .find(|scope| scope.worker_id.0 == url)
3538 }
3539
3540 pub fn register_worker_script_load_state(
3549 &mut self,
3550 worker_id: WorkerId,
3551 state: WorkerScriptLoadState,
3552 ) {
3553 self.worker_script_load_states.insert(worker_id, state);
3554 }
3555
3556 pub fn update_worker_script_load_state(
3563 &mut self,
3564 worker_id: &WorkerId,
3565 state: WorkerScriptLoadState,
3566 ) {
3567 if let Some(current) = self.worker_script_load_states.get_mut(worker_id) {
3568 *current = state;
3569 }
3570 }
3571
3572 pub fn worker_script_load_state(&self, worker_id: &WorkerId) -> Option<&WorkerScriptLoadState> {
3576 self.worker_script_load_states.get(worker_id)
3577 }
3578
3579 pub fn remove_worker_script_load_state(
3583 &mut self,
3584 worker_id: &WorkerId,
3585 ) -> Option<WorkerScriptLoadState> {
3586 self.worker_script_load_states.remove(worker_id)
3587 }
3588
3589 pub fn worker_script_load_state_count(&self) -> usize {
3593 self.worker_script_load_states.len()
3594 }
3595
3596 fn reap_terminated_worker_script_load_states(&mut self) {
3603 let active_ids: std::collections::HashSet<WorkerId> = self
3604 .active_workers
3605 .iter()
3606 .map(|g| WorkerId(g.handle().script_url.clone()))
3607 .collect();
3608 self.worker_script_load_states
3609 .retain(|id, _| active_ids.contains(id));
3610 }
3611
3612 pub fn worker_lifecycle_states(&self) -> Vec<(WorkerId, WorkerLifecycleState)> {
3618 self.active_workers
3619 .iter()
3620 .map(|g| {
3621 let id = WorkerId(g.handle().script_url.clone());
3622 (id, g.lifecycle_state())
3623 })
3624 .collect()
3625 }
3626
3627 pub fn set_worker_scope_config(&mut self, config: WorkerScopeConfig) {
3634 self.worker_scope_config = config;
3635 }
3636
3637 pub fn forward_worker_message_event(&self, event: WorkerMessageEvent) {
3646 if let Some(ref tx) = self.event_tx {
3647 let direction = match event.direction {
3648 WorkerMessageDirection::PageToWorker => "page→worker",
3649 WorkerMessageDirection::WorkerToPage => "worker→page",
3650 };
3651 let _ = tx.send(ServoEvent::Console {
3652 target_id: "0".to_string(),
3653 level: ConsoleLevel::Debug,
3654 text: format!("[Worker] postMessage {}: {}", direction, event.worker_id.0),
3655 url: None,
3656 line: None,
3657 column: None,
3658 });
3659 }
3660 }
3661
3662 pub fn forward_worker_structured_message(&self, msg: &WorkerStructuredMessage) {
3670 if let Some(ref tx) = self.event_tx {
3671 let direction = match msg.direction {
3672 WorkerMessageDirection::PageToWorker => "page→worker",
3673 WorkerMessageDirection::WorkerToPage => "worker→page",
3674 };
3675 let payload_info = match &msg.payload {
3676 Some(p) => format!(
3677 "{} bytes, {} transferable(s)",
3678 p.data.len(),
3679 p.transferable_count
3680 ),
3681 None => "metadata-only (servo handles clone)".to_string(),
3682 };
3683 let _ = tx.send(ServoEvent::Console {
3684 target_id: "0".to_string(),
3685 level: ConsoleLevel::Debug,
3686 text: format!(
3687 "[Worker] postMessage #{} {}: {} [{}]",
3688 msg.message_id, direction, msg.worker_id.0, payload_info
3689 ),
3690 url: None,
3691 line: None,
3692 column: None,
3693 });
3694 }
3695 }
3696
3697 pub fn forward_worker_error_event(&self, event: WorkerErrorEvent) {
3706 if let Some(ref tx) = self.event_tx {
3707 let _ = tx.send(ServoEvent::PageError {
3708 target_id: "0".to_string(),
3709 text: format!("[Worker] {}: {}", event.worker_id.0, event.message),
3710 url: Some(event.filename.clone()),
3711 line: Some(event.lineno),
3712 column: Some(event.colno),
3713 stack: None,
3714 });
3715 }
3716 }
3717
3718 pub fn register_worker_channel(&mut self, bridge: WorkerChannelBridge) {
3728 let id = bridge.worker_id.clone();
3729 self.worker_channels.insert(id, bridge);
3730 }
3731
3732 pub fn create_worker_channel(&mut self, worker_id: WorkerId) -> WorkerChannelEndpoints {
3739 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
3740 self.worker_channels
3741 .insert(bridge.worker_id.clone(), bridge);
3742 endpoints
3743 }
3744
3745 pub fn remove_worker_channel(&mut self, worker_id: &WorkerId) -> Option<WorkerChannelBridge> {
3749 self.worker_channels.remove(worker_id)
3750 }
3751
3752 pub fn worker_channel(&self, worker_id: &WorkerId) -> Option<&WorkerChannelBridge> {
3756 self.worker_channels.get(worker_id)
3757 }
3758
3759 pub fn post_to_worker(
3766 &self,
3767 worker_id: &WorkerId,
3768 payload: StructuredClonePayload,
3769 ) -> Result<(), String> {
3770 match self.worker_channels.get(worker_id) {
3771 Some(bridge) => bridge
3772 .post_message_to_worker(payload)
3773 .map_err(|e| format!("Worker channel closed: {}", e)),
3774 None => Err(format!("No channel bridge for worker: {}", worker_id.0)),
3775 }
3776 }
3777
3778 pub fn drain_all_worker_messages(&self) -> (Vec<WorkerStructuredMessage>, Vec<WorkerId>) {
3788 let mut all_messages = Vec::new();
3789 let mut disconnected_workers = Vec::new();
3790 for (id, bridge) in &self.worker_channels {
3791 let result = bridge.drain_worker_messages();
3792 all_messages.extend(result.messages);
3793 if result.disconnected {
3794 disconnected_workers.push(id.clone());
3795 }
3796 }
3797 (all_messages, disconnected_workers)
3798 }
3799
3800 pub fn drain_and_forward_worker_messages(&self) -> Vec<WorkerId> {
3809 let (messages, disconnected) = self.drain_all_worker_messages();
3810 for msg in &messages {
3811 self.forward_worker_structured_message(msg);
3812 }
3813 disconnected
3814 }
3815
3816 pub fn reap_terminated_worker_channels(&mut self) {
3823 let active_ids: std::collections::HashSet<WorkerId> = self
3826 .active_workers
3827 .iter()
3828 .map(|g| WorkerId(g.handle().script_url.clone()))
3829 .collect();
3830 self.worker_channels.retain(|id, _| active_ids.contains(id));
3831 }
3832
3833 pub fn worker_channel_count(&self) -> usize {
3837 self.worker_channels.len()
3838 }
3839
3840 pub fn track_shared_worker_port(&mut self, port_ref: SharedWorkerPortRef) {
3851 self.shared_worker_ports.push(port_ref);
3852 }
3853
3854 pub fn disconnect_shared_worker_ports(&mut self) {
3868 if !self.shared_worker_ports.is_empty() {
3869 log::debug!(
3870 "[delegate] page navigation: disconnecting {} shared worker ports",
3871 self.shared_worker_ports.len()
3872 );
3873 }
3874 self.shared_worker_ports.clear();
3875 self.shared_worker_channels.clear();
3880 self.shared_worker_scopes.clear();
3883 }
3884
3885 pub fn shared_worker_port_count(&self) -> usize {
3889 self.shared_worker_ports.len()
3890 }
3891
3892 pub fn forward_shared_worker_connect_event(&self, event: SharedWorkerConnectEvent) {
3900 if let Some(ref tx) = self.event_tx {
3901 let _ = tx.send(ServoEvent::Console {
3902 target_id: "0".to_string(),
3903 level: ConsoleLevel::Debug,
3904 text: format!(
3905 "[SharedWorker] connect: {} (name={}) from {}",
3906 event.shared_worker_id.script_url,
3907 if event.shared_worker_id.name.is_empty() {
3908 "<default>"
3909 } else {
3910 &event.shared_worker_id.name
3911 },
3912 event.page_url
3913 ),
3914 url: None,
3915 line: None,
3916 column: None,
3917 });
3918 }
3919 }
3920
3921 pub fn register_shared_worker_channel(&mut self, bridge: SharedWorkerChannelBridge) {
3931 let id = bridge.shared_worker_id.clone();
3932 self.shared_worker_channels.insert(id, bridge);
3933 }
3934
3935 pub fn create_shared_worker_channel(&mut self, shared_worker_id: SharedWorkerId) {
3942 let bridge = SharedWorkerChannelBridge::new(shared_worker_id.clone());
3943 self.shared_worker_channels.insert(shared_worker_id, bridge);
3944 }
3945
3946 pub fn add_shared_worker_port(
3954 &mut self,
3955 shared_worker_id: SharedWorkerId,
3956 ) -> SharedWorkerPortEndpoints {
3957 if !self.shared_worker_channels.contains_key(&shared_worker_id) {
3958 self.create_shared_worker_channel(shared_worker_id.clone());
3959 }
3960 self.shared_worker_channels
3961 .get_mut(&shared_worker_id)
3962 .expect("just created")
3963 .add_port()
3964 }
3965
3966 pub fn shared_worker_channel(&self, id: &SharedWorkerId) -> Option<&SharedWorkerChannelBridge> {
3970 self.shared_worker_channels.get(id)
3971 }
3972
3973 pub fn remove_shared_worker_channel(
3977 &mut self,
3978 id: &SharedWorkerId,
3979 ) -> Option<SharedWorkerChannelBridge> {
3980 self.shared_worker_channels.remove(id)
3981 }
3982
3983 pub fn drain_all_shared_worker_messages(
3993 &self,
3994 ) -> (Vec<WorkerStructuredMessage>, Vec<SharedWorkerId>) {
3995 let mut all_messages = Vec::new();
3996 let mut all_disconnected = Vec::new();
3997 for (_, bridge) in &self.shared_worker_channels {
3998 let (messages, disconnected) = bridge.drain_all_worker_messages();
3999 all_messages.extend(messages);
4000 all_disconnected.extend(disconnected);
4001 }
4002 (all_messages, all_disconnected)
4003 }
4004
4005 pub fn drain_and_forward_shared_worker_messages(&self) -> Vec<SharedWorkerId> {
4012 let (messages, disconnected) = self.drain_all_shared_worker_messages();
4013 for msg in &messages {
4014 self.forward_worker_structured_message(&msg);
4015 }
4016 disconnected
4017 }
4018
4019 pub fn post_to_worker_via_shared_port(
4026 &self,
4027 id: &SharedWorkerId,
4028 port_index: usize,
4029 payload: StructuredClonePayload,
4030 ) -> Result<(), String> {
4031 match self.shared_worker_channels.get(id) {
4032 Some(bridge) => bridge.post_to_worker_from_port(port_index, payload),
4033 None => Err(format!(
4034 "No channel bridge for SharedWorker: {}:{}",
4035 id.script_url, id.name
4036 )),
4037 }
4038 }
4039
4040 pub fn reap_disconnected_shared_worker_ports(&mut self) {
4044 for (_, bridge) in &mut self.shared_worker_channels {
4045 bridge.remove_disconnected_ports();
4046 }
4047 self.shared_worker_channels
4049 .retain(|_, bridge| bridge.port_count() > 0);
4050 }
4051
4052 pub fn shared_worker_channel_count(&self) -> usize {
4056 self.shared_worker_channels
4057 .values()
4058 .map(|b| b.port_count())
4059 .sum()
4060 }
4061
4062 pub fn register_shared_worker_scope(
4069 &mut self,
4070 id: SharedWorkerId,
4071 scope: SharedWorkerGlobalScopeState,
4072 ) {
4073 self.shared_worker_scopes.insert(id, scope);
4074 }
4075
4076 pub fn shared_worker_scope(
4080 &self,
4081 id: &SharedWorkerId,
4082 ) -> Option<&SharedWorkerGlobalScopeState> {
4083 self.shared_worker_scopes.get(id)
4084 }
4085
4086 pub fn shared_worker_scope_mut(
4090 &mut self,
4091 id: &SharedWorkerId,
4092 ) -> Option<&mut SharedWorkerGlobalScopeState> {
4093 self.shared_worker_scopes.get_mut(id)
4094 }
4095
4096 pub fn remove_shared_worker_scope(
4100 &mut self,
4101 id: &SharedWorkerId,
4102 ) -> Option<SharedWorkerGlobalScopeState> {
4103 self.shared_worker_scopes.remove(id)
4104 }
4105
4106 pub fn shared_worker_scope_count(&self) -> usize {
4110 self.shared_worker_scopes.len()
4111 }
4112
4113 pub fn shared_worker_scopes(&self) -> Vec<&SharedWorkerGlobalScopeState> {
4120 self.shared_worker_scopes.values().collect()
4121 }
4122
4123 pub fn shared_worker_scope_by_script_url(
4128 &self,
4129 script_url: &str,
4130 ) -> Option<&SharedWorkerGlobalScopeState> {
4131 self.shared_worker_scopes
4132 .values()
4133 .find(|scope| scope.shared_worker_id.script_url == script_url)
4134 }
4135
4136 pub fn set_shared_worker_scope_config(
4143 &mut self,
4144 shared_worker_id: &SharedWorkerId,
4145 config: &SharedWorkerScopeConfig,
4146 ) {
4147 if let Some(scope) = self.shared_worker_scopes.get_mut(shared_worker_id) {
4148 scope.scope.navigator = WorkerNavigator::from_shared_scope_config(config);
4149 }
4150 }
4151
4152 pub fn set_controlling_service_worker(&mut self, handle: ServiceWorkerHandle) {
4162 self.controlled_service_worker = Some(handle);
4163 }
4164
4165 pub fn clear_controlling_service_worker(&mut self) {
4174 self.controlled_service_worker = None;
4175 self.service_worker_scope = None;
4176 }
4177
4178 pub fn controlling_service_worker(&self) -> Option<&ServiceWorkerHandle> {
4182 self.controlled_service_worker.as_ref()
4183 }
4184
4185 pub fn is_controlled_by_service_worker(&self) -> bool {
4189 self.controlled_service_worker.is_some()
4190 }
4191
4192 pub fn is_url_in_service_worker_scope(&self, url: &str) -> bool {
4199 self.service_worker_scope
4200 .as_ref()
4201 .map(|scope| scope.is_url_in_scope(url))
4202 .unwrap_or(false)
4203 }
4204
4205 pub fn register_service_worker_scope(&mut self, scope: ServiceWorkerGlobalScopeState) {
4209 self.service_worker_scope = Some(scope);
4210 }
4211
4212 pub fn service_worker_scope(&self) -> Option<&ServiceWorkerGlobalScopeState> {
4216 self.service_worker_scope.as_ref()
4217 }
4218
4219 pub fn service_worker_scope_mut(&mut self) -> Option<&mut ServiceWorkerGlobalScopeState> {
4223 self.service_worker_scope.as_mut()
4224 }
4225
4226 pub fn remove_service_worker_scope(&mut self) -> Option<ServiceWorkerGlobalScopeState> {
4230 self.service_worker_scope.take()
4231 }
4232
4233 pub fn forward_service_worker_fetch_event(&self, event: ServiceWorkerFetchEvent) {
4241 if let Some(ref tx) = self.event_tx {
4242 let stealth_status = if event.stealth_profile_applied {
4243 "stealth profile applied"
4244 } else {
4245 "⚠️ STEALTH BOUNDARY VIOLATION"
4246 };
4247 let _ = tx.send(ServoEvent::Console {
4248 target_id: "0".to_string(),
4249 level: if event.stealth_profile_applied {
4250 ConsoleLevel::Debug
4251 } else {
4252 ConsoleLevel::Warning
4253 },
4254 text: format!(
4255 "[ServiceWorker] fetch {} {} -> {} ({})",
4256 event.method,
4257 event.request_url,
4258 event.registration_id.script_url,
4259 stealth_status
4260 ),
4261 url: None,
4262 line: None,
4263 column: None,
4264 });
4265 }
4266 }
4267
4268 pub fn set_service_worker_scope_config(&mut self, config: &ServiceWorkerScopeConfig) {
4275 if let Some(scope) = &mut self.service_worker_scope {
4276 scope.scope.navigator = WorkerNavigator::from_service_scope_config(config);
4277 }
4278 }
4279}
4280
4281pub struct BaoServoDelegate {
4282 last_error: RefCell<Option<String>>,
4283 console_log_tx: RefCell<Option<std::sync::mpsc::Sender<ConsoleMessage>>>,
4286 event_tx: RefCell<Option<Sender<ServoEvent>>>,
4290 shared_workers: RefCell<Vec<SharedWorkerHandle>>,
4297 service_workers: RefCell<Vec<ServiceWorkerHandle>>,
4306}
4307
4308impl Default for BaoServoDelegate {
4309 fn default() -> Self {
4310 BaoServoDelegate {
4311 last_error: RefCell::new(None),
4312 console_log_tx: RefCell::new(None),
4313 event_tx: RefCell::new(None),
4314 shared_workers: RefCell::new(Vec::new()),
4315 service_workers: RefCell::new(Vec::new()),
4316 }
4317 }
4318}
4319
4320impl BaoServoDelegate {
4321 pub fn new() -> Self {
4322 Self::default()
4323 }
4324
4325 pub fn last_error(&self) -> Option<String> {
4326 self.last_error.borrow().clone()
4327 }
4328
4329 pub fn set_console_log_tx(&self, tx: std::sync::mpsc::Sender<ConsoleMessage>) {
4332 *self.console_log_tx.borrow_mut() = Some(tx);
4333 }
4334
4335 pub fn console_log_tx(&self) -> Option<std::sync::mpsc::Sender<ConsoleMessage>> {
4338 self.console_log_tx.borrow().clone()
4339 }
4340
4341 pub fn set_event_tx(&self, tx: Sender<ServoEvent>) {
4345 *self.event_tx.borrow_mut() = Some(tx);
4346 }
4347
4348 pub fn event_tx(&self) -> Option<Sender<ServoEvent>> {
4352 self.event_tx.borrow().clone()
4353 }
4354
4355 pub fn register_shared_worker(&self, handle: SharedWorkerHandle) -> SharedWorkerHandle {
4366 let id = handle.id();
4367 let mut shared_workers = self.shared_workers.borrow_mut();
4368 if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
4369 existing.clone()
4370 } else {
4371 shared_workers.push(handle.clone());
4372 handle
4373 }
4374 }
4375
4376 pub fn find_shared_worker(&self, script_url: &str, name: &str) -> Option<SharedWorkerHandle> {
4385 self.shared_workers
4386 .borrow()
4387 .iter()
4388 .find(|h| h.script_url == script_url && h.name == name)
4389 .cloned()
4390 }
4391
4392 pub fn reap_terminated_shared_workers(&self) {
4399 self.shared_workers
4400 .borrow_mut()
4401 .retain(|h| !h.is_terminated() || h.connected_page_count() > 0);
4402 }
4403
4404 pub fn shared_worker_count(&self) -> usize {
4408 self.shared_workers.borrow().len()
4409 }
4410
4411 pub fn route_shared_worker(&self, handle: SharedWorkerHandle) -> (SharedWorkerHandle, bool) {
4423 let id = handle.id();
4424 let mut shared_workers = self.shared_workers.borrow_mut();
4425 if let Some(existing) = shared_workers.iter().find(|h| h.id() == id) {
4426 (existing.clone(), false)
4427 } else {
4428 shared_workers.push(handle.clone());
4429 (handle, true)
4430 }
4431 }
4432
4433 pub fn get_or_create_shared_worker(
4440 &self,
4441 script_url: &str,
4442 name: &str,
4443 ) -> (SharedWorkerHandle, bool) {
4444 if let Some(existing) = self.find_shared_worker(script_url, name) {
4445 (existing, false)
4446 } else {
4447 let handle = SharedWorkerHandle::new(script_url.to_string(), name.to_string());
4448 let returned = self.register_shared_worker(handle);
4449 (returned, true)
4450 }
4451 }
4452
4453 pub fn unregister_shared_worker(&self, id: &SharedWorkerId) -> bool {
4460 let mut shared_workers = self.shared_workers.borrow_mut();
4461 let before = shared_workers.len();
4462 shared_workers.retain(|h| &h.id() != id);
4463 shared_workers.len() < before
4464 }
4465
4466 pub fn all_shared_workers(&self) -> Vec<SharedWorkerHandle> {
4472 self.shared_workers.borrow().iter().cloned().collect()
4473 }
4474
4475 pub fn register_service_worker(&self, handle: ServiceWorkerHandle) -> ServiceWorkerHandle {
4488 let id = handle.id();
4489 let mut service_workers = self.service_workers.borrow_mut();
4490 if let Some(existing) = service_workers.iter().find(|h| h.id() == id) {
4491 existing.clone()
4492 } else {
4493 service_workers.push(handle.clone());
4494 handle
4495 }
4496 }
4497
4498 pub fn find_service_worker(
4504 &self,
4505 script_url: &str,
4506 scope: &str,
4507 ) -> Option<ServiceWorkerHandle> {
4508 self.service_workers
4509 .borrow()
4510 .iter()
4511 .find(|h| h.script_url == script_url && h.scope == scope)
4512 .cloned()
4513 }
4514
4515 pub fn find_service_worker_for_url(&self, url: &str) -> Option<ServiceWorkerHandle> {
4523 self.service_workers
4524 .borrow()
4525 .iter()
4526 .filter(|h| h.is_intercepting_fetch())
4527 .find(|h| url.starts_with(&h.scope))
4528 .cloned()
4529 }
4530
4531 pub fn reap_terminated_service_workers(&self) {
4538 self.service_workers
4539 .borrow_mut()
4540 .retain(|h| !h.is_terminated());
4541 }
4542
4543 pub fn service_worker_count(&self) -> usize {
4547 self.service_workers.borrow().len()
4548 }
4549
4550 pub fn unregister_service_worker(&self, id: &ServiceWorkerRegistrationId) -> bool {
4558 let mut service_workers = self.service_workers.borrow_mut();
4559 let before = service_workers.len();
4560 service_workers.retain(|h| &h.id() != id);
4561 service_workers.len() < before
4562 }
4563
4564 pub fn get_or_create_service_worker(
4571 &self,
4572 script_url: &str,
4573 scope: &str,
4574 stealth_profile: Option<bao_stealth::StealthProfile>,
4575 ) -> (ServiceWorkerHandle, bool) {
4576 if let Some(existing) = self.find_service_worker(script_url, scope) {
4577 (existing, false)
4578 } else {
4579 let handle = ServiceWorkerHandle::new(
4580 script_url.to_string(),
4581 scope.to_string(),
4582 stealth_profile,
4583 );
4584 let returned = self.register_service_worker(handle);
4585 (returned, true)
4586 }
4587 }
4588
4589 pub fn all_service_workers(&self) -> Vec<ServiceWorkerHandle> {
4595 self.service_workers.borrow().iter().cloned().collect()
4596 }
4597
4598 pub fn verify_service_worker_stealth_consistency(
4610 &self,
4611 page_stealth_profile: &bao_stealth::StealthProfile,
4612 ) -> Vec<ServiceWorkerRegistrationId> {
4613 self.service_workers
4614 .borrow()
4615 .iter()
4616 .filter(|h| h.is_intercepting_fetch())
4617 .filter(|h| {
4618 match &h.stealth_profile {
4622 Some(sw_profile) => {
4623 sw_profile.navigator.user_agent != page_stealth_profile.navigator.user_agent
4626 || sw_profile.navigator.platform
4627 != page_stealth_profile.navigator.platform
4628 }
4629 None => {
4630 true
4633 }
4634 }
4635 })
4636 .map(|h| h.id())
4637 .collect()
4638 }
4639}
4640
4641impl ServoDelegate for BaoServoDelegate {
4642 fn notify_error(&self, error: ServoError) {
4643 let error_str = format!("{error:?}");
4644 *self.last_error.borrow_mut() = Some(error_str.clone());
4645 if error_str.to_lowercase().contains("certificate")
4650 || error_str.to_lowercase().contains("tls")
4651 {
4652 if let Some(ref tx) = *self.console_log_tx.borrow() {
4653 let _ = tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
4654 event_id: 0,
4655 error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
4656 url: String::new(),
4657 }));
4658 }
4659 }
4660 }
4661
4662 fn show_console_message(&self, level: ConsoleLogLevel, message: String) {
4663 let level_str = match level {
4664 ConsoleLogLevel::Debug => "debug",
4665 ConsoleLogLevel::Log => "info",
4666 ConsoleLogLevel::Info => "info",
4667 ConsoleLogLevel::Warn => "warning",
4668 ConsoleLogLevel::Error => "error",
4669 ConsoleLogLevel::Trace => "verbose",
4670 ConsoleLogLevel::Dir => "info",
4671 };
4672 log::trace!("[servo] {message}");
4673
4674 let event_tx = self.event_tx.borrow();
4679 if let Some(ref tx) = *event_tx {
4680 let servo_level = match level {
4681 ConsoleLogLevel::Debug => ConsoleLevel::Debug,
4682 ConsoleLogLevel::Log => ConsoleLevel::Info,
4683 ConsoleLogLevel::Info => ConsoleLevel::Info,
4684 ConsoleLogLevel::Warn => ConsoleLevel::Warning,
4685 ConsoleLogLevel::Error => ConsoleLevel::Error,
4686 ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
4687 ConsoleLogLevel::Dir => ConsoleLevel::Info,
4688 };
4689 let _ = tx.send(ServoEvent::Console {
4690 target_id: "0".to_string(),
4691 level: servo_level,
4692 text: message,
4693 url: None,
4694 line: None,
4695 column: None,
4696 });
4697 } else if let Some(ref tx) = *self.console_log_tx.borrow() {
4698 let msg = match BaoEvent::from_console_text(&message) {
4699 Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
4700 _ => ConsoleMessage::Log {
4701 level: level_str.to_string(),
4702 text: message,
4703 },
4704 };
4705 let _ = tx.send(msg);
4706 }
4707 }
4708
4709 fn request_devtools_connection(&self, request: AllowOrDenyRequest) {
4710 request.allow();
4711 }
4712}
4713
4714pub struct BaoWebViewDelegate {
4715 state: Rc<RefCell<BaoWebViewState>>,
4716 viewport: PhysicalSize<u32>,
4717}
4718
4719impl BaoWebViewDelegate {
4720 pub fn new(state: Rc<RefCell<BaoWebViewState>>, viewport: PhysicalSize<u32>) -> Self {
4721 BaoWebViewDelegate { state, viewport }
4722 }
4723
4724 pub fn state(&self) -> &Rc<RefCell<BaoWebViewState>> {
4725 &self.state
4726 }
4727}
4728
4729impl WebViewDelegate for BaoWebViewDelegate {
4730 fn screen_geometry(&self, _webview: WebView) -> Option<ScreenGeometry> {
4731 let screen_size =
4732 DeviceIntSize::new(self.viewport.width as i32, self.viewport.height as i32);
4733 Some(ScreenGeometry {
4734 size: screen_size,
4735 available_size: screen_size,
4736 window_rect: DeviceIntRect::from_origin_and_size(DeviceIntPoint::zero(), screen_size),
4737 })
4738 }
4739
4740 fn notify_url_changed(&self, _webview: WebView, url: url::Url) {
4741 let url_str = url.to_string();
4742 self.state.borrow_mut().url = Some(url);
4743 let event_tx = self.state.borrow().event_tx.clone();
4747 if let Some(ref tx) = event_tx {
4748 let _ = tx.send(ServoEvent::FrameNavigated {
4749 target_id: "0".to_string(),
4750 frame_id: "0".to_string(),
4751 url: url_str,
4752 name: None,
4753 });
4754 } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4755 let loader_id = format!("{:016x}", url_str.len() as u64);
4756 let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
4757 frame_id: "0".to_string(),
4758 url: url_str,
4759 loader_id,
4760 }));
4761 }
4762 }
4763
4764 fn notify_page_title_changed(&self, _webview: WebView, title: Option<String>) {
4765 self.state.borrow_mut().title = title;
4766 }
4767
4768 fn notify_load_status_changed(&self, _webview: WebView, status: LoadStatus) {
4769 self.state.borrow_mut().load_status = status;
4770 match status {
4771 LoadStatus::Started => {
4772 {
4778 let mut state = self.state.borrow_mut();
4779 if !state.active_workers.is_empty() {
4780 log::debug!(
4781 "[delegate] page navigation: terminating {} active workers",
4782 state.active_worker_count()
4783 );
4784 state.terminate_all_workers();
4785 }
4786 state.disconnect_shared_worker_ports();
4789 state.clear_controlling_service_worker();
4795 }
4796
4797 let event_tx = self.state.borrow().event_tx.clone();
4802 if let Some(ref tx) = event_tx {
4803 let _ = tx.send(ServoEvent::FrameStartedLoading {
4804 target_id: "0".to_string(),
4805 frame_id: "0".to_string(),
4806 });
4807 }
4808 }
4809 LoadStatus::Complete => {
4810 self.state.borrow_mut().dom_proxies_dirty = true;
4811
4812 self.state.borrow_mut().reap_terminated_workers();
4817
4818 let event_tx = self.state.borrow().event_tx.clone();
4822 if let Some(ref tx) = event_tx {
4823 let _ = tx.send(ServoEvent::FrameStoppedLoading {
4824 target_id: "0".to_string(),
4825 frame_id: "0".to_string(),
4826 });
4827 } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4828 let timestamp = std::time::SystemTime::now()
4829 .duration_since(std::time::UNIX_EPOCH)
4830 .unwrap_or_default()
4831 .as_secs_f64();
4832 let _ = tx.send(ConsoleMessage::Event(BaoEvent::PageLoadEventFired {
4833 timestamp,
4834 }));
4835 }
4836 }
4837 LoadStatus::HeadParsed => {}
4838 }
4839 }
4840
4841 fn notify_new_frame_ready(&self, _webview: WebView) {
4842 self.state.borrow_mut().frame_ready = true;
4843 }
4844
4845 fn request_navigation(&self, _webview: WebView, request: NavigationRequest) {
4846 request.allow();
4847 }
4848
4849 fn request_permission(&self, _webview: WebView, request: PermissionRequest) {
4850 request.allow();
4851 }
4852
4853 fn request_create_new(&self, _parent_webview: WebView, _request: CreateNewWebViewRequest) {}
4854
4855 fn show_console_message(&self, _webview: WebView, level: ConsoleLogLevel, message: String) {
4856 let level_str = match level {
4857 ConsoleLogLevel::Debug => "debug",
4858 ConsoleLogLevel::Log => "info",
4859 ConsoleLogLevel::Info => "info",
4860 ConsoleLogLevel::Warn => "warning",
4861 ConsoleLogLevel::Error => "error",
4862 ConsoleLogLevel::Trace => "verbose",
4863 ConsoleLogLevel::Dir => "info",
4864 };
4865 log::trace!("[webview] {message}");
4866
4867 let event_tx = self.state.borrow().event_tx.clone();
4871 if let Some(ref tx) = event_tx {
4872 let servo_level = match level {
4873 ConsoleLogLevel::Debug => ConsoleLevel::Debug,
4874 ConsoleLogLevel::Log => ConsoleLevel::Info,
4875 ConsoleLogLevel::Info => ConsoleLevel::Info,
4876 ConsoleLogLevel::Warn => ConsoleLevel::Warning,
4877 ConsoleLogLevel::Error => ConsoleLevel::Error,
4878 ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
4879 ConsoleLogLevel::Dir => ConsoleLevel::Info,
4880 };
4881 let _ = tx.send(ServoEvent::Console {
4882 target_id: "0".to_string(),
4883 level: servo_level,
4884 text: message,
4885 url: None,
4886 line: None,
4887 column: None,
4888 });
4889 } else if let Some(ref tx) = self.state.borrow().console_log_tx {
4890 let msg = match BaoEvent::from_console_text(&message) {
4891 Some(ConsoleMessage::Event(evt)) => ConsoleMessage::Event(evt),
4892 _ => ConsoleMessage::Log {
4893 level: level_str.to_string(),
4894 text: message,
4895 },
4896 };
4897 let _ = tx.send(msg);
4898 }
4899 }
4900
4901 fn show_embedder_control(&self, _webview: WebView, _control: EmbedderControl) {}
4902
4903 fn hide_embedder_control(&self, _webview: WebView, _id: EmbedderControlId) {}
4904
4905 fn notify_crashed(&self, _webview: WebView, reason: String, _backtrace: Option<String>) {
4906 log::error!("[webview] crashed: {reason}");
4907 }
4908}
4909
4910#[cfg(test)]
4911mod tests {
4912 use super::*;
4913
4914 #[test]
4918 fn test_webview_state_default() {
4919 let state = BaoWebViewState::default();
4920 assert!(state.url.is_none());
4921 assert!(state.title.is_none());
4922 assert!(matches!(state.load_status, LoadStatus::Started));
4923 assert!(!state.frame_ready);
4924 assert!(!state.dom_proxies_dirty);
4925 }
4926
4927 #[test]
4928 fn test_webview_state_url_mutate() {
4929 let mut state = BaoWebViewState::default();
4930 state.url = Some(url::Url::parse("https://example.com").unwrap());
4931 assert!(state.url.is_some());
4932 assert_eq!(state.url.unwrap().as_str(), "https://example.com/");
4933 }
4934
4935 #[test]
4936 fn test_webview_state_title_mutate() {
4937 let mut state = BaoWebViewState::default();
4938 state.title = Some("Test Page".to_string());
4939 assert_eq!(state.title.as_deref(), Some("Test Page"));
4940 }
4941
4942 #[test]
4943 fn test_webview_state_frame_ready_toggle() {
4944 let mut state = BaoWebViewState::default();
4945 assert!(!state.frame_ready);
4946 state.frame_ready = true;
4947 assert!(state.frame_ready);
4948 }
4949
4950 #[test]
4954 fn test_servo_delegate_new_no_error() {
4955 let delegate = BaoServoDelegate::new();
4956 assert!(delegate.last_error().is_none());
4957 }
4958
4959 #[test]
4960 fn test_servo_delegate_default_no_error() {
4961 let delegate = BaoServoDelegate::default();
4962 assert!(delegate.last_error().is_none());
4963 }
4964
4965 #[test]
4969 fn test_webview_delegate_new_with_state() {
4970 let state = Rc::new(RefCell::new(BaoWebViewState::default()));
4971 let viewport = PhysicalSize::new(1024, 768);
4972 let delegate = BaoWebViewDelegate::new(state, viewport);
4973 assert!(delegate.state().borrow().url.is_none());
4974 }
4975
4976 #[test]
4977 fn test_webview_delegate_state_rc_shared() {
4978 let state = Rc::new(RefCell::new(BaoWebViewState::default()));
4979 let viewport = PhysicalSize::new(800, 600);
4980 let delegate = BaoWebViewDelegate::new(Rc::clone(&state), viewport);
4981 state.borrow_mut().title = Some("External".to_string());
4983 assert_eq!(delegate.state().borrow().title.as_deref(), Some("External"));
4985 }
4986
4987 #[test]
4988 fn test_webview_delegate_viewport_size() {
4989 let state = Rc::new(RefCell::new(BaoWebViewState::default()));
4990 let viewport = PhysicalSize::new(1440, 900);
4991 let delegate = BaoWebViewDelegate::new(state, viewport);
4992 assert!(delegate.state().borrow().url.is_none());
4994 }
4995
4996 #[test]
5000 fn test_pool_stats_fields() {
5001 let stats = crate::page_pool::PoolStats {
5002 active: 3,
5003 idle: 1,
5004 total_created: 5,
5005 total_destroyed: 2,
5006 };
5007 assert_eq!(stats.active, 3);
5008 assert_eq!(stats.idle, 1);
5009 assert_eq!(stats.total_created, 5);
5010 assert_eq!(stats.total_destroyed, 2);
5011 }
5012
5013 #[test]
5017 fn test_dom_proxies_dirty_default_false() {
5018 let state = BaoWebViewState::default();
5019 assert!(!state.dom_proxies_dirty);
5020 }
5021
5022 #[test]
5023 fn test_dom_proxies_dirty_set_on_complete() {
5024 let mut state = BaoWebViewState::default();
5025 state.load_status = LoadStatus::Complete;
5026 state.dom_proxies_dirty = true;
5027 assert!(state.dom_proxies_dirty);
5028 }
5029
5030 #[test]
5031 fn test_dom_proxies_dirty_clear_after_refresh() {
5032 let mut state = BaoWebViewState::default();
5033 state.dom_proxies_dirty = true;
5034 state.dom_proxies_dirty = false;
5035 assert!(!state.dom_proxies_dirty);
5036 }
5037
5038 #[test]
5042 fn test_servo_delegate_console_log_channel_set_and_get() {
5043 let delegate = BaoServoDelegate::new();
5044 assert!(delegate.console_log_tx().is_none());
5045 let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5046 delegate.set_console_log_tx(tx);
5047 assert!(delegate.console_log_tx().is_some());
5048 }
5049
5050 #[test]
5051 fn test_servo_delegate_console_log_tx_clones() {
5052 let delegate = BaoServoDelegate::new();
5053 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5054 delegate.set_console_log_tx(tx);
5055 let cloned = delegate.console_log_tx().unwrap();
5057 cloned
5058 .send(ConsoleMessage::Log {
5059 level: "info".into(),
5060 text: "hello".into(),
5061 })
5062 .unwrap();
5063 let msg = rx.try_recv().unwrap();
5064 match msg {
5065 ConsoleMessage::Log { level, text } => {
5066 assert_eq!(level, "info");
5067 assert_eq!(text, "hello");
5068 }
5069 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5070 }
5071 }
5072
5073 #[test]
5074 fn test_webview_state_console_log_tx_propagation() {
5075 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5076 let mut state = BaoWebViewState::default();
5077 state.console_log_tx = Some(tx);
5078 if let Some(ref tx) = state.console_log_tx {
5080 tx.send(ConsoleMessage::Log {
5081 level: "warning".into(),
5082 text: "test message".into(),
5083 })
5084 .unwrap();
5085 }
5086 let msg = rx.try_recv().unwrap();
5087 match msg {
5088 ConsoleMessage::Log { level, text } => {
5089 assert_eq!(level, "warning");
5090 assert_eq!(text, "test message");
5091 }
5092 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5093 }
5094 }
5095
5096 #[test]
5097 fn test_webview_state_console_log_tx_default_none() {
5098 let state = BaoWebViewState::default();
5099 assert!(state.console_log_tx.is_none());
5100 }
5101
5102 #[test]
5103 fn test_console_log_all_level_mappings() {
5104 let delegate = BaoServoDelegate::new();
5105 let (tx, _rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5106 delegate.set_console_log_tx(tx);
5107
5108 let cases: Vec<(ConsoleLogLevel, &str)> = vec![
5111 (ConsoleLogLevel::Debug, "debug"),
5112 (ConsoleLogLevel::Log, "info"),
5113 (ConsoleLogLevel::Info, "info"),
5114 (ConsoleLogLevel::Warn, "warning"),
5115 (ConsoleLogLevel::Error, "error"),
5116 (ConsoleLogLevel::Trace, "verbose"),
5117 (ConsoleLogLevel::Dir, "info"),
5118 ];
5119 for (level, expected_str) in cases {
5120 let mapped = match level {
5121 ConsoleLogLevel::Debug => "debug",
5122 ConsoleLogLevel::Log => "info",
5123 ConsoleLogLevel::Info => "info",
5124 ConsoleLogLevel::Warn => "warning",
5125 ConsoleLogLevel::Error => "error",
5126 ConsoleLogLevel::Trace => "verbose",
5127 ConsoleLogLevel::Dir => "info",
5128 };
5129 assert_eq!(
5130 mapped, expected_str,
5131 "level {:?} should map to {}",
5132 level, expected_str
5133 );
5134 }
5135 }
5136
5137 #[test]
5138 fn test_webview_delegate_console_log_forwarding() {
5139 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5140 let state = Rc::new(RefCell::new(BaoWebViewState {
5141 console_log_tx: Some(tx),
5142 ..Default::default()
5143 }));
5144 let viewport = PhysicalSize::new(800, 600);
5145 let _delegate = BaoWebViewDelegate::new(state, viewport);
5146
5147 if let Some(ref tx) = _delegate.state().borrow().console_log_tx {
5149 tx.send(ConsoleMessage::Log {
5150 level: "error".into(),
5151 text: "crash!".into(),
5152 })
5153 .unwrap();
5154 }
5155 let msg = rx.try_recv().unwrap();
5156 match msg {
5157 ConsoleMessage::Log { level, text } => {
5158 assert_eq!(level, "error");
5159 assert_eq!(text, "crash!");
5160 }
5161 ConsoleMessage::Event(_) => panic!("expected Log, got Event"),
5162 }
5163 }
5164
5165 #[test]
5169 fn test_notify_url_changed_emits_frame_navigated() {
5170 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5171 let state = Rc::new(RefCell::new(BaoWebViewState {
5172 console_log_tx: Some(tx),
5173 ..Default::default()
5174 }));
5175 let viewport = PhysicalSize::new(800, 600);
5176 let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
5177
5178 let url = url::Url::parse("https://example.com").unwrap();
5180 let url_str = url.to_string();
5181 let loader_id = format!("{:016x}", url_str.len() as u64);
5182 if let Some(ref tx) = state.borrow().console_log_tx {
5183 tx.send(ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
5184 frame_id: "0".to_string(),
5185 url: url_str.clone(),
5186 loader_id: loader_id.clone(),
5187 }))
5188 .unwrap();
5189 }
5190
5191 let msg = rx.try_recv().unwrap();
5192 match msg {
5193 ConsoleMessage::Event(BaoEvent::PageFrameNavigated {
5194 frame_id,
5195 url,
5196 loader_id: lid,
5197 }) => {
5198 assert_eq!(frame_id, "0");
5199 assert!(url.starts_with("https://example.com"));
5200 assert_eq!(lid, loader_id);
5201 }
5202 other => panic!("expected PageFrameNavigated, got {:?}", other),
5203 }
5204 }
5205
5206 #[test]
5210 fn test_notify_error_certificate_error_emits_security_event() {
5211 let delegate = BaoServoDelegate::new();
5212 let (tx, rx) = std::sync::mpsc::channel::<ConsoleMessage>();
5213 delegate.set_console_log_tx(tx);
5214
5215 if let Some(ref tx) = *delegate.console_log_tx.borrow() {
5217 tx.send(ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
5218 event_id: 0,
5219 error_type: "net::ERR_CERT_AUTHORITY_INVALID".to_string(),
5220 url: String::new(),
5221 }))
5222 .unwrap();
5223 }
5224
5225 let msg = rx.try_recv().unwrap();
5226 match msg {
5227 ConsoleMessage::Event(BaoEvent::SecurityCertificateError {
5228 event_id,
5229 error_type,
5230 url,
5231 }) => {
5232 assert_eq!(event_id, 0);
5233 assert_eq!(error_type, "net::ERR_CERT_AUTHORITY_INVALID");
5234 assert_eq!(url, "");
5235 }
5236 other => panic!("expected SecurityCertificateError, got {:?}", other),
5237 }
5238 }
5239
5240 #[test]
5244 fn test_servo_delegate_event_tx_set_and_get() {
5245 let delegate = BaoServoDelegate::new();
5246 assert!(delegate.event_tx().is_none());
5247 let (tx, _rx) = std::sync::mpsc::channel::<ServoEvent>();
5248 delegate.set_event_tx(tx);
5249 assert!(delegate.event_tx().is_some());
5250 }
5251
5252 #[test]
5253 fn test_servo_delegate_event_tx_sends_console_event() {
5254 let delegate = BaoServoDelegate::new();
5255 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5256 delegate.set_event_tx(tx);
5257
5258 if let Some(ref tx) = delegate.event_tx() {
5260 tx.send(ServoEvent::Console {
5261 target_id: "0".to_string(),
5262 level: ConsoleLevel::Info,
5263 text: "hello".to_string(),
5264 url: None,
5265 line: None,
5266 column: None,
5267 })
5268 .unwrap();
5269 }
5270
5271 let event = rx.try_recv().unwrap();
5272 match event {
5273 ServoEvent::Console { level, text, .. } => {
5274 assert_eq!(level, ConsoleLevel::Info);
5275 assert_eq!(text, "hello");
5276 }
5277 _ => panic!("expected Console event"),
5278 }
5279 }
5280
5281 #[test]
5282 fn test_webview_state_event_tx_default_none() {
5283 let state = BaoWebViewState::default();
5284 assert!(state.event_tx.is_none());
5285 }
5286
5287 #[test]
5288 fn test_webview_state_event_tx_propagation() {
5289 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5290 let mut state = BaoWebViewState::default();
5291 state.event_tx = Some(tx);
5292 if let Some(ref tx) = state.event_tx {
5294 tx.send(ServoEvent::FrameNavigated {
5295 target_id: "0".to_string(),
5296 frame_id: "0".to_string(),
5297 url: "https://example.com/".to_string(),
5298 name: None,
5299 })
5300 .unwrap();
5301 }
5302 let event = rx.try_recv().unwrap();
5303 match event {
5304 ServoEvent::FrameNavigated { url, .. } => {
5305 assert_eq!(url, "https://example.com/");
5306 }
5307 _ => panic!("expected FrameNavigated event"),
5308 }
5309 }
5310
5311 #[test]
5312 fn test_event_tx_console_level_mapping() {
5313 let cases: Vec<(ConsoleLogLevel, ConsoleLevel)> = vec![
5315 (ConsoleLogLevel::Debug, ConsoleLevel::Debug),
5316 (ConsoleLogLevel::Log, ConsoleLevel::Info),
5317 (ConsoleLogLevel::Info, ConsoleLevel::Info),
5318 (ConsoleLogLevel::Warn, ConsoleLevel::Warning),
5319 (ConsoleLogLevel::Error, ConsoleLevel::Error),
5320 (ConsoleLogLevel::Trace, ConsoleLevel::Verbose),
5321 ];
5322 for (servo_level, expected) in cases {
5323 let mapped = match servo_level {
5324 ConsoleLogLevel::Debug => ConsoleLevel::Debug,
5325 ConsoleLogLevel::Log => ConsoleLevel::Info,
5326 ConsoleLogLevel::Info => ConsoleLevel::Info,
5327 ConsoleLogLevel::Warn => ConsoleLevel::Warning,
5328 ConsoleLogLevel::Error => ConsoleLevel::Error,
5329 ConsoleLogLevel::Trace => ConsoleLevel::Verbose,
5330 ConsoleLogLevel::Dir => ConsoleLevel::Info,
5331 };
5332 assert_eq!(
5333 mapped, expected,
5334 "servo {:?} should map to {:?}",
5335 servo_level, expected
5336 );
5337 }
5338 }
5339
5340 #[test]
5341 fn test_notify_load_started_emits_frame_started_loading() {
5342 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5345 let state = Rc::new(RefCell::new(BaoWebViewState {
5346 event_tx: Some(tx),
5347 ..Default::default()
5348 }));
5349 let viewport = PhysicalSize::new(800, 600);
5350 let _delegate = BaoWebViewDelegate::new(state.clone(), viewport);
5351
5352 if let Some(ref tx) = state.borrow().event_tx {
5354 tx.send(ServoEvent::FrameStartedLoading {
5355 target_id: "0".to_string(),
5356 frame_id: "0".to_string(),
5357 })
5358 .unwrap();
5359 }
5360
5361 let event = rx.try_recv().unwrap();
5362 match event {
5363 ServoEvent::FrameStartedLoading {
5364 target_id,
5365 frame_id,
5366 } => {
5367 assert_eq!(target_id, "0");
5368 assert_eq!(frame_id, "0");
5369 }
5370 _ => panic!("expected FrameStartedLoading event"),
5371 }
5372 }
5373
5374 #[test]
5378 fn test_worker_handle_new_is_running() {
5379 let handle = WorkerHandle::new("https://example.com/worker.js".to_string());
5380 assert_eq!(handle.script_url, "https://example.com/worker.js");
5381 assert!(!handle.is_closing());
5382 assert!(!handle.is_terminated());
5383 }
5384
5385 #[test]
5386 fn test_worker_handle_terminate_sets_closing() {
5387 let handle = WorkerHandle::new("worker.js".to_string());
5388 assert!(!handle.is_closing());
5389 handle.terminate();
5390 assert!(handle.is_closing());
5391 handle.terminate();
5393 assert!(handle.is_closing());
5394 }
5395
5396 #[test]
5397 fn test_worker_handle_mark_terminated() {
5398 let handle = WorkerHandle::new("worker.js".to_string());
5399 assert!(!handle.is_terminated());
5400 handle.mark_terminated();
5401 assert!(handle.is_terminated());
5402 }
5403
5404 #[test]
5405 fn test_worker_handle_terminate_then_terminated() {
5406 let handle = WorkerHandle::new("worker.js".to_string());
5407 handle.terminate();
5408 assert!(handle.is_closing());
5409 assert!(!handle.is_terminated());
5410 handle.mark_terminated();
5411 assert!(handle.is_terminated());
5412 }
5413
5414 #[test]
5415 fn test_worker_handle_clone_shares_state() {
5416 let handle = WorkerHandle::new("worker.js".to_string());
5417 let clone = handle.clone();
5418 handle.terminate();
5419 assert!(
5420 clone.is_closing(),
5421 "clone should see closing flag from original"
5422 );
5423 clone.mark_terminated();
5424 assert!(
5425 handle.is_terminated(),
5426 "original should see terminated flag from clone"
5427 );
5428 }
5429
5430 #[test]
5431 fn test_webview_state_active_workers_default_empty() {
5432 let state = BaoWebViewState::default();
5433 assert!(state.active_workers.is_empty());
5434 assert_eq!(state.active_worker_count(), 0);
5435 }
5436
5437 #[test]
5438 fn test_webview_state_track_worker() {
5439 let mut state = BaoWebViewState::default();
5440 let handle = WorkerHandle::new("worker1.js".to_string());
5441 state.track_worker(handle);
5442 assert_eq!(state.active_worker_count(), 1);
5443 assert_eq!(state.active_workers.len(), 1);
5444 assert_eq!(state.active_workers[0].handle().script_url, "worker1.js");
5445 }
5446
5447 #[test]
5448 fn test_webview_state_track_multiple_workers() {
5449 let mut state = BaoWebViewState::default();
5450 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5451 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5452 state.track_worker(WorkerHandle::new("worker3.js".to_string()));
5453 assert_eq!(state.active_worker_count(), 3);
5454 }
5455
5456 #[test]
5457 fn test_webview_state_terminate_all_workers() {
5458 let mut state = BaoWebViewState::default();
5459 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5460 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5461 assert!(!state.active_workers[0].handle().is_closing());
5462 assert!(!state.active_workers[1].handle().is_closing());
5463 state.terminate_all_workers();
5464 assert!(state.active_workers[0].handle().is_closing());
5465 assert!(state.active_workers[1].handle().is_closing());
5466 }
5467
5468 #[test]
5469 fn test_webview_state_reap_terminated_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.active_workers[0].handle().terminate();
5475 state.active_workers[0].handle().mark_terminated();
5476 assert_eq!(state.active_worker_count(), 1);
5477 state.reap_terminated_workers();
5478 assert_eq!(state.active_workers.len(), 1);
5479 assert_eq!(state.active_workers[0].handle().script_url, "worker2.js");
5480 }
5481
5482 #[test]
5483 fn test_webview_state_reap_all_terminated() {
5484 let mut state = BaoWebViewState::default();
5485 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5486 state.terminate_all_workers();
5488 state.reap_terminated_workers();
5490 assert!(state.active_workers.is_empty());
5491 assert_eq!(state.active_worker_count(), 0);
5492 }
5493
5494 #[test]
5495 fn test_worker_id_equality() {
5496 let id1 = WorkerId("worker1.js".to_string());
5497 let id2 = WorkerId("worker1.js".to_string());
5498 let id3 = WorkerId("worker2.js".to_string());
5499 assert_eq!(id1, id2);
5500 assert_ne!(id1, id3);
5501 }
5502
5503 #[test]
5504 fn test_worker_message_direction() {
5505 assert_eq!(
5506 WorkerMessageDirection::PageToWorker,
5507 WorkerMessageDirection::PageToWorker
5508 );
5509 assert_ne!(
5510 WorkerMessageDirection::PageToWorker,
5511 WorkerMessageDirection::WorkerToPage
5512 );
5513 }
5514
5515 #[test]
5516 fn test_worker_message_event_creation() {
5517 let event = WorkerMessageEvent {
5518 worker_id: WorkerId("worker1.js".to_string()),
5519 direction: WorkerMessageDirection::PageToWorker,
5520 };
5521 assert_eq!(event.worker_id.0, "worker1.js");
5522 assert_eq!(event.direction, WorkerMessageDirection::PageToWorker);
5523 }
5524
5525 #[test]
5526 fn test_webview_state_forward_worker_message_to_event_tx() {
5527 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5528 let state = BaoWebViewState {
5529 event_tx: Some(tx),
5530 ..Default::default()
5531 };
5532 let msg = WorkerMessageEvent {
5533 worker_id: WorkerId("worker1.js".to_string()),
5534 direction: WorkerMessageDirection::WorkerToPage,
5535 };
5536 state.forward_worker_message_event(msg);
5537 let event = rx.try_recv().unwrap();
5538 match event {
5539 ServoEvent::Console { level, text, .. } => {
5540 assert_eq!(level, ConsoleLevel::Debug);
5541 assert!(text.contains("worker→page"));
5542 assert!(text.contains("worker1.js"));
5543 }
5544 _ => panic!("expected Console event for worker message"),
5545 }
5546 }
5547
5548 #[test]
5549 fn test_webview_state_forward_worker_message_no_event_tx() {
5550 let state = BaoWebViewState::default();
5552 let msg = WorkerMessageEvent {
5553 worker_id: WorkerId("worker1.js".to_string()),
5554 direction: WorkerMessageDirection::PageToWorker,
5555 };
5556 state.forward_worker_message_event(msg);
5558 }
5559
5560 #[test]
5561 fn test_terminate_on_navigation_then_reap() {
5562 let mut state = BaoWebViewState::default();
5564 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5565 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5566 assert_eq!(state.active_worker_count(), 2);
5567
5568 state.terminate_all_workers();
5574 assert!(state.active_workers[0].handle().is_closing());
5575 assert!(state.active_workers[1].handle().is_closing());
5576 assert_eq!(state.active_worker_count(), 0);
5579
5580 state.reap_terminated_workers();
5582 assert!(state.active_workers.is_empty());
5583 }
5584
5585 #[test]
5589 fn test_worker_error_event_creation() {
5590 let event = WorkerErrorEvent {
5591 worker_id: WorkerId("worker1.js".to_string()),
5592 message: "Uncaught TypeError: x is not a function".to_string(),
5593 filename: "worker1.js".to_string(),
5594 lineno: 42,
5595 colno: 5,
5596 };
5597 assert_eq!(event.worker_id.0, "worker1.js");
5598 assert_eq!(event.message, "Uncaught TypeError: x is not a function");
5599 assert_eq!(event.filename, "worker1.js");
5600 assert_eq!(event.lineno, 42);
5601 assert_eq!(event.colno, 5);
5602 }
5603
5604 #[test]
5605 fn test_webview_state_forward_worker_error_to_event_tx() {
5606 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
5607 let state = BaoWebViewState {
5608 event_tx: Some(tx),
5609 ..Default::default()
5610 };
5611 let error = WorkerErrorEvent {
5612 worker_id: WorkerId("worker1.js".to_string()),
5613 message: "Uncaught Error: boom".to_string(),
5614 filename: "worker1.js".to_string(),
5615 lineno: 10,
5616 colno: 3,
5617 };
5618 state.forward_worker_error_event(error);
5619 let event = rx.try_recv().unwrap();
5620 match event {
5621 ServoEvent::PageError {
5622 text,
5623 url,
5624 line,
5625 column,
5626 ..
5627 } => {
5628 assert!(text.contains("worker1.js"));
5629 assert!(text.contains("Uncaught Error: boom"));
5630 assert_eq!(url.as_deref(), Some("worker1.js"));
5631 assert_eq!(line, Some(10));
5632 assert_eq!(column, Some(3));
5633 }
5634 _ => panic!("expected PageError event for worker error"),
5635 }
5636 }
5637
5638 #[test]
5639 fn test_webview_state_forward_worker_error_no_event_tx() {
5640 let state = BaoWebViewState::default();
5641 let error = WorkerErrorEvent {
5642 worker_id: WorkerId("worker1.js".to_string()),
5643 message: "error".to_string(),
5644 filename: "worker1.js".to_string(),
5645 lineno: 1,
5646 colno: 1,
5647 };
5648 state.forward_worker_error_event(error);
5650 }
5651
5652 #[test]
5656 fn test_worker_teardown_path_equality() {
5657 assert_eq!(WorkerTeardownPath::Terminate, WorkerTeardownPath::Terminate);
5658 assert_eq!(WorkerTeardownPath::SelfClose, WorkerTeardownPath::SelfClose);
5659 assert_eq!(
5660 WorkerTeardownPath::PageUnload,
5661 WorkerTeardownPath::PageUnload
5662 );
5663 assert_ne!(WorkerTeardownPath::Terminate, WorkerTeardownPath::SelfClose);
5664 }
5665
5666 #[test]
5667 fn test_worker_lifecycle_state_running() {
5668 let handle = WorkerHandle::new("worker.js".to_string());
5669 let guard = AutoCloseWorker::new(handle);
5670 assert_eq!(guard.lifecycle_state(), WorkerLifecycleState::Running);
5671 }
5672
5673 #[test]
5674 fn test_worker_lifecycle_state_closing() {
5675 let handle = WorkerHandle::new("worker.js".to_string());
5676 let mut guard = AutoCloseWorker::new(handle);
5677 guard.terminate_via(WorkerTeardownPath::Terminate);
5678 assert_eq!(
5679 guard.lifecycle_state(),
5680 WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5681 );
5682 }
5683
5684 #[test]
5685 fn test_worker_lifecycle_state_terminated() {
5686 let handle = WorkerHandle::new("worker.js".to_string());
5687 let mut guard = AutoCloseWorker::new(handle);
5688 guard.terminate_via(WorkerTeardownPath::SelfClose);
5689 guard.handle().mark_terminated();
5690 assert_eq!(
5691 guard.lifecycle_state(),
5692 WorkerLifecycleState::Terminated(WorkerTeardownPath::SelfClose)
5693 );
5694 }
5695
5696 #[test]
5697 fn test_worker_lifecycle_states_snapshot() {
5698 let mut state = BaoWebViewState::default();
5699 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
5700 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
5701 let snapshot = state.worker_lifecycle_states();
5702 assert_eq!(snapshot.len(), 2);
5703 assert_eq!(snapshot[0].0, WorkerId("worker1.js".to_string()));
5704 assert_eq!(snapshot[0].1, WorkerLifecycleState::Running);
5705 assert_eq!(snapshot[1].0, WorkerId("worker2.js".to_string()));
5706 assert_eq!(snapshot[1].1, WorkerLifecycleState::Running);
5707 }
5708
5709 #[test]
5713 fn test_auto_close_worker_new_is_running() {
5714 let handle = WorkerHandle::new("worker.js".to_string());
5715 let guard = AutoCloseWorker::new(handle);
5716 assert!(!guard.handle().is_closing());
5717 assert!(!guard.handle().is_terminated());
5718 }
5719
5720 #[test]
5721 fn test_auto_close_worker_terminate_via() {
5722 let handle = WorkerHandle::new("worker.js".to_string());
5723 let mut guard = AutoCloseWorker::new(handle);
5724 guard.terminate_via(WorkerTeardownPath::Terminate);
5725 assert!(guard.handle().is_closing());
5726 assert_eq!(
5727 guard.lifecycle_state(),
5728 WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5729 );
5730 }
5731
5732 #[test]
5733 fn test_auto_close_worker_terminate_via_idempotent() {
5734 let handle = WorkerHandle::new("worker.js".to_string());
5735 let mut guard = AutoCloseWorker::new(handle);
5736 guard.terminate_via(WorkerTeardownPath::Terminate);
5737 guard.terminate_via(WorkerTeardownPath::SelfClose);
5738 assert_eq!(
5740 guard.lifecycle_state(),
5741 WorkerLifecycleState::Closing(WorkerTeardownPath::Terminate)
5742 );
5743 }
5744
5745 #[test]
5746 fn test_auto_close_worker_drop_terminates() {
5747 let handle = WorkerHandle::new("worker.js".to_string());
5748 let handle_clone = handle.clone();
5749 let guard = AutoCloseWorker::new(handle);
5750 assert!(!handle_clone.is_closing());
5751 drop(guard);
5752 assert!(handle_clone.is_closing());
5754 }
5755
5756 #[test]
5757 fn test_auto_close_worker_drop_already_closing() {
5758 let handle = WorkerHandle::new("worker.js".to_string());
5759 let handle_clone = handle.clone();
5760 let mut guard = AutoCloseWorker::new(handle);
5761 guard.terminate_via(WorkerTeardownPath::Terminate);
5762 drop(guard);
5763 assert!(handle_clone.is_closing());
5765 assert!(handle_clone.is_terminated());
5767 }
5768
5769 #[test]
5773 fn test_worker_handle_global_addr_default_zero() {
5774 let handle = WorkerHandle::new("worker.js".to_string());
5775 assert_eq!(handle.worker_global_addr(), 0);
5776 }
5777
5778 #[test]
5779 fn test_worker_handle_global_addr_set_and_get() {
5780 let handle = WorkerHandle::new("worker.js".to_string());
5781 handle.set_worker_global_addr(0xDEADBEEF);
5782 assert_eq!(handle.worker_global_addr(), 0xDEADBEEF);
5783 }
5784
5785 #[test]
5786 fn test_worker_handle_global_addr_arc_shared() {
5787 let handle = WorkerHandle::new("worker.js".to_string());
5788 let arc = handle.worker_global_addr_arc();
5789 arc.store(0xCAFEBABE_usize as u64, Ordering::Release);
5791 assert_eq!(handle.worker_global_addr(), 0xCAFEBABE);
5793 }
5794
5795 #[test]
5796 fn test_worker_handle_unregister_stealth_profile_no_addr() {
5797 let handle = WorkerHandle::new("worker.js".to_string());
5799 handle.unregister_stealth_profile();
5801 }
5802
5803 #[test]
5804 fn test_worker_handle_unregister_stealth_profile_with_addr() {
5805 let fake_addr = 0x12345678_usize;
5807 bao_stealth::engine_props::set_profile_for_global(
5808 fake_addr,
5809 &bao_stealth::StealthProfile::firefox_default(),
5810 );
5811 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
5813 let handle = WorkerHandle::new("worker.js".to_string());
5815 handle.set_worker_global_addr(fake_addr);
5816 handle.unregister_stealth_profile();
5817 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5819 bao_stealth::engine_props::clear_all_realm_profiles();
5821 }
5822
5823 #[test]
5824 fn test_teardown_result_crash_safe() {
5825 let result = WorkerTeardownResult {
5826 path: WorkerTeardownPath::Terminate,
5827 thread_joined: true,
5828 realm_profile_unregistered: true,
5829 closing_flag_set: true,
5830 never_registered: false,
5831 };
5832 assert!(result.is_crash_safe());
5833 }
5834
5835 #[test]
5836 fn test_teardown_result_not_crash_safe_no_join() {
5837 let result = WorkerTeardownResult {
5838 path: WorkerTeardownPath::PageUnload,
5839 thread_joined: false,
5840 realm_profile_unregistered: true,
5841 closing_flag_set: true,
5842 never_registered: false,
5843 };
5844 assert!(!result.is_crash_safe());
5845 }
5846
5847 #[test]
5848 fn test_teardown_result_not_crash_safe_no_closing() {
5849 let result = WorkerTeardownResult {
5850 path: WorkerTeardownPath::SelfClose,
5851 thread_joined: true,
5852 realm_profile_unregistered: true,
5853 closing_flag_set: false,
5854 never_registered: false,
5855 };
5856 assert!(!result.is_crash_safe());
5857 }
5858
5859 #[test]
5860 fn test_crash_safe_teardown_no_web_worker() {
5861 let handle = WorkerHandle::new("worker.js".to_string());
5863 let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::Terminate);
5864 assert!(result.closing_flag_set);
5865 assert!(result.thread_joined); assert!(!result.realm_profile_unregistered); assert!(handle.is_closing());
5868 assert!(handle.is_terminated());
5869 }
5870
5871 #[test]
5872 fn test_crash_safe_teardown_with_stealth_profile() {
5873 let fake_addr = 0xABCD0000_usize;
5875 bao_stealth::engine_props::set_profile_for_global(
5876 fake_addr,
5877 &bao_stealth::StealthProfile::firefox_default(),
5878 );
5879
5880 let handle = WorkerHandle::new("worker.js".to_string());
5881 handle.set_worker_global_addr(fake_addr);
5882
5883 let result = crash_safe_teardown_worker(&handle, WorkerTeardownPath::SelfClose);
5884 assert!(result.closing_flag_set);
5885 assert!(result.thread_joined);
5886 assert!(result.realm_profile_unregistered);
5887 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5889 assert!(handle.is_closing());
5890 assert!(handle.is_terminated());
5891 }
5892
5893 #[test]
5894 fn test_auto_close_worker_drop_unregisters_stealth_profile() {
5895 let fake_addr = 0xBEEF0000_usize;
5897 bao_stealth::engine_props::set_profile_for_global(
5898 fake_addr,
5899 &bao_stealth::StealthProfile::firefox_default(),
5900 );
5901
5902 let handle = WorkerHandle::new("worker.js".to_string());
5903 handle.set_worker_global_addr(fake_addr);
5904 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_some());
5906
5907 let guard = AutoCloseWorker::new(handle);
5908 drop(guard);
5910 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5912 }
5913
5914 #[test]
5915 fn test_terminate_all_workers_unregisters_stealth_profiles() {
5916 let fake_addr1 = 0xAAAA0001_usize;
5918 let fake_addr2 = 0xAAAA0002_usize;
5919 bao_stealth::engine_props::set_profile_for_global(
5920 fake_addr1,
5921 &bao_stealth::StealthProfile::firefox_default(),
5922 );
5923 bao_stealth::engine_props::set_profile_for_global(
5924 fake_addr2,
5925 &bao_stealth::StealthProfile::firefox_default(),
5926 );
5927
5928 let mut state = BaoWebViewState::default();
5929 let h1 = WorkerHandle::new("worker1.js".to_string());
5930 h1.set_worker_global_addr(fake_addr1);
5931 let h2 = WorkerHandle::new("worker2.js".to_string());
5932 h2.set_worker_global_addr(fake_addr2);
5933 state.track_worker(h1);
5934 state.track_worker(h2);
5935
5936 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_some());
5938 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_some());
5939
5940 state.terminate_all_workers();
5942
5943 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr1).is_none());
5945 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr2).is_none());
5946 assert!(state.active_workers.iter().all(|g| g.handle().is_closing()));
5948 assert!(state
5949 .active_workers
5950 .iter()
5951 .all(|g| g.handle().is_terminated()));
5952 }
5953
5954 #[test]
5955 fn test_terminate_worker_via_path_terminate() {
5956 let fake_addr = 0xCCCC0001_usize;
5958 bao_stealth::engine_props::set_profile_for_global(
5959 fake_addr,
5960 &bao_stealth::StealthProfile::firefox_default(),
5961 );
5962
5963 let mut state = BaoWebViewState::default();
5964 let handle = WorkerHandle::new("worker.js".to_string());
5965 handle.set_worker_global_addr(fake_addr);
5966 let worker_id = WorkerId("worker.js".to_string());
5967 state.track_worker(handle.clone());
5968
5969 let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
5970 assert!(result.is_some());
5971 let result = result.unwrap();
5972 assert_eq!(result.path, WorkerTeardownPath::Terminate);
5973 assert!(result.closing_flag_set);
5974 assert!(result.thread_joined);
5975 assert!(result.realm_profile_unregistered);
5976 assert!(bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none());
5978 assert!(handle.is_closing());
5980 assert!(handle.is_terminated());
5981 }
5982
5983 #[test]
5984 fn test_terminate_worker_via_path_self_close() {
5985 let fake_addr = 0xDDDD0001_usize;
5987 bao_stealth::engine_props::set_profile_for_global(
5988 fake_addr,
5989 &bao_stealth::StealthProfile::firefox_default(),
5990 );
5991
5992 let mut state = BaoWebViewState::default();
5993 let handle = WorkerHandle::new("worker.js".to_string());
5994 handle.set_worker_global_addr(fake_addr);
5995 let worker_id = WorkerId("worker.js".to_string());
5996 state.track_worker(handle.clone());
5997
5998 let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::SelfClose);
5999 assert!(result.is_some());
6000 let result = result.unwrap();
6001 assert_eq!(result.path, WorkerTeardownPath::SelfClose);
6002 assert!(result.closing_flag_set);
6003 assert!(result.realm_profile_unregistered);
6004 }
6005
6006 #[test]
6007 fn test_terminate_worker_via_path_not_found() {
6008 let mut state = BaoWebViewState::default();
6009 let worker_id = WorkerId("nonexistent.js".to_string());
6010 let result = state.terminate_worker_via_path(&worker_id, WorkerTeardownPath::Terminate);
6011 assert!(result.is_none());
6012 }
6013
6014 #[test]
6015 fn test_three_paths_all_crash_safe() {
6016 for path in [
6018 WorkerTeardownPath::Terminate,
6019 WorkerTeardownPath::SelfClose,
6020 WorkerTeardownPath::PageUnload,
6021 ] {
6022 let fake_addr = 0x12340000_usize
6023 + match &path {
6024 WorkerTeardownPath::Terminate => 1,
6025 WorkerTeardownPath::SelfClose => 2,
6026 WorkerTeardownPath::PageUnload => 3,
6027 };
6028 bao_stealth::engine_props::set_profile_for_global(
6029 fake_addr,
6030 &bao_stealth::StealthProfile::firefox_default(),
6031 );
6032
6033 let handle = WorkerHandle::new("worker.js".to_string());
6034 handle.set_worker_global_addr(fake_addr);
6035 let result = crash_safe_teardown_worker(&handle, path.clone());
6036 assert!(
6037 result.closing_flag_set,
6038 "closing flag not set for {:?}",
6039 path
6040 );
6041 assert!(result.thread_joined, "thread not joined for {:?}", path);
6042 assert!(
6043 result.realm_profile_unregistered,
6044 "profile not unregistered for {:?}",
6045 path
6046 );
6047 assert!(result.is_crash_safe(), "not crash-safe for {:?}", path);
6048 assert!(handle.is_closing(), "handle not closing for {:?}", path);
6049 assert!(
6050 handle.is_terminated(),
6051 "handle not terminated for {:?}",
6052 path
6053 );
6054 assert!(
6055 bao_stealth::engine_props::canvas_seed_for_test(fake_addr).is_none(),
6056 "profile not removed for {:?}",
6057 path
6058 );
6059 }
6060 }
6061
6062 #[test]
6063 fn test_track_worker_guard() {
6064 let handle = WorkerHandle::new("worker.js".to_string());
6065 let guard = AutoCloseWorker::new(handle);
6066 let mut state = BaoWebViewState::default();
6067 state.track_worker_guard(guard);
6068 assert_eq!(state.active_worker_count(), 1);
6069 }
6070
6071 #[test]
6075 fn test_worker_scope_config_default() {
6076 let config = WorkerScopeConfig::default();
6077 assert!(config.stealth_profile.is_none());
6078 assert!(config.user_agent.is_empty());
6079 assert!(config.platform.is_empty());
6080 assert!(config.hardware_concurrency > 0);
6081 assert_eq!(config.language, "en-US");
6082 assert!(!config.languages.is_empty());
6083 }
6084
6085 #[test]
6086 fn test_worker_scope_config_set_on_state() {
6087 let mut state = BaoWebViewState::default();
6088 let config = WorkerScopeConfig {
6089 stealth_profile: None,
6090 user_agent: "Bao/1.0".to_string(),
6091 platform: "Linux x86_64".to_string(),
6092 hardware_concurrency: 8,
6093 language: "zh-CN".to_string(),
6094 languages: vec!["zh-CN".to_string(), "zh".to_string(), "en".to_string()],
6095 };
6096 state.set_worker_scope_config(config);
6097 assert_eq!(state.worker_scope_config.user_agent, "Bao/1.0");
6098 assert_eq!(state.worker_scope_config.platform, "Linux x86_64");
6099 assert_eq!(state.worker_scope_config.hardware_concurrency, 8);
6100 assert_eq!(state.worker_scope_config.language, "zh-CN");
6101 assert_eq!(state.worker_scope_config.languages.len(), 3);
6102 }
6103
6104 #[test]
6105 fn test_webview_state_default_worker_scope_config() {
6106 let state = BaoWebViewState::default();
6107 assert!(state.worker_scope_config.stealth_profile.is_none());
6108 assert!(state.worker_scope_config.hardware_concurrency > 0);
6109 }
6110
6111 #[test]
6115 fn test_shared_worker_id_equality() {
6116 let id1 = SharedWorkerId {
6117 script_url: "sw.js".to_string(),
6118 name: "myworker".to_string(),
6119 };
6120 let id2 = SharedWorkerId {
6121 script_url: "sw.js".to_string(),
6122 name: "myworker".to_string(),
6123 };
6124 let id3 = SharedWorkerId {
6125 script_url: "sw.js".to_string(),
6126 name: "other".to_string(),
6127 };
6128 let id4 = SharedWorkerId {
6129 script_url: "other.js".to_string(),
6130 name: "myworker".to_string(),
6131 };
6132 assert_eq!(id1, id2);
6133 assert_ne!(id1, id3); assert_ne!(id1, id4); }
6136
6137 #[test]
6138 fn test_shared_worker_id_default_name() {
6139 let id = SharedWorkerId {
6140 script_url: "sw.js".to_string(),
6141 name: String::new(),
6142 };
6143 assert!(id.name.is_empty());
6144 }
6145
6146 #[test]
6147 fn test_shared_worker_handle_new_is_running() {
6148 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6149 assert_eq!(handle.script_url, "sw.js");
6150 assert_eq!(handle.name, "myname");
6151 assert!(!handle.is_closing());
6152 assert!(!handle.is_terminated());
6153 assert_eq!(handle.connected_page_count(), 0);
6154 }
6155
6156 #[test]
6157 fn test_shared_worker_handle_id() {
6158 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6159 let id = handle.id();
6160 assert_eq!(id.script_url, "sw.js");
6161 assert_eq!(id.name, "myname");
6162 }
6163
6164 #[test]
6165 fn test_shared_worker_handle_close() {
6166 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6167 assert!(!handle.is_closing());
6168 handle.close();
6169 assert!(handle.is_closing());
6170 handle.close();
6172 assert!(handle.is_closing());
6173 }
6174
6175 #[test]
6176 fn test_shared_worker_handle_mark_terminated() {
6177 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6178 assert!(!handle.is_terminated());
6179 handle.mark_terminated();
6180 assert!(handle.is_terminated());
6181 }
6182
6183 #[test]
6184 fn test_shared_worker_handle_connected_pages() {
6185 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6186 assert_eq!(handle.connected_page_count(), 0);
6187 handle.page_connected();
6188 assert_eq!(handle.connected_page_count(), 1);
6189 handle.page_connected();
6190 assert_eq!(handle.connected_page_count(), 2);
6191 handle.page_disconnected();
6192 assert_eq!(handle.connected_page_count(), 1);
6193 handle.page_disconnected();
6194 assert_eq!(handle.connected_page_count(), 0);
6195 }
6196
6197 #[test]
6198 fn test_shared_worker_handle_clone_shares_state() {
6199 let handle = SharedWorkerHandle::new("sw.js".to_string(), "name".to_string());
6200 let clone = handle.clone();
6201 handle.close();
6202 assert!(
6203 clone.is_closing(),
6204 "clone should see closing flag from original"
6205 );
6206 clone.mark_terminated();
6207 assert!(
6208 handle.is_terminated(),
6209 "original should see terminated flag from clone"
6210 );
6211 }
6212
6213 #[test]
6214 fn test_shared_worker_port_ref_increments_connected() {
6215 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6216 let port = SharedWorkerPortRef::new(handle.clone());
6217 assert_eq!(handle.connected_page_count(), 1);
6218 assert_eq!(port.handle().script_url, "sw.js");
6219 }
6220
6221 #[test]
6222 fn test_shared_worker_port_ref_drop_decrements_connected() {
6223 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6224 {
6225 let _port = SharedWorkerPortRef::new(handle.clone());
6226 assert_eq!(handle.connected_page_count(), 1);
6227 }
6228 assert_eq!(
6229 handle.connected_page_count(),
6230 0,
6231 "dropping port should decrement connected count"
6232 );
6233 }
6234
6235 #[test]
6236 fn test_shared_worker_port_ref_clone_increments_connected() {
6237 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6238 let port = SharedWorkerPortRef::new(handle.clone());
6239 assert_eq!(handle.connected_page_count(), 1);
6240 let _port2 = port.clone();
6241 assert_eq!(handle.connected_page_count(), 2);
6242 }
6243
6244 #[test]
6245 fn test_shared_worker_port_ref_multiple_pages() {
6246 let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
6247 let _port1 = SharedWorkerPortRef::new(handle.clone());
6248 let _port2 = SharedWorkerPortRef::new(handle.clone());
6249 assert_eq!(handle.connected_page_count(), 2);
6250 }
6251
6252 #[test]
6253 fn test_webview_state_track_shared_worker_port() {
6254 let mut state = BaoWebViewState::default();
6255 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6256 state.track_shared_worker_port(SharedWorkerPortRef::new(handle));
6257 assert_eq!(state.shared_worker_port_count(), 1);
6258 }
6259
6260 #[test]
6261 fn test_webview_state_disconnect_shared_worker_ports() {
6262 let mut state = BaoWebViewState::default();
6263 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6264 state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6265 state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6266 assert_eq!(state.shared_worker_port_count(), 2);
6267 assert_eq!(handle.connected_page_count(), 2);
6268 state.disconnect_shared_worker_ports();
6269 assert_eq!(state.shared_worker_port_count(), 0);
6270 assert_eq!(
6271 handle.connected_page_count(),
6272 0,
6273 "disconnect should drop ports and decrement counter"
6274 );
6275 }
6276
6277 #[test]
6278 fn test_webview_state_disconnect_shared_worker_ports_empty() {
6279 let mut state = BaoWebViewState::default();
6280 state.disconnect_shared_worker_ports();
6282 assert_eq!(state.shared_worker_port_count(), 0);
6283 }
6284
6285 #[test]
6286 fn test_delegate_register_shared_worker_new() {
6287 let delegate = BaoServoDelegate::new();
6288 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6289 let returned = delegate.register_shared_worker(handle);
6290 assert_eq!(returned.script_url, "sw.js");
6291 assert_eq!(returned.name, "myname");
6292 assert_eq!(delegate.shared_worker_count(), 1);
6293 }
6294
6295 #[test]
6296 fn test_delegate_register_shared_worker_dedup() {
6297 let delegate = BaoServoDelegate::new();
6298 let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6299 let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6300 delegate.register_shared_worker(handle1);
6301 let returned = delegate.register_shared_worker(handle2);
6302 assert_eq!(delegate.shared_worker_count(), 1);
6304 assert_eq!(returned.script_url, "sw.js");
6305 }
6306
6307 #[test]
6308 fn test_delegate_find_shared_worker() {
6309 let delegate = BaoServoDelegate::new();
6310 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
6311 delegate.register_shared_worker(handle);
6312 let found = delegate.find_shared_worker("sw.js", "myname");
6313 assert!(found.is_some());
6314 assert_eq!(found.unwrap().script_url, "sw.js");
6315 assert!(delegate.find_shared_worker("other.js", "myname").is_none());
6316 assert!(delegate.find_shared_worker("sw.js", "other").is_none());
6317 }
6318
6319 #[test]
6320 fn test_delegate_reap_terminated_shared_workers() {
6321 let delegate = BaoServoDelegate::new();
6322 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6323 delegate.register_shared_worker(handle.clone());
6324 assert_eq!(delegate.shared_worker_count(), 1);
6325 handle.close();
6327 handle.mark_terminated();
6328 delegate.reap_terminated_shared_workers();
6329 assert_eq!(
6330 delegate.shared_worker_count(),
6331 0,
6332 "terminated shared worker with zero pages should be reaped"
6333 );
6334 }
6335
6336 #[test]
6337 fn test_delegate_reap_keeps_terminated_with_connected_pages() {
6338 let delegate = BaoServoDelegate::new();
6339 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6340 delegate.register_shared_worker(handle.clone());
6341 let _port = SharedWorkerPortRef::new(handle.clone());
6343 handle.close();
6344 handle.mark_terminated();
6345 delegate.reap_terminated_shared_workers();
6346 assert_eq!(
6347 delegate.shared_worker_count(),
6348 1,
6349 "terminated but still has connected pages — keep in registry"
6350 );
6351 }
6352
6353 #[test]
6354 fn test_shared_worker_connect_event_creation() {
6355 let event = SharedWorkerConnectEvent {
6356 shared_worker_id: SharedWorkerId {
6357 script_url: "sw.js".to_string(),
6358 name: "myname".to_string(),
6359 },
6360 page_url: "https://example.com/page1".to_string(),
6361 };
6362 assert_eq!(event.shared_worker_id.script_url, "sw.js");
6363 assert_eq!(event.shared_worker_id.name, "myname");
6364 assert_eq!(event.page_url, "https://example.com/page1");
6365 }
6366
6367 #[test]
6368 fn test_forward_shared_worker_connect_event() {
6369 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6370 let state = BaoWebViewState {
6371 event_tx: Some(tx),
6372 ..Default::default()
6373 };
6374 let event = SharedWorkerConnectEvent {
6375 shared_worker_id: SharedWorkerId {
6376 script_url: "sw.js".to_string(),
6377 name: "myname".to_string(),
6378 },
6379 page_url: "https://example.com".to_string(),
6380 };
6381 state.forward_shared_worker_connect_event(event);
6382 let recv = rx.try_recv().unwrap();
6383 match recv {
6384 ServoEvent::Console { level, text, .. } => {
6385 assert_eq!(level, ConsoleLevel::Debug);
6386 assert!(text.contains("sw.js"));
6387 assert!(text.contains("myname"));
6388 assert!(text.contains("https://example.com"));
6389 }
6390 _ => panic!("expected Console event for shared worker connect"),
6391 }
6392 }
6393
6394 #[test]
6395 fn test_forward_shared_worker_connect_event_no_tx() {
6396 let state = BaoWebViewState::default();
6397 let event = SharedWorkerConnectEvent {
6398 shared_worker_id: SharedWorkerId {
6399 script_url: "sw.js".to_string(),
6400 name: String::new(),
6401 },
6402 page_url: "https://example.com".to_string(),
6403 };
6404 state.forward_shared_worker_connect_event(event);
6406 }
6407
6408 #[test]
6409 fn test_shared_worker_scope_config_default() {
6410 let config = SharedWorkerScopeConfig::default();
6411 assert!(config.stealth_profile.is_none());
6412 assert!(config.user_agent.is_empty());
6413 assert!(config.platform.is_empty());
6414 assert!(config.hardware_concurrency > 0);
6415 assert_eq!(config.language, "en-US");
6416 assert!(!config.languages.is_empty());
6417 }
6418
6419 #[test]
6420 fn test_page_navigation_disconnects_shared_workers() {
6421 let mut state = BaoWebViewState::default();
6422 let handle = SharedWorkerHandle::new("sw.js".to_string(), String::new());
6423 state.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6424 assert_eq!(state.shared_worker_port_count(), 1);
6425 assert_eq!(handle.connected_page_count(), 1);
6426 state.disconnect_shared_worker_ports();
6428 assert_eq!(state.shared_worker_port_count(), 0);
6429 assert_eq!(handle.connected_page_count(), 0);
6430 }
6431
6432 #[test]
6433 fn test_shared_worker_cross_page_sharing() {
6434 let handle = SharedWorkerHandle::new("sw.js".to_string(), "shared".to_string());
6436
6437 let mut state1 = BaoWebViewState::default();
6439 state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6440 assert_eq!(handle.connected_page_count(), 1);
6441
6442 let mut state2 = BaoWebViewState::default();
6444 state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
6445 assert_eq!(handle.connected_page_count(), 2);
6446
6447 state1.disconnect_shared_worker_ports();
6449 assert_eq!(handle.connected_page_count(), 1);
6450
6451 assert_eq!(state2.shared_worker_port_count(), 1);
6453
6454 assert!(!handle.is_closing());
6456 assert!(!handle.is_terminated());
6457 }
6458
6459 #[test]
6463 fn test_structured_clone_payload_creation() {
6464 let payload = StructuredClonePayload {
6465 data: vec![1, 2, 3, 4, 5],
6466 transferable_count: 0,
6467 };
6468 assert_eq!(payload.data.len(), 5);
6469 assert_eq!(payload.transferable_count, 0);
6470 }
6471
6472 #[test]
6473 fn test_structured_clone_payload_with_transferables() {
6474 let payload = StructuredClonePayload {
6475 data: vec![0u8; 1024],
6476 transferable_count: 2,
6477 };
6478 assert_eq!(payload.data.len(), 1024);
6479 assert_eq!(payload.transferable_count, 2);
6480 }
6481
6482 #[test]
6483 fn test_structured_clone_payload_clone() {
6484 let payload = StructuredClonePayload {
6485 data: vec![42u8; 100],
6486 transferable_count: 1,
6487 };
6488 let cloned = payload.clone();
6489 assert_eq!(cloned.data, payload.data);
6490 assert_eq!(cloned.transferable_count, payload.transferable_count);
6491 }
6492
6493 #[test]
6494 fn test_worker_structured_message_metadata_only() {
6495 let msg = WorkerStructuredMessage::metadata_only(
6496 WorkerId("worker1.js".to_string()),
6497 WorkerMessageDirection::PageToWorker,
6498 );
6499 assert!(msg.payload.is_none());
6500 assert_eq!(msg.worker_id.0, "worker1.js");
6501 assert_eq!(msg.direction, WorkerMessageDirection::PageToWorker);
6502 assert!(msg.message_id > 0);
6503 }
6504
6505 #[test]
6506 fn test_worker_structured_message_with_payload() {
6507 let msg = WorkerStructuredMessage::with_payload(
6508 WorkerId("worker2.js".to_string()),
6509 WorkerMessageDirection::WorkerToPage,
6510 vec![1, 2, 3],
6511 1,
6512 );
6513 assert!(msg.payload.is_some());
6514 let payload = msg.payload.unwrap();
6515 assert_eq!(payload.data, vec![1, 2, 3]);
6516 assert_eq!(payload.transferable_count, 1);
6517 assert_eq!(msg.direction, WorkerMessageDirection::WorkerToPage);
6518 }
6519
6520 #[test]
6521 fn test_worker_structured_message_unique_ids() {
6522 let msg1 = WorkerStructuredMessage::metadata_only(
6523 WorkerId("w.js".to_string()),
6524 WorkerMessageDirection::PageToWorker,
6525 );
6526 let msg2 = WorkerStructuredMessage::metadata_only(
6527 WorkerId("w.js".to_string()),
6528 WorkerMessageDirection::PageToWorker,
6529 );
6530 assert_ne!(msg1.message_id, msg2.message_id);
6532 }
6533
6534 #[test]
6535 fn test_worker_channel_bridge_creation() {
6536 let worker_id = WorkerId("worker1.js".to_string());
6537 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id.clone());
6538 assert_eq!(bridge.worker_id, worker_id);
6539 assert_eq!(endpoints.worker_id, worker_id);
6540 assert!(endpoints.page_to_worker_rx.is_some());
6542 assert!(endpoints.worker_to_page_tx.is_some());
6543 }
6544
6545 #[test]
6546 fn test_worker_channel_bridge_page_to_worker() {
6547 let worker_id = WorkerId("worker1.js".to_string());
6548 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6549 let payload = StructuredClonePayload {
6551 data: vec![1, 2, 3],
6552 transferable_count: 0,
6553 };
6554 bridge.post_message_to_worker(payload).unwrap();
6555 let rx = endpoints.page_to_worker_rx.unwrap();
6557 let received = rx.try_recv().unwrap();
6558 assert_eq!(received.data, vec![1, 2, 3]);
6559 }
6560
6561 #[test]
6562 fn test_worker_channel_bridge_worker_to_page() {
6563 let worker_id = WorkerId("worker1.js".to_string());
6564 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6565 let msg = WorkerStructuredMessage::with_payload(
6567 WorkerId("worker1.js".to_string()),
6568 WorkerMessageDirection::WorkerToPage,
6569 vec![4, 5, 6],
6570 0,
6571 );
6572 let tx = endpoints.worker_to_page_tx.unwrap();
6573 tx.send(msg).unwrap();
6574 let result = bridge.try_recv_from_worker().unwrap();
6576 assert!(result.is_some());
6577 let received = result.unwrap();
6578 assert_eq!(received.payload.unwrap().data, vec![4, 5, 6]);
6579 }
6580
6581 #[test]
6582 fn test_worker_channel_bridge_drain() {
6583 let worker_id = WorkerId("worker1.js".to_string());
6584 let (bridge, endpoints) = WorkerChannelBridge::new(worker_id);
6585 let tx = endpoints.worker_to_page_tx.unwrap();
6586 for i in 0..3 {
6588 let msg = WorkerStructuredMessage::with_payload(
6589 WorkerId("worker1.js".to_string()),
6590 WorkerMessageDirection::WorkerToPage,
6591 vec![i],
6592 0,
6593 );
6594 tx.send(msg).unwrap();
6595 }
6596 let result = bridge.drain_worker_messages();
6598 assert_eq!(result.messages.len(), 3);
6599 assert!(!result.disconnected);
6600 let empty = bridge.drain_worker_messages();
6602 assert!(empty.messages.is_empty());
6603 assert!(!empty.disconnected);
6604 }
6605
6606 #[test]
6607 fn test_webview_state_worker_channel_registration() {
6608 let mut state = BaoWebViewState::default();
6609 let worker_id = WorkerId("worker1.js".to_string());
6610 let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id.clone());
6611 state.register_worker_channel(bridge);
6612 assert_eq!(state.worker_channel_count(), 1);
6613 assert!(state.worker_channel(&worker_id).is_some());
6614 }
6615
6616 #[test]
6617 fn test_webview_state_create_worker_channel() {
6618 let mut state = BaoWebViewState::default();
6619 let worker_id = WorkerId("worker1.js".to_string());
6620 let endpoints = state.create_worker_channel(worker_id.clone());
6621 assert_eq!(state.worker_channel_count(), 1);
6622 assert_eq!(endpoints.worker_id, worker_id);
6623 assert!(endpoints.page_to_worker_rx.is_some());
6624 assert!(endpoints.worker_to_page_tx.is_some());
6625 }
6626
6627 #[test]
6628 fn test_webview_state_post_to_worker() {
6629 let mut state = BaoWebViewState::default();
6630 let worker_id = WorkerId("worker1.js".to_string());
6631 let endpoints = state.create_worker_channel(worker_id.clone());
6632 let payload = StructuredClonePayload {
6633 data: vec![42],
6634 transferable_count: 0,
6635 };
6636 let result = state.post_to_worker(&worker_id, payload);
6638 assert!(result.is_ok());
6639 let rx = endpoints.page_to_worker_rx.unwrap();
6641 let received = rx.try_recv().unwrap();
6642 assert_eq!(received.data, vec![42]);
6643 let result = state.post_to_worker(
6645 &WorkerId("nonexistent.js".to_string()),
6646 StructuredClonePayload {
6647 data: vec![],
6648 transferable_count: 0,
6649 },
6650 );
6651 assert!(result.is_err());
6652 }
6653
6654 #[test]
6655 fn test_webview_state_drain_all_worker_messages() {
6656 let mut state = BaoWebViewState::default();
6657 let worker_id1 = WorkerId("worker1.js".to_string());
6658 let worker_id2 = WorkerId("worker2.js".to_string());
6659 let endpoints1 = state.create_worker_channel(worker_id1);
6660 let endpoints2 = state.create_worker_channel(worker_id2);
6661 let tx1 = endpoints1.worker_to_page_tx.unwrap();
6663 let tx2 = endpoints2.worker_to_page_tx.unwrap();
6664 tx1.send(WorkerStructuredMessage::metadata_only(
6665 WorkerId("worker1.js".to_string()),
6666 WorkerMessageDirection::WorkerToPage,
6667 ))
6668 .unwrap();
6669 tx2.send(WorkerStructuredMessage::metadata_only(
6670 WorkerId("worker2.js".to_string()),
6671 WorkerMessageDirection::WorkerToPage,
6672 ))
6673 .unwrap();
6674 let (messages, disconnected) = state.drain_all_worker_messages();
6676 assert_eq!(messages.len(), 2);
6677 assert!(disconnected.is_empty());
6678 }
6679
6680 #[test]
6681 fn test_webview_state_terminate_clears_channels() {
6682 let mut state = BaoWebViewState::default();
6683 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
6684 state.create_worker_channel(WorkerId("worker1.js".to_string()));
6685 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
6686 state.create_worker_channel(WorkerId("worker2.js".to_string()));
6687 assert_eq!(state.worker_channel_count(), 2);
6688 state.terminate_all_workers();
6690 assert_eq!(state.worker_channel_count(), 0);
6691 }
6692
6693 #[test]
6694 fn test_webview_state_reap_terminated_worker_channels() {
6695 let mut state = BaoWebViewState::default();
6696 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
6697 state.create_worker_channel(WorkerId("worker1.js".to_string()));
6698 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
6699 state.create_worker_channel(WorkerId("worker2.js".to_string()));
6700 state.active_workers[0].handle().terminate();
6702 state.active_workers[0].handle().mark_terminated();
6703 state.reap_terminated_workers();
6704 assert_eq!(state.worker_channel_count(), 1);
6706 assert!(state
6707 .worker_channel(&WorkerId("worker2.js".to_string()))
6708 .is_some());
6709 }
6710
6711 #[test]
6712 fn test_webview_state_remove_worker_channel() {
6713 let mut state = BaoWebViewState::default();
6714 let worker_id = WorkerId("worker1.js".to_string());
6715 state.create_worker_channel(worker_id.clone());
6716 assert_eq!(state.worker_channel_count(), 1);
6717 let removed = state.remove_worker_channel(&worker_id);
6718 assert!(removed.is_some());
6719 assert_eq!(state.worker_channel_count(), 0);
6720 }
6721
6722 #[test]
6723 fn test_forward_worker_structured_message_with_payload() {
6724 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6725 let state = BaoWebViewState {
6726 event_tx: Some(tx),
6727 ..Default::default()
6728 };
6729 let msg = WorkerStructuredMessage::with_payload(
6730 WorkerId("worker1.js".to_string()),
6731 WorkerMessageDirection::WorkerToPage,
6732 vec![1, 2, 3],
6733 1,
6734 );
6735 state.forward_worker_structured_message(&msg);
6736 let event = rx.try_recv().unwrap();
6737 match event {
6738 ServoEvent::Console { level, text, .. } => {
6739 assert_eq!(level, ConsoleLevel::Debug);
6740 assert!(text.contains("worker→page"));
6741 assert!(text.contains("worker1.js"));
6742 assert!(text.contains("3 bytes"));
6743 assert!(text.contains("1 transferable"));
6744 }
6745 _ => panic!("expected Console event for structured message"),
6746 }
6747 }
6748
6749 #[test]
6750 fn test_forward_worker_structured_message_metadata_only() {
6751 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6752 let state = BaoWebViewState {
6753 event_tx: Some(tx),
6754 ..Default::default()
6755 };
6756 let msg = WorkerStructuredMessage::metadata_only(
6757 WorkerId("worker1.js".to_string()),
6758 WorkerMessageDirection::PageToWorker,
6759 );
6760 state.forward_worker_structured_message(&msg);
6761 let event = rx.try_recv().unwrap();
6762 match event {
6763 ServoEvent::Console { text, .. } => {
6764 assert!(text.contains("metadata-only"));
6765 }
6766 _ => panic!("expected Console event"),
6767 }
6768 }
6769
6770 #[test]
6771 fn test_drain_and_forward_worker_messages() {
6772 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
6773 let mut state = BaoWebViewState {
6774 event_tx: Some(tx),
6775 ..Default::default()
6776 };
6777 let endpoints = state.create_worker_channel(WorkerId("worker1.js".to_string()));
6778 let worker_tx = endpoints.worker_to_page_tx.unwrap();
6779 worker_tx
6780 .send(WorkerStructuredMessage::metadata_only(
6781 WorkerId("worker1.js".to_string()),
6782 WorkerMessageDirection::WorkerToPage,
6783 ))
6784 .unwrap();
6785 let disconnected = state.drain_and_forward_worker_messages();
6787 assert!(disconnected.is_empty());
6788 let event = rx.try_recv().unwrap();
6790 match event {
6791 ServoEvent::Console { text, .. } => {
6792 assert!(text.contains("worker→page"));
6793 }
6794 _ => panic!("expected Console event"),
6795 }
6796 }
6797
6798 #[test]
6799 fn test_worker_channel_bridge_disconnected() {
6800 let worker_id = WorkerId("worker1.js".to_string());
6801 let (bridge, _endpoints) = WorkerChannelBridge::new(worker_id);
6802 let result = bridge.try_recv_from_worker();
6807 assert!(result.is_ok()); assert!(result.unwrap().is_none());
6809 }
6810
6811 #[test]
6815 fn test_worker_location_from_https_url() {
6816 let loc = WorkerLocation::from_url("https://example.com:8080/path?q=1#hash").unwrap();
6817 assert_eq!(loc.href, "https://example.com:8080/path?q=1#hash");
6818 assert_eq!(loc.protocol, "https:");
6819 assert_eq!(loc.host, "example.com:8080");
6820 assert_eq!(loc.hostname, "example.com");
6821 assert_eq!(loc.port, "8080");
6822 assert_eq!(loc.pathname, "/path");
6823 assert_eq!(loc.search, "?q=1");
6824 assert_eq!(loc.hash, "#hash");
6825 assert_eq!(loc.origin, "https://example.com:8080");
6826 }
6827
6828 #[test]
6829 fn test_worker_location_from_default_port() {
6830 let loc = WorkerLocation::from_url("https://example.com/path").unwrap();
6831 assert_eq!(loc.host, "example.com");
6832 assert_eq!(loc.port, "");
6833 assert_eq!(loc.origin, "https://example.com");
6834 }
6835
6836 #[test]
6837 fn test_worker_location_from_http_url() {
6838 let loc = WorkerLocation::from_url("http://localhost:3000/worker.js").unwrap();
6839 assert_eq!(loc.protocol, "http:");
6840 assert_eq!(loc.hostname, "localhost");
6841 assert_eq!(loc.port, "3000");
6842 assert_eq!(loc.pathname, "/worker.js");
6843 }
6844
6845 #[test]
6846 fn test_worker_location_from_url_no_query_no_hash() {
6847 let loc = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
6848 assert_eq!(loc.search, "");
6849 assert_eq!(loc.hash, "");
6850 }
6851
6852 #[test]
6853 fn test_worker_location_from_invalid_url() {
6854 assert!(WorkerLocation::from_url("not a url").is_none());
6855 }
6856
6857 #[test]
6858 fn test_worker_location_from_url_value() {
6859 let url = url::Url::parse("https://example.com/worker.js").unwrap();
6860 let loc = WorkerLocation::from_url_value(url);
6861 assert_eq!(loc.protocol, "https:");
6862 assert_eq!(loc.hostname, "example.com");
6863 assert_eq!(loc.pathname, "/worker.js");
6864 }
6865
6866 #[test]
6870 fn test_worker_navigator_default() {
6871 let nav = WorkerNavigator::default();
6872 assert!(nav.user_agent.is_empty());
6873 assert!(nav.platform.is_empty());
6874 assert!(nav.hardware_concurrency > 0);
6875 assert_eq!(nav.language, "en-US");
6876 assert!(!nav.languages.is_empty());
6877 assert!(nav.connection.is_none());
6878 assert!(!nav.cookie_enabled);
6879 assert_eq!(nav.max_touch_points, 0);
6880 assert_eq!(nav.product, "Gecko");
6881 assert_eq!(nav.app_code_name, "Mozilla");
6882 assert_eq!(nav.app_name, "Netscape");
6883 assert!(nav.app_version.is_empty());
6884 }
6885
6886 #[test]
6887 fn test_worker_navigator_from_scope_config() {
6888 let config = WorkerScopeConfig {
6889 stealth_profile: None,
6890 user_agent: "Bao/1.0".to_string(),
6891 platform: "Linux x86_64".to_string(),
6892 hardware_concurrency: 8,
6893 language: "zh-CN".to_string(),
6894 languages: vec!["zh-CN".to_string(), "zh".to_string()],
6895 };
6896 let nav = WorkerNavigator::from_scope_config(&config);
6897 assert_eq!(nav.user_agent, "Bao/1.0");
6898 assert_eq!(nav.platform, "Linux x86_64");
6899 assert_eq!(nav.hardware_concurrency, 8);
6900 assert_eq!(nav.language, "zh-CN");
6901 assert_eq!(nav.languages.len(), 2);
6902 assert_eq!(nav.app_version, "Bao/1.0"); assert_eq!(nav.product, "Gecko");
6904 assert_eq!(nav.app_code_name, "Mozilla");
6905 assert_eq!(nav.app_name, "Netscape");
6906 }
6907
6908 #[test]
6909 fn test_worker_navigator_from_shared_scope_config() {
6910 let config = SharedWorkerScopeConfig {
6911 stealth_profile: None,
6912 user_agent: "Bao/2.0".to_string(),
6913 platform: "MacOS".to_string(),
6914 hardware_concurrency: 4,
6915 language: "ja".to_string(),
6916 languages: vec!["ja".to_string(), "en".to_string()],
6917 };
6918 let nav = WorkerNavigator::from_shared_scope_config(&config);
6919 assert_eq!(nav.user_agent, "Bao/2.0");
6920 assert_eq!(nav.platform, "MacOS");
6921 assert_eq!(nav.hardware_concurrency, 4);
6922 assert_eq!(nav.app_version, "Bao/2.0");
6923 }
6924
6925 #[test]
6926 fn test_worker_network_information() {
6927 let info = WorkerNetworkInformation {
6928 effective_type: "4g".to_string(),
6929 downlink: 10,
6930 rtt: 50,
6931 save_data: false,
6932 };
6933 assert_eq!(info.effective_type, "4g");
6934 assert_eq!(info.downlink, 10);
6935 assert_eq!(info.rtt, 50);
6936 assert!(!info.save_data);
6937 }
6938
6939 #[test]
6943 fn test_worker_global_scope_state_new() {
6944 let config = WorkerScopeConfig {
6945 stealth_profile: None,
6946 user_agent: "Bao/1.0".to_string(),
6947 platform: "Linux".to_string(),
6948 hardware_concurrency: 8,
6949 language: "en-US".to_string(),
6950 languages: vec!["en-US".to_string()],
6951 };
6952 let scope =
6953 WorkerGlobalScopeState::new("https://example.com/worker.js".to_string(), &config);
6954 assert_eq!(scope.worker_url, "https://example.com/worker.js");
6955 assert!(!scope.closing);
6956 assert!(scope.location.is_some());
6957 assert_eq!(scope.navigator.user_agent, "Bao/1.0");
6958 }
6959
6960 #[test]
6961 fn test_worker_global_scope_state_new_shared() {
6962 let config = SharedWorkerScopeConfig {
6963 stealth_profile: None,
6964 user_agent: "Bao/2.0".to_string(),
6965 platform: "MacOS".to_string(),
6966 hardware_concurrency: 4,
6967 language: "ja".to_string(),
6968 languages: vec!["ja".to_string()],
6969 };
6970 let scope =
6971 WorkerGlobalScopeState::new_shared("https://example.com/sw.js".to_string(), &config);
6972 assert_eq!(scope.worker_url, "https://example.com/sw.js");
6973 assert_eq!(scope.navigator.user_agent, "Bao/2.0");
6974 }
6975
6976 #[test]
6977 fn test_worker_global_scope_state_location_parsed() {
6978 let config = WorkerScopeConfig::default();
6979 let scope = WorkerGlobalScopeState::new(
6980 "https://example.com:8080/app/worker.js?debug=true#section".to_string(),
6981 &config,
6982 );
6983 let loc = scope.location.unwrap();
6984 assert_eq!(loc.hostname, "example.com");
6985 assert_eq!(loc.port, "8080");
6986 assert_eq!(loc.pathname, "/app/worker.js");
6987 assert_eq!(loc.search, "?debug=true");
6988 assert_eq!(loc.hash, "#section");
6989 }
6990
6991 #[test]
6992 fn test_worker_global_scope_state_invalid_url_no_location() {
6993 let config = WorkerScopeConfig::default();
6994 let scope = WorkerGlobalScopeState::new("not-a-url".to_string(), &config);
6995 assert!(scope.location.is_none());
6996 }
6997
6998 #[test]
7002 fn test_dedicated_worker_global_scope_state_new() {
7003 let worker_id = WorkerId("https://example.com/worker.js".to_string());
7004 let config = WorkerScopeConfig {
7005 stealth_profile: None,
7006 user_agent: "Bao/1.0".to_string(),
7007 platform: "Linux".to_string(),
7008 hardware_concurrency: 8,
7009 language: "en-US".to_string(),
7010 languages: vec!["en-US".to_string()],
7011 };
7012 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7013 assert_eq!(scope.worker_id, worker_id);
7014 assert!(!scope.has_onmessage);
7015 assert!(!scope.has_onerror);
7016 assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
7017 }
7018
7019 #[test]
7020 fn test_dedicated_worker_global_scope_state_location() {
7021 let worker_id = WorkerId("https://example.com/worker.js".to_string());
7022 let config = WorkerScopeConfig::default();
7023 let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7024 let loc = scope.location().unwrap();
7025 assert_eq!(loc.hostname, "example.com");
7026 assert_eq!(loc.pathname, "/worker.js");
7027 }
7028
7029 #[test]
7030 fn test_dedicated_worker_global_scope_state_navigator() {
7031 let worker_id = WorkerId("worker.js".to_string());
7032 let config = WorkerScopeConfig {
7033 stealth_profile: None,
7034 user_agent: "Bao/1.0".to_string(),
7035 platform: "Linux".to_string(),
7036 hardware_concurrency: 8,
7037 language: "zh-CN".to_string(),
7038 languages: vec!["zh-CN".to_string()],
7039 };
7040 let scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7041 let nav = scope.navigator();
7042 assert_eq!(nav.user_agent, "Bao/1.0");
7043 assert_eq!(nav.hardware_concurrency, 8);
7044 }
7045
7046 #[test]
7047 fn test_dedicated_worker_global_scope_state_event_handlers() {
7048 let worker_id = WorkerId("worker.js".to_string());
7049 let config = WorkerScopeConfig::default();
7050 let mut scope = DedicatedWorkerGlobalScopeState::new(worker_id, &config);
7051 assert!(!scope.has_onmessage);
7052 assert!(!scope.has_onerror);
7053 scope.set_onmessage();
7054 assert!(scope.has_onmessage);
7055 assert!(!scope.has_onerror);
7056 scope.set_onerror();
7057 assert!(scope.has_onmessage);
7058 assert!(scope.has_onerror);
7059 }
7060
7061 #[test]
7065 fn test_webview_state_dedicated_worker_scope_registration() {
7066 let mut state = BaoWebViewState::default();
7067 let worker_id = WorkerId("worker1.js".to_string());
7068 let config = WorkerScopeConfig::default();
7069 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7070 state.register_dedicated_worker_scope(worker_id.clone(), scope);
7071 assert_eq!(state.dedicated_worker_scope_count(), 1);
7072 assert!(state.dedicated_worker_scope(&worker_id).is_some());
7073 }
7074
7075 #[test]
7076 fn test_webview_state_dedicated_worker_scope_get_mut() {
7077 let mut state = BaoWebViewState::default();
7078 let worker_id = WorkerId("worker1.js".to_string());
7079 let config = WorkerScopeConfig::default();
7080 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7081 state.register_dedicated_worker_scope(worker_id.clone(), scope);
7082 state
7084 .dedicated_worker_scope_mut(&worker_id)
7085 .unwrap()
7086 .set_onmessage();
7087 assert!(
7088 state
7089 .dedicated_worker_scope(&worker_id)
7090 .unwrap()
7091 .has_onmessage
7092 );
7093 }
7094
7095 #[test]
7096 fn test_webview_state_dedicated_worker_scope_remove() {
7097 let mut state = BaoWebViewState::default();
7098 let worker_id = WorkerId("worker1.js".to_string());
7099 let config = WorkerScopeConfig::default();
7100 let scope = DedicatedWorkerGlobalScopeState::new(worker_id.clone(), &config);
7101 state.register_dedicated_worker_scope(worker_id.clone(), scope);
7102 let removed = state.remove_dedicated_worker_scope(&worker_id);
7103 assert!(removed.is_some());
7104 assert_eq!(state.dedicated_worker_scope_count(), 0);
7105 }
7106
7107 #[test]
7108 fn test_webview_state_dedicated_worker_scopes_snapshot() {
7109 let mut state = BaoWebViewState::default();
7110 let config = WorkerScopeConfig::default();
7111 let id1 = WorkerId("worker1.js".to_string());
7112 let id2 = WorkerId("worker2.js".to_string());
7113 state.register_dedicated_worker_scope(
7114 id1,
7115 DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7116 );
7117 state.register_dedicated_worker_scope(
7118 id2,
7119 DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
7120 );
7121 let scopes = state.dedicated_worker_scopes();
7122 assert_eq!(scopes.len(), 2);
7123 }
7124
7125 #[test]
7126 fn test_webview_state_terminate_clears_dedicated_worker_scopes() {
7127 let mut state = BaoWebViewState::default();
7128 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7129 let config = WorkerScopeConfig::default();
7130 state.register_dedicated_worker_scope(
7131 WorkerId("worker1.js".to_string()),
7132 DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7133 );
7134 assert_eq!(state.dedicated_worker_scope_count(), 1);
7135 state.terminate_all_workers();
7136 assert_eq!(state.dedicated_worker_scope_count(), 0);
7137 }
7138
7139 #[test]
7140 fn test_webview_state_reap_terminated_dedicated_worker_scopes() {
7141 let mut state = BaoWebViewState::default();
7142 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7143 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
7144 let config = WorkerScopeConfig::default();
7145 state.register_dedicated_worker_scope(
7146 WorkerId("worker1.js".to_string()),
7147 DedicatedWorkerGlobalScopeState::new(WorkerId("worker1.js".to_string()), &config),
7148 );
7149 state.register_dedicated_worker_scope(
7150 WorkerId("worker2.js".to_string()),
7151 DedicatedWorkerGlobalScopeState::new(WorkerId("worker2.js".to_string()), &config),
7152 );
7153 state.active_workers[0].handle().terminate();
7155 state.active_workers[0].handle().mark_terminated();
7156 state.reap_terminated_workers();
7157 assert_eq!(state.dedicated_worker_scope_count(), 1);
7159 assert!(state
7160 .dedicated_worker_scope(&WorkerId("worker2.js".to_string()))
7161 .is_some());
7162 }
7163
7164 #[test]
7165 fn test_worker_location_equality() {
7166 let loc1 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
7167 let loc2 = WorkerLocation::from_url("https://example.com/worker.js").unwrap();
7168 assert_eq!(loc1, loc2);
7169 }
7170
7171 #[test]
7175 fn test_worker_script_source_inline() {
7176 let source = WorkerScriptSource::Inline("var x = 1;".to_string());
7177 assert_eq!(source, WorkerScriptSource::Inline("var x = 1;".to_string()));
7178 assert_ne!(source, WorkerScriptSource::Inline("var y = 2;".to_string()));
7179 }
7180
7181 #[test]
7182 fn test_worker_script_source_url() {
7183 let source = WorkerScriptSource::Url("https://example.com/worker.js".to_string());
7184 assert_eq!(
7185 source,
7186 WorkerScriptSource::Url("https://example.com/worker.js".to_string())
7187 );
7188 assert_ne!(
7189 source,
7190 WorkerScriptSource::Url("https://other.com/worker.js".to_string())
7191 );
7192 }
7193
7194 #[test]
7195 fn test_worker_script_load_result() {
7196 let result = WorkerScriptLoadResult {
7197 source: "self.onmessage = function(e) {}".to_string(),
7198 final_url: "https://example.com/worker.js".to_string(),
7199 mime_type: Some("text/javascript".to_string()),
7200 };
7201 assert_eq!(result.source, "self.onmessage = function(e) {}");
7202 assert_eq!(result.final_url, "https://example.com/worker.js");
7203 assert_eq!(result.mime_type.as_deref(), Some("text/javascript"));
7204 }
7205
7206 #[test]
7207 fn test_worker_script_load_error_network() {
7208 let err = WorkerScriptLoadError::NetworkError("404 Not Found".to_string());
7209 assert_eq!(
7210 err,
7211 WorkerScriptLoadError::NetworkError("404 Not Found".to_string())
7212 );
7213 }
7214
7215 #[test]
7216 fn test_worker_script_load_error_invalid_mime() {
7217 let err = WorkerScriptLoadError::InvalidMimeType {
7218 received: "text/html".to_string(),
7219 url: "https://example.com/worker.js".to_string(),
7220 };
7221 match err {
7222 WorkerScriptLoadError::InvalidMimeType { received, url } => {
7223 assert_eq!(received, "text/html");
7224 assert_eq!(url, "https://example.com/worker.js");
7225 }
7226 _ => panic!("expected InvalidMimeType"),
7227 }
7228 }
7229
7230 #[test]
7231 fn test_worker_script_load_error_utf8() {
7232 let err = WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string());
7233 assert_eq!(
7234 err,
7235 WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string())
7236 );
7237 }
7238
7239 #[test]
7240 fn test_worker_script_load_error_invalid_url() {
7241 let err = WorkerScriptLoadError::InvalidUrl("bad url".to_string());
7242 assert_eq!(
7243 err,
7244 WorkerScriptLoadError::InvalidUrl("bad url".to_string())
7245 );
7246 }
7247
7248 #[test]
7249 fn test_worker_script_load_error_cancelled() {
7250 let err = WorkerScriptLoadError::Cancelled;
7251 assert_eq!(err, WorkerScriptLoadError::Cancelled);
7252 }
7253
7254 #[test]
7255 fn test_worker_script_type_default_classic() {
7256 assert_eq!(WorkerScriptType::default(), WorkerScriptType::Classic);
7257 }
7258
7259 #[test]
7260 fn test_worker_script_type_equality() {
7261 assert_eq!(WorkerScriptType::Classic, WorkerScriptType::Classic);
7262 assert_eq!(WorkerScriptType::Module, WorkerScriptType::Module);
7263 assert_ne!(WorkerScriptType::Classic, WorkerScriptType::Module);
7264 }
7265
7266 #[test]
7267 fn test_is_javascript_mime_type_valid() {
7268 assert!(is_javascript_mime_type("text/javascript"));
7269 assert!(is_javascript_mime_type("application/javascript"));
7270 assert!(is_javascript_mime_type("application/ecmascript"));
7271 assert!(is_javascript_mime_type("application/x-javascript"));
7272 assert!(is_javascript_mime_type("text/ecmascript"));
7273 assert!(is_javascript_mime_type("text/x-javascript"));
7274 assert!(is_javascript_mime_type("text/jscript"));
7275 assert!(is_javascript_mime_type("text/livescript"));
7276 }
7277
7278 #[test]
7279 fn test_is_javascript_mime_type_case_insensitive() {
7280 assert!(is_javascript_mime_type("Text/JavaScript"));
7281 assert!(is_javascript_mime_type("APPLICATION/JAVASCRIPT"));
7282 assert!(is_javascript_mime_type("text/JavaScript"));
7283 }
7284
7285 #[test]
7286 fn test_is_javascript_mime_type_with_charset() {
7287 assert!(is_javascript_mime_type("text/javascript; charset=utf-8"));
7289 assert!(is_javascript_mime_type(
7290 "application/javascript;charset=utf-8"
7291 ));
7292 }
7293
7294 #[test]
7295 fn test_is_javascript_mime_type_invalid() {
7296 assert!(!is_javascript_mime_type("text/html"));
7297 assert!(!is_javascript_mime_type("application/json"));
7298 assert!(!is_javascript_mime_type("text/plain"));
7299 assert!(!is_javascript_mime_type("application/octet-stream"));
7300 assert!(!is_javascript_mime_type("text/css"));
7301 }
7302
7303 #[test]
7304 fn test_worker_script_loader_inline() {
7305 let loader =
7306 WorkerScriptLoader::inline("var x = 1;".to_string(), WorkerScriptType::Classic);
7307 assert!(loader.script_url().is_none());
7308 assert!(!loader.requires_fetch());
7309 let resolved = loader.resolve().unwrap();
7310 assert_eq!(
7311 resolved,
7312 WorkerScriptSource::Inline("var x = 1;".to_string())
7313 );
7314 }
7315
7316 #[test]
7317 fn test_worker_script_loader_url_https() {
7318 let loader = WorkerScriptLoader::url(
7319 "https://example.com/worker.js".to_string(),
7320 WorkerScriptType::Classic,
7321 );
7322 assert_eq!(loader.script_url(), Some("https://example.com/worker.js"));
7323 assert!(loader.requires_fetch());
7324 let resolved = loader.resolve().unwrap();
7325 assert_eq!(
7326 resolved,
7327 WorkerScriptSource::Url("https://example.com/worker.js".to_string())
7328 );
7329 }
7330
7331 #[test]
7332 fn test_worker_script_loader_url_http() {
7333 let loader = WorkerScriptLoader::url(
7334 "http://localhost:3000/worker.js".to_string(),
7335 WorkerScriptType::Module,
7336 );
7337 assert!(loader.requires_fetch());
7338 assert_eq!(loader.script_type, WorkerScriptType::Module);
7339 }
7340
7341 #[test]
7342 fn test_worker_script_loader_url_invalid() {
7343 let loader = WorkerScriptLoader::url("not a url".to_string(), WorkerScriptType::Classic);
7344 let result = loader.resolve();
7345 assert!(result.is_err());
7346 match result.unwrap_err() {
7347 WorkerScriptLoadError::InvalidUrl(msg) => {
7348 assert!(msg.contains("Invalid Worker script URL"));
7349 }
7350 _ => panic!("expected InvalidUrl error"),
7351 }
7352 }
7353
7354 #[test]
7355 fn test_worker_script_loader_url_unsupported_scheme() {
7356 let loader = WorkerScriptLoader::url(
7357 "ftp://example.com/worker.js".to_string(),
7358 WorkerScriptType::Classic,
7359 );
7360 let result = loader.resolve();
7361 assert!(result.is_err());
7362 match result.unwrap_err() {
7363 WorkerScriptLoadError::InvalidUrl(msg) => {
7364 assert!(msg.contains("Unsupported") || msg.contains("ftp"));
7365 }
7366 _ => panic!("expected InvalidUrl error"),
7367 }
7368 }
7369
7370 #[test]
7371 fn test_worker_script_loader_data_url_text() {
7372 let loader = WorkerScriptLoader::url(
7373 "data:text/javascript,self.postMessage('hello')".to_string(),
7374 WorkerScriptType::Classic,
7375 );
7376 let resolved = loader.resolve().unwrap();
7377 match resolved {
7378 WorkerScriptSource::Inline(script) => {
7379 assert_eq!(script, "self.postMessage('hello')");
7380 }
7381 WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
7382 }
7383 }
7384
7385 #[test]
7386 fn test_worker_script_loader_data_url_base64() {
7387 let loader = WorkerScriptLoader::url(
7389 "data:text/javascript;base64,dmFyIHggPSAxOw==".to_string(),
7390 WorkerScriptType::Classic,
7391 );
7392 let resolved = loader.resolve().unwrap();
7393 match resolved {
7394 WorkerScriptSource::Inline(script) => {
7395 assert_eq!(script, "var x = 1;");
7396 }
7397 WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
7398 }
7399 }
7400
7401 #[test]
7402 fn test_worker_script_loader_data_url_invalid_base64() {
7403 let loader = WorkerScriptLoader::url(
7404 "data:text/javascript;base64,!!!invalid!!!".to_string(),
7405 WorkerScriptType::Classic,
7406 );
7407 let result = loader.resolve();
7408 assert!(result.is_err());
7409 }
7410
7411 #[test]
7412 fn test_worker_script_loader_data_url_missing_comma() {
7413 let loader = WorkerScriptLoader::url(
7414 "data:text/javascript".to_string(),
7415 WorkerScriptType::Classic,
7416 );
7417 let result = loader.resolve();
7418 assert!(result.is_err());
7419 match result.unwrap_err() {
7420 WorkerScriptLoadError::InvalidUrl(msg) => {
7421 assert!(msg.contains("comma separator"));
7422 }
7423 _ => panic!("expected InvalidUrl error"),
7424 }
7425 }
7426
7427 #[test]
7428 fn test_worker_script_loader_blob_url_passthrough() {
7429 let loader = WorkerScriptLoader::url(
7430 "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string(),
7431 WorkerScriptType::Classic,
7432 );
7433 let resolved = loader.resolve().unwrap();
7434 assert_eq!(
7435 resolved,
7436 WorkerScriptSource::Url(
7437 "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string()
7438 )
7439 );
7440 }
7441
7442 #[test]
7443 fn test_worker_script_loader_from_source() {
7444 let loader = WorkerScriptLoader::from_source(
7445 WorkerScriptSource::Inline("code".to_string()),
7446 WorkerScriptType::Module,
7447 );
7448 assert_eq!(loader.script_type, WorkerScriptType::Module);
7449 assert!(loader.script_url().is_none());
7450 }
7451
7452 #[test]
7453 fn test_worker_script_loader_validate_mime_type_valid() {
7454 assert!(WorkerScriptLoader::validate_mime_type(
7455 "text/javascript",
7456 "https://example.com/worker.js"
7457 )
7458 .is_ok());
7459 assert!(WorkerScriptLoader::validate_mime_type(
7460 "application/javascript",
7461 "https://example.com/worker.js"
7462 )
7463 .is_ok());
7464 }
7465
7466 #[test]
7467 fn test_worker_script_loader_validate_mime_type_invalid() {
7468 let result =
7469 WorkerScriptLoader::validate_mime_type("text/html", "https://example.com/worker.js");
7470 assert!(result.is_err());
7471 match result.unwrap_err() {
7472 WorkerScriptLoadError::InvalidMimeType { received, url } => {
7473 assert_eq!(received, "text/html");
7474 assert_eq!(url, "https://example.com/worker.js");
7475 }
7476 _ => panic!("expected InvalidMimeType error"),
7477 }
7478 }
7479
7480 #[test]
7481 fn test_worker_script_load_state_transitions() {
7482 let mut state = WorkerScriptLoadState::Pending;
7483 assert!(state.is_loading());
7484 assert!(!state.is_ready());
7485 assert!(!state.is_failed());
7486
7487 state = WorkerScriptLoadState::Fetching;
7488 assert!(state.is_loading());
7489
7490 state = WorkerScriptLoadState::Validating;
7491 assert!(state.is_loading());
7492
7493 state = WorkerScriptLoadState::Decoding;
7494 assert!(state.is_loading());
7495
7496 state = WorkerScriptLoadState::Compiling;
7497 assert!(state.is_loading());
7498
7499 state = WorkerScriptLoadState::Ready;
7500 assert!(!state.is_loading());
7501 assert!(state.is_ready());
7502
7503 state = WorkerScriptLoadState::Failed(WorkerScriptLoadError::NetworkError(
7504 "timeout".to_string(),
7505 ));
7506 assert!(!state.is_loading());
7507 assert!(state.is_failed());
7508 }
7509
7510 #[test]
7511 fn test_webview_state_worker_script_load_state_registration() {
7512 let mut state = BaoWebViewState::default();
7513 let worker_id = WorkerId("worker1.js".to_string());
7514 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7515 assert_eq!(state.worker_script_load_state_count(), 1);
7516 assert!(state.worker_script_load_state(&worker_id).is_some());
7517 assert_eq!(
7518 state.worker_script_load_state(&worker_id).unwrap(),
7519 &WorkerScriptLoadState::Pending
7520 );
7521 }
7522
7523 #[test]
7524 fn test_webview_state_worker_script_load_state_update() {
7525 let mut state = BaoWebViewState::default();
7526 let worker_id = WorkerId("worker1.js".to_string());
7527 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7528 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
7529 assert_eq!(
7530 state.worker_script_load_state(&worker_id).unwrap(),
7531 &WorkerScriptLoadState::Fetching
7532 );
7533 }
7534
7535 #[test]
7536 fn test_webview_state_worker_script_load_state_remove() {
7537 let mut state = BaoWebViewState::default();
7538 let worker_id = WorkerId("worker1.js".to_string());
7539 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Ready);
7540 let removed = state.remove_worker_script_load_state(&worker_id);
7541 assert!(removed.is_some());
7542 assert_eq!(removed.unwrap(), WorkerScriptLoadState::Ready);
7543 assert_eq!(state.worker_script_load_state_count(), 0);
7544 }
7545
7546 #[test]
7547 fn test_webview_state_terminate_clears_script_load_states() {
7548 let mut state = BaoWebViewState::default();
7549 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7550 state.register_worker_script_load_state(
7551 WorkerId("worker1.js".to_string()),
7552 WorkerScriptLoadState::Fetching,
7553 );
7554 assert_eq!(state.worker_script_load_state_count(), 1);
7555 state.terminate_all_workers();
7556 assert_eq!(state.worker_script_load_state_count(), 0);
7557 }
7558
7559 #[test]
7560 fn test_webview_state_reap_terminated_worker_script_load_states() {
7561 let mut state = BaoWebViewState::default();
7562 state.track_worker(WorkerHandle::new("worker1.js".to_string()));
7563 state.track_worker(WorkerHandle::new("worker2.js".to_string()));
7564 state.register_worker_script_load_state(
7565 WorkerId("worker1.js".to_string()),
7566 WorkerScriptLoadState::Ready,
7567 );
7568 state.register_worker_script_load_state(
7569 WorkerId("worker2.js".to_string()),
7570 WorkerScriptLoadState::Fetching,
7571 );
7572 state.active_workers[0].handle().terminate();
7574 state.active_workers[0].handle().mark_terminated();
7575 state.reap_terminated_workers();
7576 assert_eq!(state.worker_script_load_state_count(), 1);
7578 assert!(state
7579 .worker_script_load_state(&WorkerId("worker2.js".to_string()))
7580 .is_some());
7581 }
7582
7583 #[test]
7584 fn test_worker_script_loader_file_url() {
7585 let temp_dir = std::env::temp_dir();
7587 let temp_file = temp_dir.join("bao_test_worker_script.js");
7588 std::fs::write(&temp_file, "var x = 42;").unwrap();
7589
7590 let file_url = format!("file://{}", temp_file.display());
7591 let loader = WorkerScriptLoader::url(file_url, WorkerScriptType::Classic);
7592 let resolved = loader.resolve().unwrap();
7593 match resolved {
7594 WorkerScriptSource::Inline(script) => {
7595 assert_eq!(script, "var x = 42;");
7596 }
7597 WorkerScriptSource::Url(_) => panic!("expected inline source from file: URL"),
7598 }
7599
7600 let _ = std::fs::remove_file(&temp_file);
7602 }
7603
7604 #[test]
7605 fn test_worker_script_loader_file_url_not_found() {
7606 let loader = WorkerScriptLoader::url(
7607 "file:///nonexistent/path/worker.js".to_string(),
7608 WorkerScriptType::Classic,
7609 );
7610 let result = loader.resolve();
7611 assert!(result.is_err());
7612 match result.unwrap_err() {
7613 WorkerScriptLoadError::NetworkError(msg) => {
7614 assert!(msg.contains("Failed to read") || msg.contains("No such file"));
7615 }
7616 _ => panic!("expected NetworkError for missing file"),
7617 }
7618 }
7619
7620 #[test]
7621 fn test_worker_script_loader_full_pipeline_states() {
7622 let mut state = BaoWebViewState::default();
7624 let worker_id = WorkerId("https://example.com/worker.js".to_string());
7625
7626 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7628 assert!(state
7629 .worker_script_load_state(&worker_id)
7630 .unwrap()
7631 .is_loading());
7632
7633 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
7635 assert!(matches!(
7636 state.worker_script_load_state(&worker_id).unwrap(),
7637 WorkerScriptLoadState::Fetching
7638 ));
7639
7640 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Validating);
7642
7643 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Decoding);
7645
7646 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Compiling);
7648
7649 state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Ready);
7651 assert!(state
7652 .worker_script_load_state(&worker_id)
7653 .unwrap()
7654 .is_ready());
7655 }
7656
7657 #[test]
7658 fn test_worker_script_loader_pipeline_failure() {
7659 let mut state = BaoWebViewState::default();
7660 let worker_id = WorkerId("https://example.com/bad-worker.js".to_string());
7661
7662 state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
7663
7664 state.update_worker_script_load_state(
7666 &worker_id,
7667 WorkerScriptLoadState::Failed(WorkerScriptLoadError::InvalidMimeType {
7668 received: "text/html".to_string(),
7669 url: "https://example.com/bad-worker.js".to_string(),
7670 }),
7671 );
7672 assert!(state
7673 .worker_script_load_state(&worker_id)
7674 .unwrap()
7675 .is_failed());
7676 }
7677
7678 #[test]
7682 fn test_worker_scope_config_from_stealth_profile_chrome() {
7683 let profile = bao_stealth::StealthProfile::chrome_default();
7684 let config = WorkerScopeConfig::from(&profile);
7685
7686 assert!(
7687 config.stealth_profile.is_some(),
7688 "stealth_profile must be Some"
7689 );
7690 assert_eq!(config.user_agent, profile.navigator.user_agent);
7691 assert_eq!(config.platform, profile.navigator.platform);
7692 assert_eq!(
7693 config.hardware_concurrency,
7694 profile.navigator.hardware_concurrency as usize
7695 );
7696 assert_eq!(config.language, profile.navigator.language);
7697 assert_eq!(config.languages, profile.navigator.languages);
7698 assert!(
7699 config.user_agent.contains("Chrome"),
7700 "Chrome profile UA must contain Chrome"
7701 );
7702 }
7703
7704 #[test]
7705 fn test_worker_scope_config_from_stealth_profile_firefox() {
7706 let profile = bao_stealth::StealthProfile::firefox_default();
7707 let config = WorkerScopeConfig::from(&profile);
7708
7709 assert!(
7710 config.stealth_profile.is_some(),
7711 "stealth_profile must be Some"
7712 );
7713 assert_eq!(config.user_agent, profile.navigator.user_agent);
7714 assert_eq!(config.platform, profile.navigator.platform);
7715 assert_eq!(
7716 config.hardware_concurrency,
7717 profile.navigator.hardware_concurrency as usize
7718 );
7719 assert_eq!(config.language, profile.navigator.language);
7720 assert_eq!(config.languages, profile.navigator.languages);
7721 assert!(
7722 config.user_agent.contains("Firefox"),
7723 "Firefox profile UA must contain Firefox"
7724 );
7725 }
7726
7727 #[test]
7728 fn test_shared_worker_scope_config_from_stealth_profile() {
7729 let profile = bao_stealth::StealthProfile::chrome_default();
7730 let config = SharedWorkerScopeConfig::from(&profile);
7731
7732 assert!(
7733 config.stealth_profile.is_some(),
7734 "stealth_profile must be Some"
7735 );
7736 assert_eq!(config.user_agent, profile.navigator.user_agent);
7737 assert_eq!(config.platform, profile.navigator.platform);
7738 assert_eq!(
7739 config.hardware_concurrency,
7740 profile.navigator.hardware_concurrency as usize
7741 );
7742 assert_eq!(config.language, profile.navigator.language);
7743 assert_eq!(config.languages, profile.navigator.languages);
7744 }
7745
7746 #[test]
7747 fn test_worker_scope_config_from_stealth_profile_carries_canvas_webgl_audio() {
7748 let profile = bao_stealth::StealthProfile::chrome_default();
7751 let config = WorkerScopeConfig::from(&profile);
7752 let worker_profile = config.stealth_profile.unwrap();
7753
7754 assert_eq!(
7755 worker_profile.canvas.seed(),
7756 profile.canvas.seed(),
7757 "Canvas seed must match"
7758 );
7759 assert!(
7760 (worker_profile.canvas.noise_amplitude() - profile.canvas.noise_amplitude()).abs()
7761 < f64::EPSILON,
7762 "Canvas amplitude must match"
7763 );
7764 assert_eq!(
7765 worker_profile.audio.seed(),
7766 profile.audio.seed(),
7767 "Audio seed must match"
7768 );
7769 assert_eq!(
7770 worker_profile.webgl.vendor, profile.webgl.vendor,
7771 "WebGL vendor must match"
7772 );
7773 assert_eq!(
7774 worker_profile.webgl.renderer, profile.webgl.renderer,
7775 "WebGL renderer must match"
7776 );
7777 }
7778
7779 #[test]
7780 fn test_worker_scope_config_from_different_profiles_produces_different_configs() {
7781 let chrome = bao_stealth::StealthProfile::chrome_default();
7783 let firefox = bao_stealth::StealthProfile::firefox_default();
7784 let chrome_config = WorkerScopeConfig::from(&chrome);
7785 let firefox_config = WorkerScopeConfig::from(&firefox);
7786
7787 assert_ne!(chrome_config.user_agent, firefox_config.user_agent);
7788 assert_ne!(
7789 chrome_config.stealth_profile.unwrap().canvas.seed(),
7790 firefox_config.stealth_profile.unwrap().canvas.seed()
7791 );
7792 }
7793
7794 #[test]
7798 fn test_shared_worker_global_scope_state_new() {
7799 let id = SharedWorkerId {
7800 script_url: "sw.js".to_string(),
7801 name: "myworker".to_string(),
7802 };
7803 let config = SharedWorkerScopeConfig {
7804 stealth_profile: None,
7805 user_agent: "Bao/1.0".to_string(),
7806 platform: "Linux".to_string(),
7807 hardware_concurrency: 8,
7808 language: "en-US".to_string(),
7809 languages: vec!["en-US".to_string()],
7810 };
7811 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
7812 assert_eq!(scope.shared_worker_id, id);
7813 assert!(!scope.has_onconnect);
7814 assert_eq!(scope.connect_count, 0);
7815 assert_eq!(scope.scope.navigator.user_agent, "Bao/1.0");
7816 }
7817
7818 #[test]
7819 fn test_shared_worker_global_scope_state_location() {
7820 let id = SharedWorkerId {
7821 script_url: "https://example.com/sw.js".to_string(),
7822 name: String::new(),
7823 };
7824 let config = SharedWorkerScopeConfig::default();
7825 let scope = SharedWorkerGlobalScopeState::new(id, &config);
7826 let loc = scope.location().unwrap();
7827 assert_eq!(loc.hostname, "example.com");
7828 assert_eq!(loc.pathname, "/sw.js");
7829 }
7830
7831 #[test]
7832 fn test_shared_worker_global_scope_state_navigator() {
7833 let id = SharedWorkerId {
7834 script_url: "sw.js".to_string(),
7835 name: "test".to_string(),
7836 };
7837 let config = SharedWorkerScopeConfig {
7838 stealth_profile: None,
7839 user_agent: "Bao/2.0".to_string(),
7840 platform: "MacOS".to_string(),
7841 hardware_concurrency: 4,
7842 language: "ja".to_string(),
7843 languages: vec!["ja".to_string()],
7844 };
7845 let scope = SharedWorkerGlobalScopeState::new(id, &config);
7846 let nav = scope.navigator();
7847 assert_eq!(nav.user_agent, "Bao/2.0");
7848 assert_eq!(nav.hardware_concurrency, 4);
7849 }
7850
7851 #[test]
7852 fn test_shared_worker_global_scope_state_onconnect() {
7853 let id = SharedWorkerId {
7854 script_url: "sw.js".to_string(),
7855 name: String::new(),
7856 };
7857 let config = SharedWorkerScopeConfig::default();
7858 let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
7859 assert!(!scope.has_onconnect);
7860 scope.set_onconnect();
7861 assert!(scope.has_onconnect);
7862 }
7863
7864 #[test]
7865 fn test_shared_worker_global_scope_state_connect_count() {
7866 let id = SharedWorkerId {
7867 script_url: "sw.js".to_string(),
7868 name: String::new(),
7869 };
7870 let config = SharedWorkerScopeConfig::default();
7871 let mut scope = SharedWorkerGlobalScopeState::new(id, &config);
7872 assert_eq!(scope.connect_count, 0);
7873 scope.page_connected();
7874 assert_eq!(scope.connect_count, 1);
7875 scope.page_connected();
7876 assert_eq!(scope.connect_count, 2);
7877 }
7878
7879 #[test]
7883 fn test_shared_worker_port_channel_creation() {
7884 let id = SharedWorkerId {
7885 script_url: "sw.js".to_string(),
7886 name: "test".to_string(),
7887 };
7888 let (port, endpoints) = SharedWorkerPortChannel::new(id.clone());
7889 assert_eq!(port.shared_worker_id, id);
7890 assert_eq!(endpoints.shared_worker_id, id);
7891 assert!(endpoints.page_to_worker_rx.is_some());
7892 assert!(endpoints.worker_to_page_tx.is_some());
7893 }
7894
7895 #[test]
7896 fn test_shared_worker_port_channel_page_to_worker() {
7897 let id = SharedWorkerId {
7898 script_url: "sw.js".to_string(),
7899 name: String::new(),
7900 };
7901 let (port, endpoints) = SharedWorkerPortChannel::new(id);
7902 let payload = StructuredClonePayload {
7903 data: vec![1, 2, 3],
7904 transferable_count: 0,
7905 };
7906 port.post_message_to_worker(payload).unwrap();
7907 let rx = endpoints.page_to_worker_rx.unwrap();
7908 let received = rx.try_recv().unwrap();
7909 assert_eq!(received.data, vec![1, 2, 3]);
7910 }
7911
7912 #[test]
7913 fn test_shared_worker_port_channel_worker_to_page() {
7914 let id = SharedWorkerId {
7915 script_url: "sw.js".to_string(),
7916 name: String::new(),
7917 };
7918 let (port, endpoints) = SharedWorkerPortChannel::new(id);
7919 let msg = WorkerStructuredMessage::with_payload(
7920 WorkerId("sw.js".to_string()),
7921 WorkerMessageDirection::WorkerToPage,
7922 vec![4, 5, 6],
7923 0,
7924 );
7925 let tx = endpoints.worker_to_page_tx.unwrap();
7926 tx.send(msg).unwrap();
7927 let result = port.try_recv_from_worker().unwrap();
7928 assert!(result.is_some());
7929 assert_eq!(result.unwrap().payload.unwrap().data, vec![4, 5, 6]);
7930 }
7931
7932 #[test]
7933 fn test_shared_worker_port_channel_drain() {
7934 let id = SharedWorkerId {
7935 script_url: "sw.js".to_string(),
7936 name: String::new(),
7937 };
7938 let (port, endpoints) = SharedWorkerPortChannel::new(id);
7939 let tx = endpoints.worker_to_page_tx.unwrap();
7940 for i in 0..3 {
7941 let msg = WorkerStructuredMessage::with_payload(
7942 WorkerId("sw.js".to_string()),
7943 WorkerMessageDirection::WorkerToPage,
7944 vec![i],
7945 0,
7946 );
7947 tx.send(msg).unwrap();
7948 }
7949 let result = port.drain_worker_messages();
7950 assert_eq!(result.messages.len(), 3);
7951 assert!(!result.disconnected);
7952 let empty = port.drain_worker_messages();
7953 assert!(empty.messages.is_empty());
7954 assert!(!empty.disconnected);
7955 }
7956
7957 #[test]
7961 fn test_shared_worker_channel_bridge_new() {
7962 let id = SharedWorkerId {
7963 script_url: "sw.js".to_string(),
7964 name: "test".to_string(),
7965 };
7966 let bridge = SharedWorkerChannelBridge::new(id.clone());
7967 assert_eq!(bridge.shared_worker_id, id);
7968 assert_eq!(bridge.port_count(), 0);
7969 }
7970
7971 #[test]
7972 fn test_shared_worker_channel_bridge_add_port() {
7973 let id = SharedWorkerId {
7974 script_url: "sw.js".to_string(),
7975 name: "test".to_string(),
7976 };
7977 let mut bridge = SharedWorkerChannelBridge::new(id.clone());
7978 let endpoints = bridge.add_port();
7979 assert_eq!(bridge.port_count(), 1);
7980 assert_eq!(endpoints.shared_worker_id, id);
7981 assert!(endpoints.page_to_worker_rx.is_some());
7982 assert!(endpoints.worker_to_page_tx.is_some());
7983 }
7984
7985 #[test]
7986 fn test_shared_worker_channel_bridge_multiple_ports() {
7987 let id = SharedWorkerId {
7988 script_url: "sw.js".to_string(),
7989 name: "test".to_string(),
7990 };
7991 let mut bridge = SharedWorkerChannelBridge::new(id);
7992 bridge.add_port(); bridge.add_port(); bridge.add_port(); assert_eq!(bridge.port_count(), 3);
7996 }
7997
7998 #[test]
7999 fn test_shared_worker_channel_bridge_drain_all() {
8000 let id = SharedWorkerId {
8001 script_url: "sw.js".to_string(),
8002 name: "test".to_string(),
8003 };
8004 let mut bridge = SharedWorkerChannelBridge::new(id);
8005 let endpoints1 = bridge.add_port();
8006 let endpoints2 = bridge.add_port();
8007 let tx1 = endpoints1.worker_to_page_tx.unwrap();
8009 let tx2 = endpoints2.worker_to_page_tx.unwrap();
8010 tx1.send(WorkerStructuredMessage::metadata_only(
8011 WorkerId("sw.js".to_string()),
8012 WorkerMessageDirection::WorkerToPage,
8013 ))
8014 .unwrap();
8015 tx2.send(WorkerStructuredMessage::metadata_only(
8016 WorkerId("sw.js".to_string()),
8017 WorkerMessageDirection::WorkerToPage,
8018 ))
8019 .unwrap();
8020 let (messages, disconnected) = bridge.drain_all_worker_messages();
8021 assert_eq!(messages.len(), 2);
8022 assert!(disconnected.is_empty());
8023 }
8024
8025 #[test]
8026 fn test_shared_worker_channel_bridge_post_to_worker() {
8027 let id = SharedWorkerId {
8028 script_url: "sw.js".to_string(),
8029 name: "test".to_string(),
8030 };
8031 let mut bridge = SharedWorkerChannelBridge::new(id);
8032 let endpoints = bridge.add_port();
8033 let payload = StructuredClonePayload {
8034 data: vec![42],
8035 transferable_count: 0,
8036 };
8037 bridge.post_to_worker_from_port(0, payload).unwrap();
8038 let rx = endpoints.page_to_worker_rx.unwrap();
8039 let received = rx.try_recv().unwrap();
8040 assert_eq!(received.data, vec![42]);
8041 }
8042
8043 #[test]
8044 fn test_shared_worker_channel_bridge_post_invalid_port() {
8045 let id = SharedWorkerId {
8046 script_url: "sw.js".to_string(),
8047 name: "test".to_string(),
8048 };
8049 let mut bridge = SharedWorkerChannelBridge::new(id);
8050 bridge.add_port();
8051 let payload = StructuredClonePayload {
8052 data: vec![],
8053 transferable_count: 0,
8054 };
8055 let result = bridge.post_to_worker_from_port(99, payload);
8056 assert!(result.is_err());
8057 }
8058
8059 #[test]
8063 fn test_webview_state_shared_worker_channel_registration() {
8064 let mut state = BaoWebViewState::default();
8065 let id = SharedWorkerId {
8066 script_url: "sw.js".to_string(),
8067 name: "test".to_string(),
8068 };
8069 let bridge = SharedWorkerChannelBridge::new(id.clone());
8070 state.register_shared_worker_channel(bridge);
8071 assert!(state.shared_worker_channel(&id).is_some());
8072 assert_eq!(state.shared_worker_channel_count(), 0); }
8074
8075 #[test]
8076 fn test_webview_state_create_shared_worker_channel() {
8077 let mut state = BaoWebViewState::default();
8078 let id = SharedWorkerId {
8079 script_url: "sw.js".to_string(),
8080 name: "test".to_string(),
8081 };
8082 state.create_shared_worker_channel(id.clone());
8083 assert!(state.shared_worker_channel(&id).is_some());
8084 }
8085
8086 #[test]
8087 fn test_webview_state_add_shared_worker_port() {
8088 let mut state = BaoWebViewState::default();
8089 let id = SharedWorkerId {
8090 script_url: "sw.js".to_string(),
8091 name: "test".to_string(),
8092 };
8093 let endpoints = state.add_shared_worker_port(id.clone());
8094 assert_eq!(state.shared_worker_channel_count(), 1);
8095 assert_eq!(endpoints.shared_worker_id, id);
8096 assert!(endpoints.page_to_worker_rx.is_some());
8097 assert!(endpoints.worker_to_page_tx.is_some());
8098 }
8099
8100 #[test]
8101 fn test_webview_state_add_shared_worker_port_multiple() {
8102 let mut state = BaoWebViewState::default();
8103 let id = SharedWorkerId {
8104 script_url: "sw.js".to_string(),
8105 name: "test".to_string(),
8106 };
8107 state.add_shared_worker_port(id.clone());
8108 state.add_shared_worker_port(id.clone());
8109 assert_eq!(state.shared_worker_channel_count(), 2); }
8111
8112 #[test]
8113 fn test_webview_state_drain_all_shared_worker_messages() {
8114 let mut state = BaoWebViewState::default();
8115 let id = SharedWorkerId {
8116 script_url: "sw.js".to_string(),
8117 name: "test".to_string(),
8118 };
8119 let endpoints = state.add_shared_worker_port(id);
8120 let tx = endpoints.worker_to_page_tx.unwrap();
8121 tx.send(WorkerStructuredMessage::metadata_only(
8122 WorkerId("sw.js".to_string()),
8123 WorkerMessageDirection::WorkerToPage,
8124 ))
8125 .unwrap();
8126 let (messages, disconnected) = state.drain_all_shared_worker_messages();
8127 assert_eq!(messages.len(), 1);
8128 assert!(disconnected.is_empty());
8129 }
8130
8131 #[test]
8132 fn test_webview_state_drain_and_forward_shared_worker_messages() {
8133 let (tx, rx) = std::sync::mpsc::channel::<ServoEvent>();
8134 let mut state = BaoWebViewState {
8135 event_tx: Some(tx),
8136 ..Default::default()
8137 };
8138 let id = SharedWorkerId {
8139 script_url: "sw.js".to_string(),
8140 name: "test".to_string(),
8141 };
8142 let endpoints = state.add_shared_worker_port(id);
8143 let worker_tx = endpoints.worker_to_page_tx.unwrap();
8144 worker_tx
8145 .send(WorkerStructuredMessage::metadata_only(
8146 WorkerId("sw.js".to_string()),
8147 WorkerMessageDirection::WorkerToPage,
8148 ))
8149 .unwrap();
8150 state.drain_and_forward_shared_worker_messages();
8151 let event = rx.try_recv().unwrap();
8152 match event {
8153 ServoEvent::Console { text, .. } => {
8154 assert!(text.contains("worker→page"));
8155 }
8156 _ => panic!("expected Console event for shared worker message"),
8157 }
8158 }
8159
8160 #[test]
8161 fn test_webview_state_disconnect_shared_worker_clears_channels() {
8162 let mut state = BaoWebViewState::default();
8163 let id = SharedWorkerId {
8164 script_url: "sw.js".to_string(),
8165 name: "test".to_string(),
8166 };
8167 state.track_shared_worker_port(SharedWorkerPortRef::new(SharedWorkerHandle::new(
8168 "sw.js".to_string(),
8169 "test".to_string(),
8170 )));
8171 state.add_shared_worker_port(id.clone());
8172 assert_eq!(state.shared_worker_port_count(), 1);
8173 assert_eq!(state.shared_worker_channel_count(), 1);
8174 state.disconnect_shared_worker_ports();
8175 assert_eq!(state.shared_worker_port_count(), 0);
8176 assert_eq!(state.shared_worker_channel_count(), 0);
8177 }
8178
8179 #[test]
8180 fn test_webview_state_shared_worker_scope_registration() {
8181 let mut state = BaoWebViewState::default();
8182 let id = SharedWorkerId {
8183 script_url: "sw.js".to_string(),
8184 name: "test".to_string(),
8185 };
8186 let config = SharedWorkerScopeConfig::default();
8187 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8188 state.register_shared_worker_scope(id.clone(), scope);
8189 assert_eq!(state.shared_worker_scope_count(), 1);
8190 assert!(state.shared_worker_scope(&id).is_some());
8191 }
8192
8193 #[test]
8194 fn test_webview_state_shared_worker_scope_get_mut() {
8195 let mut state = BaoWebViewState::default();
8196 let id = SharedWorkerId {
8197 script_url: "sw.js".to_string(),
8198 name: "test".to_string(),
8199 };
8200 let config = SharedWorkerScopeConfig::default();
8201 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8202 state.register_shared_worker_scope(id.clone(), scope);
8203 state.shared_worker_scope_mut(&id).unwrap().set_onconnect();
8204 assert!(state.shared_worker_scope(&id).unwrap().has_onconnect);
8205 }
8206
8207 #[test]
8208 fn test_webview_state_shared_worker_scope_remove() {
8209 let mut state = BaoWebViewState::default();
8210 let id = SharedWorkerId {
8211 script_url: "sw.js".to_string(),
8212 name: "test".to_string(),
8213 };
8214 let config = SharedWorkerScopeConfig::default();
8215 let scope = SharedWorkerGlobalScopeState::new(id.clone(), &config);
8216 state.register_shared_worker_scope(id.clone(), scope);
8217 let removed = state.remove_shared_worker_scope(&id);
8218 assert!(removed.is_some());
8219 assert_eq!(state.shared_worker_scope_count(), 0);
8220 }
8221
8222 #[test]
8223 fn test_webview_state_shared_worker_scopes_snapshot() {
8224 let mut state = BaoWebViewState::default();
8225 let id1 = SharedWorkerId {
8226 script_url: "sw1.js".to_string(),
8227 name: "a".to_string(),
8228 };
8229 let id2 = SharedWorkerId {
8230 script_url: "sw2.js".to_string(),
8231 name: "b".to_string(),
8232 };
8233 let config = SharedWorkerScopeConfig::default();
8234 state.register_shared_worker_scope(
8235 id1,
8236 SharedWorkerGlobalScopeState::new(
8237 SharedWorkerId {
8238 script_url: "sw1.js".to_string(),
8239 name: "a".to_string(),
8240 },
8241 &config,
8242 ),
8243 );
8244 state.register_shared_worker_scope(
8245 id2,
8246 SharedWorkerGlobalScopeState::new(
8247 SharedWorkerId {
8248 script_url: "sw2.js".to_string(),
8249 name: "b".to_string(),
8250 },
8251 &config,
8252 ),
8253 );
8254 let scopes = state.shared_worker_scopes();
8255 assert_eq!(scopes.len(), 2);
8256 }
8257
8258 #[test]
8259 fn test_webview_state_disconnect_shared_worker_clears_scopes() {
8260 let mut state = BaoWebViewState::default();
8261 let id = SharedWorkerId {
8262 script_url: "sw.js".to_string(),
8263 name: "test".to_string(),
8264 };
8265 let config = SharedWorkerScopeConfig::default();
8266 state.register_shared_worker_scope(
8267 id,
8268 SharedWorkerGlobalScopeState::new(
8269 SharedWorkerId {
8270 script_url: "sw.js".to_string(),
8271 name: "test".to_string(),
8272 },
8273 &config,
8274 ),
8275 );
8276 assert_eq!(state.shared_worker_scope_count(), 1);
8277 state.disconnect_shared_worker_ports();
8278 assert_eq!(state.shared_worker_scope_count(), 0);
8279 }
8280
8281 #[test]
8282 fn test_webview_state_set_shared_worker_scope_config() {
8283 let mut state = BaoWebViewState::default();
8284 let id = SharedWorkerId {
8285 script_url: "sw.js".to_string(),
8286 name: "test".to_string(),
8287 };
8288 let config = SharedWorkerScopeConfig::default();
8289 state.register_shared_worker_scope(
8290 id.clone(),
8291 SharedWorkerGlobalScopeState::new(id.clone(), &config),
8292 );
8293 assert!(state
8294 .shared_worker_scope(&id)
8295 .unwrap()
8296 .navigator()
8297 .user_agent
8298 .is_empty());
8299 let new_config = SharedWorkerScopeConfig {
8300 stealth_profile: None,
8301 user_agent: "Bao/1.0".to_string(),
8302 platform: "Linux".to_string(),
8303 hardware_concurrency: 8,
8304 language: "en-US".to_string(),
8305 languages: vec!["en-US".to_string()],
8306 };
8307 state.set_shared_worker_scope_config(&id, &new_config);
8308 assert_eq!(
8309 state
8310 .shared_worker_scope(&id)
8311 .unwrap()
8312 .navigator()
8313 .user_agent,
8314 "Bao/1.0"
8315 );
8316 }
8317
8318 #[test]
8322 fn test_delegate_route_shared_worker_new() {
8323 let delegate = BaoServoDelegate::new();
8324 let handle = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8325 let (returned, is_new) = delegate.route_shared_worker(handle);
8326 assert!(is_new);
8327 assert_eq!(returned.script_url, "sw.js");
8328 assert_eq!(delegate.shared_worker_count(), 1);
8329 }
8330
8331 #[test]
8332 fn test_delegate_route_shared_worker_existing() {
8333 let delegate = BaoServoDelegate::new();
8334 let handle1 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8335 let handle2 = SharedWorkerHandle::new("sw.js".to_string(), "myname".to_string());
8336 delegate.route_shared_worker(handle1);
8337 let (_, is_new) = delegate.route_shared_worker(handle2);
8338 assert!(
8339 !is_new,
8340 "same (url, name) should return existing, not create new"
8341 );
8342 assert_eq!(delegate.shared_worker_count(), 1);
8343 }
8344
8345 #[test]
8346 fn test_delegate_get_or_create_shared_worker_new() {
8347 let delegate = BaoServoDelegate::new();
8348 let (handle, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
8349 assert!(is_new);
8350 assert_eq!(handle.script_url, "sw.js");
8351 assert_eq!(handle.name, "myname");
8352 }
8353
8354 #[test]
8355 fn test_delegate_get_or_create_shared_worker_existing() {
8356 let delegate = BaoServoDelegate::new();
8357 delegate.get_or_create_shared_worker("sw.js", "myname");
8358 let (_, is_new) = delegate.get_or_create_shared_worker("sw.js", "myname");
8359 assert!(!is_new);
8360 assert_eq!(delegate.shared_worker_count(), 1);
8361 }
8362
8363 #[test]
8364 fn test_delegate_unregister_shared_worker() {
8365 let delegate = BaoServoDelegate::new();
8366 let id = SharedWorkerId {
8367 script_url: "sw.js".to_string(),
8368 name: "myname".to_string(),
8369 };
8370 delegate.get_or_create_shared_worker("sw.js", "myname");
8371 assert_eq!(delegate.shared_worker_count(), 1);
8372 let removed = delegate.unregister_shared_worker(&id);
8373 assert!(removed);
8374 assert_eq!(delegate.shared_worker_count(), 0);
8375 }
8376
8377 #[test]
8378 fn test_delegate_unregister_nonexistent_shared_worker() {
8379 let delegate = BaoServoDelegate::new();
8380 let id = SharedWorkerId {
8381 script_url: "sw.js".to_string(),
8382 name: "nonexistent".to_string(),
8383 };
8384 let removed = delegate.unregister_shared_worker(&id);
8385 assert!(!removed);
8386 }
8387
8388 #[test]
8389 fn test_delegate_all_shared_workers() {
8390 let delegate = BaoServoDelegate::new();
8391 delegate.get_or_create_shared_worker("sw1.js", "a");
8392 delegate.get_or_create_shared_worker("sw2.js", "b");
8393 let all = delegate.all_shared_workers();
8394 assert_eq!(all.len(), 2);
8395 }
8396
8397 #[test]
8398 fn test_shared_worker_cross_page_routing_full_lifecycle() {
8399 let delegate = BaoServoDelegate::new();
8402
8403 let (handle, is_new) = delegate.route_shared_worker(SharedWorkerHandle::new(
8405 "sw.js".to_string(),
8406 "shared".to_string(),
8407 ));
8408 assert!(is_new);
8409 assert_eq!(handle.connected_page_count(), 0);
8410
8411 let mut state1 = BaoWebViewState::default();
8413 let id = SharedWorkerId {
8414 script_url: "sw.js".to_string(),
8415 name: "shared".to_string(),
8416 };
8417 let config = SharedWorkerScopeConfig::default();
8418 state1.register_shared_worker_scope(
8419 id.clone(),
8420 SharedWorkerGlobalScopeState::new(id.clone(), &config),
8421 );
8422 state1.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
8423 assert_eq!(handle.connected_page_count(), 1);
8425 let endpoints1 = state1.add_shared_worker_port(id.clone());
8427
8428 let mut state2 = BaoWebViewState::default();
8430 state2.register_shared_worker_scope(
8431 id.clone(),
8432 SharedWorkerGlobalScopeState::new(id.clone(), &config),
8433 );
8434 state2.track_shared_worker_port(SharedWorkerPortRef::new(handle.clone()));
8435 let endpoints2 = state2.add_shared_worker_port(id.clone());
8436 assert_eq!(handle.connected_page_count(), 2);
8437
8438 let payload1 = StructuredClonePayload {
8440 data: vec![1],
8441 transferable_count: 0,
8442 };
8443 state1
8444 .post_to_worker_via_shared_port(&id, 0, payload1)
8445 .unwrap();
8446 let payload2 = StructuredClonePayload {
8447 data: vec![2],
8448 transferable_count: 0,
8449 };
8450 state2
8451 .post_to_worker_via_shared_port(&id, 0, payload2)
8452 .unwrap();
8453
8454 let rx1 = endpoints1.page_to_worker_rx.unwrap();
8456 let rx2 = endpoints2.page_to_worker_rx.unwrap();
8457 assert_eq!(rx1.try_recv().unwrap().data, vec![1]);
8458 assert_eq!(rx2.try_recv().unwrap().data, vec![2]);
8459
8460 let tx1 = endpoints1.worker_to_page_tx.unwrap();
8462 let tx2 = endpoints2.worker_to_page_tx.unwrap();
8463 tx1.send(WorkerStructuredMessage::metadata_only(
8464 WorkerId("sw.js".to_string()),
8465 WorkerMessageDirection::WorkerToPage,
8466 ))
8467 .unwrap();
8468 tx2.send(WorkerStructuredMessage::metadata_only(
8469 WorkerId("sw.js".to_string()),
8470 WorkerMessageDirection::WorkerToPage,
8471 ))
8472 .unwrap();
8473
8474 let (msgs1, disc1) = state1.drain_all_shared_worker_messages();
8476 assert_eq!(msgs1.len(), 1);
8477 assert!(disc1.is_empty());
8478 let (msgs2, disc2) = state2.drain_all_shared_worker_messages();
8480 assert_eq!(msgs2.len(), 1);
8481 assert!(disc2.is_empty());
8482
8483 state1.disconnect_shared_worker_ports();
8485 assert_eq!(handle.connected_page_count(), 1);
8487 assert!(!handle.is_closing());
8488
8489 assert_eq!(state2.shared_worker_port_count(), 1);
8491
8492 handle.close();
8494 handle.mark_terminated();
8495 assert!(handle.is_closing());
8496 assert!(handle.is_terminated());
8497
8498 state2.disconnect_shared_worker_ports();
8501 assert_eq!(handle.connected_page_count(), 0);
8502 delegate.reap_terminated_shared_workers();
8503 assert_eq!(delegate.shared_worker_count(), 0);
8504 }
8505
8506 #[test]
8511 fn test_service_worker_registration_id_equality() {
8512 let id1 = ServiceWorkerRegistrationId {
8513 script_url: "sw.js".to_string(),
8514 scope: "/".to_string(),
8515 };
8516 let id2 = ServiceWorkerRegistrationId {
8517 script_url: "sw.js".to_string(),
8518 scope: "/".to_string(),
8519 };
8520 let id3 = ServiceWorkerRegistrationId {
8521 script_url: "sw.js".to_string(),
8522 scope: "/app/".to_string(),
8523 };
8524 assert_eq!(id1, id2);
8525 assert_ne!(id1, id3);
8526 }
8527
8528 #[test]
8529 fn test_service_worker_handle_lifecycle() {
8530 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8531 assert!(!handle.is_closing());
8532 assert!(!handle.is_terminated());
8533 assert_eq!(
8534 handle.registration_state(),
8535 ServiceWorkerRegistrationState::Installing
8536 );
8537 assert!(!handle.is_intercepting_fetch());
8538 }
8539
8540 #[test]
8541 fn test_service_worker_handle_state_transitions() {
8542 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8543 handle.transition_state(ServiceWorkerRegistrationState::Installed);
8545 assert_eq!(
8546 handle.registration_state(),
8547 ServiceWorkerRegistrationState::Installed
8548 );
8549
8550 handle.transition_state(ServiceWorkerRegistrationState::Activating);
8552 assert_eq!(
8553 handle.registration_state(),
8554 ServiceWorkerRegistrationState::Activating
8555 );
8556
8557 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8559 handle.enable_fetch_interception();
8560 assert_eq!(
8561 handle.registration_state(),
8562 ServiceWorkerRegistrationState::Activated
8563 );
8564 assert!(handle.is_intercepting_fetch());
8565 assert_eq!(
8566 handle.fetch_intercept_mode(),
8567 ServiceWorkerFetchInterceptMode::Intercepting
8568 );
8569 }
8570
8571 #[test]
8572 fn test_service_worker_handle_terminate_disables_interception() {
8573 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8574 handle.enable_fetch_interception();
8575 assert!(handle.is_intercepting_fetch());
8576
8577 handle.terminate();
8579 assert!(handle.is_closing());
8580 assert!(!handle.is_intercepting_fetch());
8581 assert_eq!(
8582 handle.fetch_intercept_mode(),
8583 ServiceWorkerFetchInterceptMode::None
8584 );
8585 }
8586
8587 #[test]
8588 fn test_service_worker_scope_config_from_stealth_profile() {
8589 let profile = bao_stealth::StealthProfile::chrome_default();
8590 let config = ServiceWorkerScopeConfig::from(&profile);
8591 assert!(config.stealth_profile.is_some());
8592 assert_eq!(config.user_agent, profile.navigator.user_agent);
8593 assert_eq!(config.platform, profile.navigator.platform);
8594 assert_eq!(
8595 config.hardware_concurrency,
8596 profile.navigator.hardware_concurrency as usize
8597 );
8598 assert_eq!(config.language, profile.navigator.language);
8599 }
8600
8601 #[test]
8602 fn test_service_worker_global_scope_state() {
8603 let reg_id = ServiceWorkerRegistrationId {
8604 script_url: "sw.js".to_string(),
8605 scope: "/app/".to_string(),
8606 };
8607 let config = ServiceWorkerScopeConfig::default();
8608 let scope = ServiceWorkerGlobalScopeState::new(reg_id.clone(), &config);
8609
8610 assert!(!scope.has_fetch_handler);
8611 assert!(!scope.has_activate_handler);
8612 assert!(!scope.has_install_handler);
8613 assert!(!scope.has_message_handler);
8614 assert_eq!(scope.scope_url, "/app/");
8615 assert!(scope.is_url_in_scope("/app/page1"));
8616 assert!(scope.is_url_in_scope("/app/sub/page2"));
8617 assert!(!scope.is_url_in_scope("/other/page"));
8618 }
8619
8620 #[test]
8621 fn test_service_worker_global_scope_fetch_handler() {
8622 let reg_id = ServiceWorkerRegistrationId {
8623 script_url: "sw.js".to_string(),
8624 scope: "/".to_string(),
8625 };
8626 let config = ServiceWorkerScopeConfig::default();
8627 let mut scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8628
8629 scope.set_fetch_handler();
8630 assert!(scope.has_fetch_handler);
8631 assert!(scope.is_url_in_scope("/anything"));
8632 }
8633
8634 #[test]
8635 fn test_webview_state_service_worker_control() {
8636 let mut state = BaoWebViewState::default();
8637 assert!(!state.is_controlled_by_service_worker());
8638
8639 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8640 state.set_controlling_service_worker(handle);
8641 assert!(state.is_controlled_by_service_worker());
8642
8643 state.clear_controlling_service_worker();
8644 assert!(!state.is_controlled_by_service_worker());
8645 }
8646
8647 #[test]
8648 fn test_webview_state_service_worker_scope_matching() {
8649 let mut state = BaoWebViewState::default();
8650 assert!(!state.is_url_in_service_worker_scope("/app/page1"));
8651
8652 let reg_id = ServiceWorkerRegistrationId {
8653 script_url: "sw.js".to_string(),
8654 scope: "/app/".to_string(),
8655 };
8656 let config = ServiceWorkerScopeConfig::default();
8657 let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8658 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/app/".to_string(), None);
8659 state.set_controlling_service_worker(handle);
8660 state.register_service_worker_scope(scope);
8661
8662 assert!(state.is_url_in_service_worker_scope("/app/page1"));
8663 assert!(state.is_url_in_service_worker_scope("/app/sub/page2"));
8664 assert!(!state.is_url_in_service_worker_scope("/other/page"));
8665 }
8666
8667 #[test]
8668 fn test_delegate_service_worker_registration() {
8669 let delegate = BaoServoDelegate::new();
8670 assert_eq!(delegate.service_worker_count(), 0);
8671
8672 let (handle, is_new) = delegate.get_or_create_service_worker("sw.js", "/", None);
8673 assert!(is_new);
8674 assert_eq!(delegate.service_worker_count(), 1);
8675
8676 let (handle2, is_new2) = delegate.get_or_create_service_worker("sw.js", "/", None);
8678 assert!(!is_new2);
8679 assert_eq!(delegate.service_worker_count(), 1);
8680 }
8681
8682 #[test]
8683 fn test_delegate_find_service_worker_for_url() {
8684 let delegate = BaoServoDelegate::new();
8685
8686 let handle = delegate
8688 .get_or_create_service_worker("sw.js", "/app/", None)
8689 .0;
8690
8691 assert!(delegate.find_service_worker_for_url("/app/page1").is_none());
8693
8694 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8696 handle.enable_fetch_interception();
8697
8698 let found = delegate.find_service_worker_for_url("/app/page1");
8700 assert!(found.is_some());
8701 assert_eq!(found.unwrap().script_url, "sw.js");
8702
8703 assert!(delegate
8705 .find_service_worker_for_url("/other/page")
8706 .is_none());
8707 }
8708
8709 #[test]
8710 fn test_delegate_service_worker_unregistration() {
8711 let delegate = BaoServoDelegate::new();
8712 let (handle, _) = delegate.get_or_create_service_worker("sw.js", "/", None);
8713 assert_eq!(delegate.service_worker_count(), 1);
8714
8715 let id = handle.id();
8716 assert!(delegate.unregister_service_worker(&id));
8717 assert_eq!(delegate.service_worker_count(), 0);
8718
8719 assert!(!delegate.unregister_service_worker(&id));
8721 }
8722
8723 #[test]
8724 fn test_delegate_service_worker_stealth_consistency_no_violations() {
8725 let delegate = BaoServoDelegate::new();
8726 let profile = bao_stealth::StealthProfile::chrome_default();
8727
8728 let handle =
8730 ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), Some(profile.clone()));
8731 delegate.register_service_worker(handle);
8732 let all = delegate.all_service_workers();
8734 all[0].transition_state(ServiceWorkerRegistrationState::Activated);
8735 all[0].enable_fetch_interception();
8736
8737 let violations = delegate.verify_service_worker_stealth_consistency(&profile);
8738 assert!(violations.is_empty());
8739 }
8740
8741 #[test]
8742 fn test_delegate_service_worker_stealth_consistency_violation_no_profile() {
8743 let delegate = BaoServoDelegate::new();
8744 let profile = bao_stealth::StealthProfile::chrome_default();
8745
8746 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8748 delegate.register_service_worker(handle);
8749 let all = delegate.all_service_workers();
8750 all[0].transition_state(ServiceWorkerRegistrationState::Activated);
8751 all[0].enable_fetch_interception();
8752
8753 let violations = delegate.verify_service_worker_stealth_consistency(&profile);
8754 assert_eq!(violations.len(), 1);
8755 }
8756
8757 #[test]
8758 fn test_service_worker_persistent_lifecycle_across_page_navigation() {
8759 let delegate = BaoServoDelegate::new();
8763 let profile = bao_stealth::StealthProfile::chrome_default();
8764
8765 let (handle, is_new) =
8767 delegate.get_or_create_service_worker("sw.js", "/", Some(profile.clone()));
8768 assert!(is_new);
8769 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8770 handle.enable_fetch_interception();
8771
8772 let mut page_state = BaoWebViewState::default();
8774 page_state.set_controlling_service_worker(handle.clone());
8775 let reg_id = ServiceWorkerRegistrationId {
8776 script_url: "sw.js".to_string(),
8777 scope: "/".to_string(),
8778 };
8779 let config = ServiceWorkerScopeConfig::from(&profile);
8780 page_state
8781 .register_service_worker_scope(ServiceWorkerGlobalScopeState::new(reg_id, &config));
8782 assert!(page_state.is_controlled_by_service_worker());
8783
8784 page_state.clear_controlling_service_worker();
8786 assert!(!page_state.is_controlled_by_service_worker());
8787
8788 assert_eq!(delegate.service_worker_count(), 1);
8790 assert!(delegate.find_service_worker_for_url("/page2").is_some());
8791
8792 let mut page2_state = BaoWebViewState::default();
8794 page2_state.set_controlling_service_worker(handle.clone());
8795 assert!(page2_state.is_controlled_by_service_worker());
8796 }
8797
8798 #[test]
8799 fn test_service_worker_fetch_intercept_mode() {
8800 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8801 assert_eq!(
8802 handle.fetch_intercept_mode(),
8803 ServiceWorkerFetchInterceptMode::None
8804 );
8805
8806 handle.enable_fetch_interception();
8807 assert_eq!(
8808 handle.fetch_intercept_mode(),
8809 ServiceWorkerFetchInterceptMode::Intercepting
8810 );
8811
8812 handle.disable_fetch_interception();
8813 assert_eq!(
8814 handle.fetch_intercept_mode(),
8815 ServiceWorkerFetchInterceptMode::None
8816 );
8817 }
8818
8819 #[test]
8820 fn test_service_worker_registration_state_all_transitions() {
8821 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8822 assert_eq!(
8823 handle.registration_state(),
8824 ServiceWorkerRegistrationState::Installing
8825 );
8826
8827 handle.transition_state(ServiceWorkerRegistrationState::Installed);
8828 assert_eq!(
8829 handle.registration_state(),
8830 ServiceWorkerRegistrationState::Installed
8831 );
8832
8833 handle.transition_state(ServiceWorkerRegistrationState::Activating);
8834 assert_eq!(
8835 handle.registration_state(),
8836 ServiceWorkerRegistrationState::Activating
8837 );
8838
8839 handle.transition_state(ServiceWorkerRegistrationState::Activated);
8840 assert_eq!(
8841 handle.registration_state(),
8842 ServiceWorkerRegistrationState::Activated
8843 );
8844
8845 handle.transition_state(ServiceWorkerRegistrationState::Redundant);
8846 assert_eq!(
8847 handle.registration_state(),
8848 ServiceWorkerRegistrationState::Redundant
8849 );
8850 }
8851
8852 #[test]
8853 fn test_service_worker_navigator_from_scope_config() {
8854 let config = ServiceWorkerScopeConfig {
8855 stealth_profile: None,
8856 user_agent: "Mozilla/5.0 Test".to_string(),
8857 platform: "Linux x86_64".to_string(),
8858 hardware_concurrency: 4,
8859 language: "zh-CN".to_string(),
8860 languages: vec!["zh-CN".to_string(), "zh".to_string()],
8861 registering_page_url: "https://example.com/".to_string(),
8862 };
8863 let nav = WorkerNavigator::from_service_scope_config(&config);
8864 assert_eq!(nav.user_agent, "Mozilla/5.0 Test");
8865 assert_eq!(nav.platform, "Linux x86_64");
8866 assert_eq!(nav.hardware_concurrency, 4);
8867 assert_eq!(nav.language, "zh-CN");
8868 assert_eq!(nav.languages, vec!["zh-CN".to_string(), "zh".to_string()]);
8869 }
8870
8871 #[test]
8872 fn test_webview_state_service_worker_scope_config() {
8873 let mut state = BaoWebViewState::default();
8874 let reg_id = ServiceWorkerRegistrationId {
8875 script_url: "sw.js".to_string(),
8876 scope: "/".to_string(),
8877 };
8878 let config = ServiceWorkerScopeConfig::default();
8879 let scope = ServiceWorkerGlobalScopeState::new(reg_id, &config);
8880 let handle = ServiceWorkerHandle::new("sw.js".to_string(), "/".to_string(), None);
8881 state.set_controlling_service_worker(handle);
8882 state.register_service_worker_scope(scope);
8883
8884 let new_config = ServiceWorkerScopeConfig {
8885 user_agent: "Updated Agent".to_string(),
8886 ..ServiceWorkerScopeConfig::default()
8887 };
8888 state.set_service_worker_scope_config(&new_config);
8889 assert_eq!(
8890 state.service_worker_scope().unwrap().navigator().user_agent,
8891 "Updated Agent"
8892 );
8893 }
8894}