use crate::cdp::session::CdpSession;
use crate::error::Result;
use serde_json::{json, Value};
use std::sync::Arc;
#[derive(Clone)]
pub struct Coverage {
inner: Arc<CoverageInner>,
}
struct CoverageInner {
session: CdpSession,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct JSCoverageStartOptions {
pub report_anonymous: Option<bool>,
pub include_block_coverage: Option<bool>,
}
impl JSCoverageStartOptions {
pub fn report_anonymous(mut self, v: bool) -> Self {
self.report_anonymous = Some(v);
self
}
pub fn include_block_coverage(mut self, v: bool) -> Self {
self.include_block_coverage = Some(v);
self
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct JSCoverageEntry {
pub url: String,
pub script_id: String,
pub functions: Vec<JSCoverageFunction>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct JSCoverageFunction {
pub function_name: String,
pub is_block_coverage: bool,
pub ranges: Vec<JSCoverageRange>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct JSCoverageRange {
pub start_offset: i64,
pub end_offset: i64,
pub call_count: i64,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct JSCoverageResult {
pub entries: Vec<JSCoverageEntry>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CSSCoverageEntry {
pub style_sheet_id: String,
pub rules: Vec<CSSCoverageRule>,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CSSCoverageRule {
pub style_sheet_id: String,
pub start_offset: i64,
pub end_offset: i64,
pub used: bool,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct CSSCoverageResult {
pub entries: Vec<CSSCoverageEntry>,
}
impl Coverage {
pub(crate) fn new(session: CdpSession) -> Self {
Self {
inner: Arc::new(CoverageInner { session }),
}
}
pub async fn start_js_coverage(
&self,
options: Option<JSCoverageStartOptions>,
) -> Result<()> {
self.inner.session.send("Profiler.enable", json!({})).await?;
let opts = options.unwrap_or_default();
let detailed = opts.include_block_coverage.unwrap_or(true);
self.inner
.session
.send(
"Profiler.startPreciseCoverage",
json!({ "callCount": true, "detailed": detailed }),
)
.await?;
Ok(())
}
pub async fn stop_js_coverage(&self) -> Result<JSCoverageResult> {
let resp: Value = self
.inner
.session
.send("Profiler.takePreciseCoverage", json!({}))
.await?;
let _ = self
.inner
.session
.send("Profiler.stopPreciseCoverage", json!({}))
.await;
let _ = self.inner.session.send("Profiler.disable", json!({})).await;
let entries = resp
.get("result")
.and_then(|v| v.as_array())
.map(|a| parse_js_entries(a))
.unwrap_or_default();
Ok(JSCoverageResult { entries })
}
pub async fn start_css_coverage(&self) -> Result<()> {
self.inner.session.send("CSS.enable", json!({})).await?;
self.inner
.session
.send("CSS.startRuleUsageTracking", json!({}))
.await?;
Ok(())
}
pub async fn stop_css_coverage(&self) -> Result<CSSCoverageResult> {
let resp: Value = self
.inner
.session
.send("CSS.stopRuleUsageTracking", json!({}))
.await?;
let _ = self.inner.session.send("CSS.disable", json!({})).await;
let rules = resp
.get("ruleUsage")
.and_then(|v| v.as_array())
.map(|a| parse_css_rules(a))
.unwrap_or_default();
let mut entries: Vec<CSSCoverageEntry> = Vec::new();
for rule in rules {
if let Some(e) = entries.iter_mut().find(|e| e.style_sheet_id == rule.style_sheet_id)
{
e.rules.push(rule);
} else {
entries.push(CSSCoverageEntry {
style_sheet_id: rule.style_sheet_id.clone(),
rules: vec![rule],
});
}
}
Ok(CSSCoverageResult { entries })
}
}
fn parse_js_entries(arr: &[Value]) -> Vec<JSCoverageEntry> {
arr.iter()
.filter_map(|s| {
let url = s.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string();
let script_id = s
.get("scriptId")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let functions = s
.get("functions")
.and_then(|v| v.as_array())
.map(|fs| fs.iter().filter_map(parse_js_function).collect())
.unwrap_or_default();
Some(JSCoverageEntry {
url,
script_id,
functions,
})
})
.collect()
}
fn parse_js_function(f: &Value) -> Option<JSCoverageFunction> {
let function_name = f
.get("functionName")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let is_block_coverage = f
.get("isBlockCoverage")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let ranges = f
.get("ranges")
.and_then(|v| v.as_array())
.map(|rs| {
rs.iter()
.filter_map(|r| {
Some(JSCoverageRange {
start_offset: r.get("startOffset").and_then(|v| v.as_i64())?,
end_offset: r.get("endOffset").and_then(|v| v.as_i64())?,
call_count: r.get("callCount").and_then(|v| v.as_i64()).unwrap_or(0),
})
})
.collect()
})
.unwrap_or_default();
Some(JSCoverageFunction {
function_name,
is_block_coverage,
ranges,
})
}
fn parse_css_rules(arr: &[Value]) -> Vec<CSSCoverageRule> {
arr.iter()
.filter_map(|r| {
Some(CSSCoverageRule {
style_sheet_id: r.get("styleSheetId").and_then(|v| v.as_str())?.to_string(),
start_offset: r.get("startOffset").and_then(|v| v.as_i64())?,
end_offset: r.get("endOffset").and_then(|v| v.as_i64())?,
used: r.get("used").and_then(|v| v.as_bool()).unwrap_or(false),
})
})
.collect()
}