use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use dpi::PhysicalSize;
use servo::{
AllowOrDenyRequest, ConsoleLogLevel, CreateNewWebViewRequest, DeviceIntPoint, DeviceIntRect,
DeviceIntSize, EmbedderControl, EmbedderControlId, InputEventId, InputEventResult, LoadStatus,
NavigationRequest, PermissionRequest, ScreenGeometry, ServoDelegate, ServoError, TraversalId,
WebView, WebViewDelegate,
};
use bao_cdp::servo_bridge::main_frame_id_for_target;
use bao_cdp::{BaoEvent, ConsoleMessage};
use bao_cdp_client::bridge::{ConsoleLevel, ServoEvent};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerScriptSource {
Inline(String),
Url(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WorkerScriptLoadResult {
pub source: String,
pub final_url: String,
pub mime_type: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerScriptLoadError {
NetworkError(String),
InvalidMimeType {
received: String,
url: String,
},
Utf8DecodeError(String),
InvalidUrl(String),
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WorkerScriptType {
Classic,
Module,
}
impl Default for WorkerScriptType {
fn default() -> Self {
WorkerScriptType::Classic
}
}
const JAVASCRIPT_MIME_TYPES: &[&str] = &[
"application/ecmascript",
"application/javascript",
"application/x-ecmascript",
"application/x-javascript",
"text/ecmascript",
"text/javascript",
"text/javascript1.0",
"text/javascript1.1",
"text/javascript1.2",
"text/javascript1.3",
"text/javascript1.4",
"text/javascript1.5",
"text/jscript",
"text/livescript",
"text/x-ecmascript",
"text/x-javascript",
];
pub fn is_javascript_mime_type(mime: &str) -> bool {
let base_type = mime.split(';').next().unwrap_or(mime).trim();
JAVASCRIPT_MIME_TYPES
.iter()
.any(|&valid| valid.eq_ignore_ascii_case(base_type))
}
#[derive(Debug, Clone)]
pub struct WorkerScriptLoader {
pub source: WorkerScriptSource,
pub script_type: WorkerScriptType,
}
impl WorkerScriptLoader {
pub fn inline(script: String, script_type: WorkerScriptType) -> Self {
WorkerScriptLoader {
source: WorkerScriptSource::Inline(script),
script_type,
}
}
pub fn url(url: String, script_type: WorkerScriptType) -> Self {
WorkerScriptLoader {
source: WorkerScriptSource::Url(url),
script_type,
}
}
pub fn from_source(source: WorkerScriptSource, script_type: WorkerScriptType) -> Self {
WorkerScriptLoader {
source,
script_type,
}
}
pub fn resolve(&self) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
match &self.source {
WorkerScriptSource::Inline(content) => {
Ok(WorkerScriptSource::Inline(content.clone()))
}
WorkerScriptSource::Url(url_str) => {
let parsed = url::Url::parse(url_str).map_err(|e| {
WorkerScriptLoadError::InvalidUrl(format!(
"Invalid Worker script URL '{}': {}",
url_str, e
))
})?;
if parsed.scheme() == "data" {
return Self::resolve_data_url(&parsed);
}
if parsed.scheme() == "blob" {
return Ok(WorkerScriptSource::Url(url_str.clone()));
}
if parsed.scheme() == "http" || parsed.scheme() == "https" {
return Ok(WorkerScriptSource::Url(url_str.clone()));
}
if parsed.scheme() == "file" {
return Self::resolve_file_url(&parsed);
}
Err(WorkerScriptLoadError::InvalidUrl(format!(
"Unsupported Worker script URL scheme '{}'",
parsed.scheme()
)))
}
}
}
fn resolve_data_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
let path = parsed.path();
let comma_pos = path.find(',').ok_or_else(|| {
WorkerScriptLoadError::InvalidUrl("data: URL missing comma separator".to_string())
})?;
let metadata = &path[..comma_pos];
let data = &path[comma_pos + 1..];
let (mime_part, is_base64) = if metadata.ends_with(";base64") {
(&metadata[..metadata.len() - 7], true)
} else if metadata.is_empty() {
("text/plain", false)
} else {
(metadata, false)
};
if !mime_part.is_empty() && !is_javascript_mime_type(mime_part) {
log::warn!(
"[WorkerScriptLoader] data: URL has non-JS MIME type '{}', loading anyway",
mime_part
);
}
let content = if is_base64 {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(data)
.map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"Failed to decode base64 data: URL: {}",
e
))
})?
} else {
decode_percent_encoded(data)?
};
let script = String::from_utf8(content).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"data: URL content is not valid UTF-8: {}",
e
))
})?;
Ok(WorkerScriptSource::Inline(script))
}
fn resolve_file_url(parsed: &url::Url) -> Result<WorkerScriptSource, WorkerScriptLoadError> {
let path = parsed.to_file_path().map_err(|_| {
WorkerScriptLoadError::InvalidUrl(format!(
"Cannot convert file: URL to path: {}",
parsed
))
})?;
let content = std::fs::read_to_string(&path).map_err(|e| {
WorkerScriptLoadError::NetworkError(format!(
"Failed to read Worker script file '{}': {}",
path.display(),
e
))
})?;
Ok(WorkerScriptSource::Inline(content))
}
pub fn validate_mime_type(mime_type: &str, url: &str) -> Result<(), WorkerScriptLoadError> {
if is_javascript_mime_type(mime_type) {
Ok(())
} else {
Err(WorkerScriptLoadError::InvalidMimeType {
received: mime_type.to_string(),
url: url.to_string(),
})
}
}
pub fn script_url(&self) -> Option<&str> {
match &self.source {
WorkerScriptSource::Url(url) => Some(url),
WorkerScriptSource::Inline(_) => None,
}
}
pub fn requires_fetch(&self) -> bool {
matches!(&self.source, WorkerScriptSource::Url(url)
if url.starts_with("http://") || url.starts_with("https://"))
}
pub fn load<F>(
&self,
stealth_profile: &Option<bao_stealth::StealthProfile>,
mut state_callback: F,
) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError>
where
F: FnMut(WorkerScriptLoadState),
{
state_callback(WorkerScriptLoadState::Pending);
let resolved = self.resolve()?;
let (source, final_url, mime_type) = match resolved {
WorkerScriptSource::Inline(content) => {
state_callback(WorkerScriptLoadState::Ready);
return Ok(WorkerScriptLoadResult {
source: content,
final_url: self.script_url().unwrap_or("inline").to_string(),
mime_type: None,
});
}
WorkerScriptSource::Url(url_str) => {
state_callback(WorkerScriptLoadState::Fetching);
let response = fetch_worker_script(&url_str, stealth_profile)
.map_err(|e| WorkerScriptLoadError::NetworkError(e))?;
state_callback(WorkerScriptLoadState::Validating);
let ct = response
.headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case("content-type"))
.map(|(_, v)| v.to_string());
if let Some(ref content_type) = ct {
Self::validate_mime_type(content_type, &url_str)?;
}
if ct.is_none() {
log::warn!(
"[WorkerScriptLoader] no Content-Type header for '{}', loading anyway",
url_str
);
}
state_callback(WorkerScriptLoadState::Decoding);
let source = String::from_utf8(response.body.to_vec()).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"Worker script response body is not valid UTF-8: {}",
e
))
})?;
(source, url_str, ct)
}
};
state_callback(WorkerScriptLoadState::Compiling);
state_callback(WorkerScriptLoadState::Ready);
Ok(WorkerScriptLoadResult {
source,
final_url,
mime_type,
})
}
pub fn load_simple(
&self,
stealth_profile: &Option<bao_stealth::StealthProfile>,
) -> Result<WorkerScriptLoadResult, WorkerScriptLoadError> {
self.load(stealth_profile, |_| {})
}
pub fn is_module(&self) -> bool {
matches!(self.script_type, WorkerScriptType::Module)
}
}
fn fetch_worker_script(
url: &str,
stealth_profile: &Option<bao_stealth::StealthProfile>,
) -> Result<WorkerScriptFetchResponse, String> {
use bun_http::Method;
use bun_runtime::stealth_http::stealth_http_request;
let result = stealth_http_request(
stealth_profile,
Method::GET,
url,
&[], None, )
.map_err(|e| format!("Failed to fetch Worker script from '{}': {}", url, e))?;
Ok(WorkerScriptFetchResponse {
status_code: result.status_code,
headers: result
.headers
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
body: result.body.to_vec(),
})
}
struct WorkerScriptFetchResponse {
status_code: u32,
headers: Vec<(String, String)>,
body: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkerScriptLoadState {
Pending,
Fetching,
Validating,
Decoding,
Compiling,
Ready,
Failed(WorkerScriptLoadError),
}
impl WorkerScriptLoadState {
pub fn is_ready(&self) -> bool {
matches!(self, WorkerScriptLoadState::Ready)
}
pub fn is_failed(&self) -> bool {
matches!(self, WorkerScriptLoadState::Failed(_))
}
pub fn is_loading(&self) -> bool {
!self.is_ready() && !self.is_failed()
}
}
fn decode_percent_encoded(data: &str) -> Result<Vec<u8>, WorkerScriptLoadError> {
let mut bytes = Vec::with_capacity(data.len());
let mut chars = data.chars();
while let Some(c) = chars.next() {
if c == '%' {
let hex: String = chars.by_ref().take(2).collect();
if hex.len() != 2 {
return Err(WorkerScriptLoadError::Utf8DecodeError(
"Incomplete percent-encoding in data: URL".to_string(),
));
}
let byte = u8::from_str_radix(&hex, 16).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"Invalid percent-encoding '%{}' in data: URL: {}",
hex, e
))
})?;
bytes.push(byte);
} else if c == '+' {
bytes.push(b' ');
} else {
let mut buf = [0u8; 4];
bytes.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
}
String::from_utf8(bytes.clone()).map_err(|e| {
WorkerScriptLoadError::Utf8DecodeError(format!(
"data: URL content is not valid UTF-8: {}",
e
))
})?;
Ok(bytes)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_worker_script_source_inline() {
let source = WorkerScriptSource::Inline("var x = 1;".to_string());
assert_eq!(source, WorkerScriptSource::Inline("var x = 1;".to_string()));
assert_ne!(source, WorkerScriptSource::Inline("var y = 2;".to_string()));
}
#[test]
fn test_worker_script_source_url() {
let source = WorkerScriptSource::Url("https://example.com/worker.js".to_string());
assert_eq!(
source,
WorkerScriptSource::Url("https://example.com/worker.js".to_string())
);
assert_ne!(
source,
WorkerScriptSource::Url("https://other.com/worker.js".to_string())
);
}
#[test]
fn test_worker_script_load_result() {
let result = WorkerScriptLoadResult {
source: "self.onmessage = function(e) {}".to_string(),
final_url: "https://example.com/worker.js".to_string(),
mime_type: Some("text/javascript".to_string()),
};
assert_eq!(result.source, "self.onmessage = function(e) {}");
assert_eq!(result.final_url, "https://example.com/worker.js");
assert_eq!(result.mime_type.as_deref(), Some("text/javascript"));
}
#[test]
fn test_worker_script_load_error_network() {
let err = WorkerScriptLoadError::NetworkError("404 Not Found".to_string());
assert_eq!(
err,
WorkerScriptLoadError::NetworkError("404 Not Found".to_string())
);
}
#[test]
fn test_worker_script_load_error_invalid_mime() {
let err = WorkerScriptLoadError::InvalidMimeType {
received: "text/html".to_string(),
url: "https://example.com/worker.js".to_string(),
};
match err {
WorkerScriptLoadError::InvalidMimeType { received, url } => {
assert_eq!(received, "text/html");
assert_eq!(url, "https://example.com/worker.js");
}
_ => panic!("expected InvalidMimeType"),
}
}
#[test]
fn test_worker_script_load_error_utf8() {
let err = WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string());
assert_eq!(
err,
WorkerScriptLoadError::Utf8DecodeError("invalid UTF-8".to_string())
);
}
#[test]
fn test_worker_script_load_error_invalid_url() {
let err = WorkerScriptLoadError::InvalidUrl("bad url".to_string());
assert_eq!(
err,
WorkerScriptLoadError::InvalidUrl("bad url".to_string())
);
}
#[test]
fn test_worker_script_load_error_cancelled() {
let err = WorkerScriptLoadError::Cancelled;
assert_eq!(err, WorkerScriptLoadError::Cancelled);
}
#[test]
fn test_worker_script_type_default_classic() {
assert_eq!(WorkerScriptType::default(), WorkerScriptType::Classic);
}
#[test]
fn test_worker_script_type_equality() {
assert_eq!(WorkerScriptType::Classic, WorkerScriptType::Classic);
assert_eq!(WorkerScriptType::Module, WorkerScriptType::Module);
assert_ne!(WorkerScriptType::Classic, WorkerScriptType::Module);
}
#[test]
fn test_is_javascript_mime_type_valid() {
assert!(is_javascript_mime_type("text/javascript"));
assert!(is_javascript_mime_type("application/javascript"));
assert!(is_javascript_mime_type("application/ecmascript"));
assert!(is_javascript_mime_type("application/x-javascript"));
assert!(is_javascript_mime_type("text/ecmascript"));
assert!(is_javascript_mime_type("text/x-javascript"));
assert!(is_javascript_mime_type("text/jscript"));
assert!(is_javascript_mime_type("text/livescript"));
}
#[test]
fn test_is_javascript_mime_type_case_insensitive() {
assert!(is_javascript_mime_type("Text/JavaScript"));
assert!(is_javascript_mime_type("APPLICATION/JAVASCRIPT"));
assert!(is_javascript_mime_type("text/JavaScript"));
}
#[test]
fn test_is_javascript_mime_type_with_charset() {
assert!(is_javascript_mime_type("text/javascript; charset=utf-8"));
assert!(is_javascript_mime_type(
"application/javascript;charset=utf-8"
));
}
#[test]
fn test_is_javascript_mime_type_invalid() {
assert!(!is_javascript_mime_type("text/html"));
assert!(!is_javascript_mime_type("application/json"));
assert!(!is_javascript_mime_type("text/plain"));
assert!(!is_javascript_mime_type("application/octet-stream"));
assert!(!is_javascript_mime_type("text/css"));
}
#[test]
fn test_worker_script_loader_inline() {
let loader =
WorkerScriptLoader::inline("var x = 1;".to_string(), WorkerScriptType::Classic);
assert!(loader.script_url().is_none());
assert!(!loader.requires_fetch());
let resolved = loader.resolve().unwrap();
assert_eq!(
resolved,
WorkerScriptSource::Inline("var x = 1;".to_string())
);
}
#[test]
fn test_worker_script_loader_url_https() {
let loader = WorkerScriptLoader::url(
"https://example.com/worker.js".to_string(),
WorkerScriptType::Classic,
);
assert_eq!(loader.script_url(), Some("https://example.com/worker.js"));
assert!(loader.requires_fetch());
let resolved = loader.resolve().unwrap();
assert_eq!(
resolved,
WorkerScriptSource::Url("https://example.com/worker.js".to_string())
);
}
#[test]
fn test_worker_script_loader_url_http() {
let loader = WorkerScriptLoader::url(
"http://localhost:3000/worker.js".to_string(),
WorkerScriptType::Module,
);
assert!(loader.requires_fetch());
assert_eq!(loader.script_type, WorkerScriptType::Module);
}
#[test]
fn test_worker_script_loader_url_invalid() {
let loader = WorkerScriptLoader::url("not a url".to_string(), WorkerScriptType::Classic);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidUrl(msg) => {
assert!(msg.contains("Invalid Worker script URL"));
}
_ => panic!("expected InvalidUrl error"),
}
}
#[test]
fn test_worker_script_loader_url_unsupported_scheme() {
let loader = WorkerScriptLoader::url(
"ftp://example.com/worker.js".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidUrl(msg) => {
assert!(msg.contains("Unsupported") || msg.contains("ftp"));
}
_ => panic!("expected InvalidUrl error"),
}
}
#[test]
fn test_worker_script_loader_data_url_text() {
let loader = WorkerScriptLoader::url(
"data:text/javascript,self.postMessage('hello')".to_string(),
WorkerScriptType::Classic,
);
let resolved = loader.resolve().unwrap();
match resolved {
WorkerScriptSource::Inline(script) => {
assert_eq!(script, "self.postMessage('hello')");
}
WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
}
}
#[test]
fn test_worker_script_loader_data_url_base64() {
let loader = WorkerScriptLoader::url(
"data:text/javascript;base64,dmFyIHggPSAxOw==".to_string(),
WorkerScriptType::Classic,
);
let resolved = loader.resolve().unwrap();
match resolved {
WorkerScriptSource::Inline(script) => {
assert_eq!(script, "var x = 1;");
}
WorkerScriptSource::Url(_) => panic!("expected inline source from data: URL"),
}
}
#[test]
fn test_worker_script_loader_data_url_invalid_base64() {
let loader = WorkerScriptLoader::url(
"data:text/javascript;base64,!!!invalid!!!".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
}
#[test]
fn test_worker_script_loader_data_url_missing_comma() {
let loader = WorkerScriptLoader::url(
"data:text/javascript".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidUrl(msg) => {
assert!(msg.contains("comma separator"));
}
_ => panic!("expected InvalidUrl error"),
}
}
#[test]
fn test_worker_script_loader_blob_url_passthrough() {
let loader = WorkerScriptLoader::url(
"blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string(),
WorkerScriptType::Classic,
);
let resolved = loader.resolve().unwrap();
assert_eq!(
resolved,
WorkerScriptSource::Url(
"blob:https://example.com/550e8400-e29b-41d4-a716-446655440000".to_string()
)
);
}
#[test]
fn test_worker_script_loader_from_source() {
let loader = WorkerScriptLoader::from_source(
WorkerScriptSource::Inline("code".to_string()),
WorkerScriptType::Module,
);
assert_eq!(loader.script_type, WorkerScriptType::Module);
assert!(loader.script_url().is_none());
}
#[test]
fn test_worker_script_loader_validate_mime_type_valid() {
assert!(WorkerScriptLoader::validate_mime_type(
"text/javascript",
"https://example.com/worker.js"
)
.is_ok());
assert!(WorkerScriptLoader::validate_mime_type(
"application/javascript",
"https://example.com/worker.js"
)
.is_ok());
}
#[test]
fn test_worker_script_loader_validate_mime_type_invalid() {
let result =
WorkerScriptLoader::validate_mime_type("text/html", "https://example.com/worker.js");
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::InvalidMimeType { received, url } => {
assert_eq!(received, "text/html");
assert_eq!(url, "https://example.com/worker.js");
}
_ => panic!("expected InvalidMimeType error"),
}
}
#[test]
fn test_worker_script_load_state_transitions() {
let mut state = WorkerScriptLoadState::Pending;
assert!(state.is_loading());
assert!(!state.is_ready());
assert!(!state.is_failed());
state = WorkerScriptLoadState::Fetching;
assert!(state.is_loading());
state = WorkerScriptLoadState::Validating;
assert!(state.is_loading());
state = WorkerScriptLoadState::Decoding;
assert!(state.is_loading());
state = WorkerScriptLoadState::Compiling;
assert!(state.is_loading());
state = WorkerScriptLoadState::Ready;
assert!(!state.is_loading());
assert!(state.is_ready());
state = WorkerScriptLoadState::Failed(WorkerScriptLoadError::NetworkError(
"timeout".to_string(),
));
assert!(!state.is_loading());
assert!(state.is_failed());
}
#[test]
fn test_webview_state_worker_script_load_state_registration() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
assert_eq!(state.worker_script_load_state_count(), 1);
assert!(state.worker_script_load_state(&worker_id).is_some());
assert_eq!(
state.worker_script_load_state(&worker_id).unwrap(),
&WorkerScriptLoadState::Pending
);
}
#[test]
fn test_webview_state_worker_script_load_state_update() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
assert_eq!(
state.worker_script_load_state(&worker_id).unwrap(),
&WorkerScriptLoadState::Fetching
);
}
#[test]
fn test_webview_state_worker_script_load_state_remove() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("worker1.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Ready);
let removed = state.remove_worker_script_load_state(&worker_id);
assert!(removed.is_some());
assert_eq!(removed.unwrap(), WorkerScriptLoadState::Ready);
assert_eq!(state.worker_script_load_state_count(), 0);
}
#[test]
fn test_webview_state_terminate_clears_script_load_states() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.register_worker_script_load_state(
WorkerId("worker1.js".to_string()),
WorkerScriptLoadState::Fetching,
);
assert_eq!(state.worker_script_load_state_count(), 1);
state.terminate_all_workers();
assert_eq!(state.worker_script_load_state_count(), 0);
}
#[test]
fn test_webview_state_reap_terminated_worker_script_load_states() {
let mut state = BaoWebViewState::default();
state.track_worker(WorkerHandle::new("worker1.js".to_string()));
state.track_worker(WorkerHandle::new("worker2.js".to_string()));
state.register_worker_script_load_state(
WorkerId("worker1.js".to_string()),
WorkerScriptLoadState::Ready,
);
state.register_worker_script_load_state(
WorkerId("worker2.js".to_string()),
WorkerScriptLoadState::Fetching,
);
state.active_workers[0].handle().terminate();
state.active_workers[0].handle().mark_terminated();
state.reap_terminated_workers();
assert_eq!(state.worker_script_load_state_count(), 1);
assert!(state
.worker_script_load_state(&WorkerId("worker2.js".to_string()))
.is_some());
}
#[test]
fn test_worker_script_loader_file_url() {
let temp_dir = std::env::temp_dir();
let temp_file = temp_dir.join("bao_test_worker_script.js");
std::fs::write(&temp_file, "var x = 42;").unwrap();
let file_url = format!("file://{}", temp_file.display());
let loader = WorkerScriptLoader::url(file_url, WorkerScriptType::Classic);
let resolved = loader.resolve().unwrap();
match resolved {
WorkerScriptSource::Inline(script) => {
assert_eq!(script, "var x = 42;");
}
WorkerScriptSource::Url(_) => panic!("expected inline source from file: URL"),
}
let _ = std::fs::remove_file(&temp_file);
}
#[test]
fn test_worker_script_loader_file_url_not_found() {
let loader = WorkerScriptLoader::url(
"file:///nonexistent/path/worker.js".to_string(),
WorkerScriptType::Classic,
);
let result = loader.resolve();
assert!(result.is_err());
match result.unwrap_err() {
WorkerScriptLoadError::NetworkError(msg) => {
assert!(msg.contains("Failed to read") || msg.contains("No such file"));
}
_ => panic!("expected NetworkError for missing file"),
}
}
#[test]
fn test_worker_script_loader_full_pipeline_states() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("https://example.com/worker.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
assert!(state
.worker_script_load_state(&worker_id)
.unwrap()
.is_loading());
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Fetching);
assert!(matches!(
state.worker_script_load_state(&worker_id).unwrap(),
WorkerScriptLoadState::Fetching
));
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Validating);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Decoding);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Compiling);
state.update_worker_script_load_state(&worker_id, WorkerScriptLoadState::Ready);
assert!(state
.worker_script_load_state(&worker_id)
.unwrap()
.is_ready());
}
#[test]
fn test_worker_script_loader_pipeline_failure() {
let mut state = BaoWebViewState::default();
let worker_id = WorkerId("https://example.com/bad-worker.js".to_string());
state.register_worker_script_load_state(worker_id.clone(), WorkerScriptLoadState::Pending);
state.update_worker_script_load_state(
&worker_id,
WorkerScriptLoadState::Failed(WorkerScriptLoadError::InvalidMimeType {
received: "text/html".to_string(),
url: "https://example.com/bad-worker.js".to_string(),
}),
);
assert!(state
.worker_script_load_state(&worker_id)
.unwrap()
.is_failed());
}
}