use crate::browser::views::{CoverageRange, CoverageResult};
use crate::error::Result;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct CoverageManager {
client: Arc<crate::browser::cdp::CdpClient>,
}
impl CoverageManager {
pub fn new(client: Arc<crate::browser::cdp::CdpClient>) -> Self {
Self { client }
}
pub async fn start_js_coverage(&self) -> Result<()> {
let _ = self
.client
.send_command("Profiler.enable", serde_json::json!({}))
.await;
self.client
.send_command(
"Profiler.startPreciseCoverage",
serde_json::json!({
"callCount": false,
"detailed": true,
}),
)
.await?;
Ok(())
}
pub async fn start_css_coverage(&self) -> Result<()> {
let _ = self
.client
.send_command("CSS.enable", serde_json::json!({}))
.await;
self.client
.send_command("CSS.startRuleUsageTracking", serde_json::json!({}))
.await?;
Ok(())
}
pub async fn stop_js_coverage(&self) -> Result<Vec<CoverageResult>> {
let result = self
.client
.send_command("Profiler.takePreciseCoverage", serde_json::json!({}))
.await?;
let entries = result
.get("result")
.and_then(|v| v.as_array())
.ok_or_else(|| crate::error::BrowsingError::Browser("No coverage result".to_string()))?;
let mut results = Vec::new();
for entry in entries {
let url = entry
.get("url")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let text_length = entry
.get("text")
.and_then(|v| v.as_str())
.map(|s| s.len() as u32)
.unwrap_or(0);
let ranges: Vec<CoverageRange> = entry
.get("functions")
.and_then(|v| v.as_array())
.map(|funcs| {
let mut all_ranges = Vec::new();
for func in funcs {
if let Some(ranges_arr) = func.get("ranges").and_then(|v| v.as_array()) {
for r in ranges_arr {
if let (Some(start), Some(end), Some(count)) = (
r.get("startOffset").and_then(|v| v.as_u64()),
r.get("endOffset").and_then(|v| v.as_u64()),
r.get("count").and_then(|v| v.as_u64()),
) {
all_ranges.push(CoverageRange {
start_offset: start as u32,
end_offset: end as u32,
count: count as u32,
});
}
}
}
}
all_ranges
})
.unwrap_or_default();
results.push(CoverageResult {
url,
content_type: "script".to_string(),
text_length,
ranges,
});
}
let _ = self
.client
.send_command("Profiler.stopPreciseCoverage", serde_json::json!({}))
.await;
Ok(results)
}
pub async fn stop_css_coverage(&self) -> Result<Vec<CoverageResult>> {
let result = self
.client
.send_command("CSS.stopRuleUsageTracking", serde_json::json!({}))
.await?;
let usages = result
.get("ruleUsage")
.and_then(|v| v.as_array())
.ok_or_else(|| crate::error::BrowsingError::Browser("No CSS rule usage".to_string()))?;
let mut results: Vec<CoverageResult> = Vec::new();
for usage in usages {
let style_sheet_id = usage
.get("styleSheetId")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let start = usage
.get("startOffset")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let end = usage
.get("endOffset")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32;
let used = usage
.get("used")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let entry = results.iter_mut().find(|r| r.url == style_sheet_id);
if let Some(entry) = entry {
if used {
entry.ranges.push(CoverageRange {
start_offset: start,
end_offset: end,
count: 1,
});
}
} else {
let mut ranges = Vec::new();
if used {
ranges.push(CoverageRange {
start_offset: start,
end_offset: end,
count: 1,
});
}
results.push(CoverageResult {
url: style_sheet_id,
content_type: "css".to_string(),
text_length: 0, ranges,
});
}
}
Ok(results)
}
pub async fn start_all(&self) -> Result<()> {
self.start_js_coverage().await?;
self.start_css_coverage().await?;
Ok(())
}
pub async fn stop_all(&self) -> Result<Vec<CoverageResult>> {
let mut js = self.stop_js_coverage().await?;
let mut css = self.stop_css_coverage().await?;
js.append(&mut css);
Ok(js)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_coverage_range_creation() {
let r = CoverageRange {
start_offset: 10,
end_offset: 50,
count: 1,
};
assert_eq!(r.start_offset, 10);
assert_eq!(r.end_offset, 50);
}
#[test]
fn test_coverage_result_creation() {
let result = CoverageResult {
url: "https://example.com/app.js".to_string(),
content_type: "script".to_string(),
text_length: 1000,
ranges: vec![CoverageRange {
start_offset: 0,
end_offset: 100,
count: 1,
}],
};
assert_eq!(result.url, "https://example.com/app.js");
assert_eq!(result.ranges.len(), 1);
}
#[test]
fn test_coverage_result_serialization() {
let result = CoverageResult {
url: "app.js".to_string(),
content_type: "script".to_string(),
text_length: 500,
ranges: vec![],
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("app.js"));
assert!(json.contains("script"));
}
}