playwright_cdp/
coverage.rs1use crate::cdp::session::CdpSession;
14use crate::error::Result;
15use serde_json::{json, Value};
16use std::sync::Arc;
17
18#[derive(Clone)]
22pub struct Coverage {
23 inner: Arc<CoverageInner>,
24}
25
26struct CoverageInner {
27 session: CdpSession,
29}
30
31#[derive(Debug, Clone, Default)]
33#[non_exhaustive]
34pub struct JSCoverageStartOptions {
35 pub report_anonymous: Option<bool>,
38 pub include_block_coverage: Option<bool>,
41}
42
43impl JSCoverageStartOptions {
44 pub fn report_anonymous(mut self, v: bool) -> Self {
45 self.report_anonymous = Some(v);
46 self
47 }
48 pub fn include_block_coverage(mut self, v: bool) -> Self {
49 self.include_block_coverage = Some(v);
50 self
51 }
52}
53
54#[derive(Debug, Clone, Default)]
56#[non_exhaustive]
57pub struct JSCoverageEntry {
58 pub url: String,
60 pub script_id: String,
62 pub functions: Vec<JSCoverageFunction>,
64}
65
66#[derive(Debug, Clone, Default)]
68#[non_exhaustive]
69pub struct JSCoverageFunction {
70 pub function_name: String,
72 pub is_block_coverage: bool,
74 pub ranges: Vec<JSCoverageRange>,
76}
77
78#[derive(Debug, Clone, Default)]
80#[non_exhaustive]
81pub struct JSCoverageRange {
82 pub start_offset: i64,
84 pub end_offset: i64,
86 pub call_count: i64,
88}
89
90#[derive(Debug, Clone, Default)]
92#[non_exhaustive]
93pub struct JSCoverageResult {
94 pub entries: Vec<JSCoverageEntry>,
96}
97
98#[derive(Debug, Clone, Default)]
100#[non_exhaustive]
101pub struct CSSCoverageEntry {
102 pub style_sheet_id: String,
104 pub rules: Vec<CSSCoverageRule>,
106}
107
108#[derive(Debug, Clone, Default)]
110#[non_exhaustive]
111pub struct CSSCoverageRule {
112 pub style_sheet_id: String,
114 pub start_offset: i64,
116 pub end_offset: i64,
118 pub used: bool,
120}
121
122#[derive(Debug, Clone, Default)]
124#[non_exhaustive]
125pub struct CSSCoverageResult {
126 pub entries: Vec<CSSCoverageEntry>,
128}
129
130impl Coverage {
131 pub(crate) fn new(session: CdpSession) -> Self {
132 Self {
133 inner: Arc::new(CoverageInner { session }),
134 }
135 }
136
137 pub async fn start_js_coverage(
146 &self,
147 options: Option<JSCoverageStartOptions>,
148 ) -> Result<()> {
149 self.inner.session.send("Profiler.enable", json!({})).await?;
150 let opts = options.unwrap_or_default();
151 let detailed = opts.include_block_coverage.unwrap_or(true);
152 self.inner
153 .session
154 .send(
155 "Profiler.startPreciseCoverage",
156 json!({ "callCount": true, "detailed": detailed }),
157 )
158 .await?;
159 Ok(())
160 }
161
162 pub async fn stop_js_coverage(&self) -> Result<JSCoverageResult> {
166 let resp: Value = self
167 .inner
168 .session
169 .send("Profiler.takePreciseCoverage", json!({}))
170 .await?;
171
172 let _ = self
175 .inner
176 .session
177 .send("Profiler.stopPreciseCoverage", json!({}))
178 .await;
179 let _ = self.inner.session.send("Profiler.disable", json!({})).await;
180
181 let entries = resp
182 .get("result")
183 .and_then(|v| v.as_array())
184 .map(|a| parse_js_entries(a))
185 .unwrap_or_default();
186
187 Ok(JSCoverageResult { entries })
188 }
189
190 pub async fn start_css_coverage(&self) -> Result<()> {
195 self.inner.session.send("CSS.enable", json!({})).await?;
196 self.inner
197 .session
198 .send("CSS.startRuleUsageTracking", json!({}))
199 .await?;
200 Ok(())
201 }
202
203 pub async fn stop_css_coverage(&self) -> Result<CSSCoverageResult> {
207 let resp: Value = self
208 .inner
209 .session
210 .send("CSS.stopRuleUsageTracking", json!({}))
211 .await?;
212
213 let _ = self.inner.session.send("CSS.disable", json!({})).await;
215
216 let rules = resp
217 .get("ruleUsage")
218 .and_then(|v| v.as_array())
219 .map(|a| parse_css_rules(a))
220 .unwrap_or_default();
221
222 let mut entries: Vec<CSSCoverageEntry> = Vec::new();
224 for rule in rules {
225 if let Some(e) = entries.iter_mut().find(|e| e.style_sheet_id == rule.style_sheet_id)
226 {
227 e.rules.push(rule);
228 } else {
229 entries.push(CSSCoverageEntry {
230 style_sheet_id: rule.style_sheet_id.clone(),
231 rules: vec![rule],
232 });
233 }
234 }
235
236 Ok(CSSCoverageResult { entries })
237 }
238}
239
240fn parse_js_entries(arr: &[Value]) -> Vec<JSCoverageEntry> {
242 arr.iter()
243 .filter_map(|s| {
244 let url = s.get("url").and_then(|v| v.as_str()).unwrap_or("").to_string();
245 let script_id = s
246 .get("scriptId")
247 .and_then(|v| v.as_str())
248 .unwrap_or("")
249 .to_string();
250 let functions = s
251 .get("functions")
252 .and_then(|v| v.as_array())
253 .map(|fs| fs.iter().filter_map(parse_js_function).collect())
254 .unwrap_or_default();
255 Some(JSCoverageEntry {
256 url,
257 script_id,
258 functions,
259 })
260 })
261 .collect()
262}
263
264fn parse_js_function(f: &Value) -> Option<JSCoverageFunction> {
266 let function_name = f
267 .get("functionName")
268 .and_then(|v| v.as_str())
269 .unwrap_or("")
270 .to_string();
271 let is_block_coverage = f
272 .get("isBlockCoverage")
273 .and_then(|v| v.as_bool())
274 .unwrap_or(false);
275 let ranges = f
276 .get("ranges")
277 .and_then(|v| v.as_array())
278 .map(|rs| {
279 rs.iter()
280 .filter_map(|r| {
281 Some(JSCoverageRange {
282 start_offset: r.get("startOffset").and_then(|v| v.as_i64())?,
283 end_offset: r.get("endOffset").and_then(|v| v.as_i64())?,
284 call_count: r.get("callCount").and_then(|v| v.as_i64()).unwrap_or(0),
285 })
286 })
287 .collect()
288 })
289 .unwrap_or_default();
290 Some(JSCoverageFunction {
291 function_name,
292 is_block_coverage,
293 ranges,
294 })
295}
296
297fn parse_css_rules(arr: &[Value]) -> Vec<CSSCoverageRule> {
299 arr.iter()
300 .filter_map(|r| {
301 Some(CSSCoverageRule {
302 style_sheet_id: r.get("styleSheetId").and_then(|v| v.as_str())?.to_string(),
303 start_offset: r.get("startOffset").and_then(|v| v.as_i64())?,
304 end_offset: r.get("endOffset").and_then(|v| v.as_i64())?,
305 used: r.get("used").and_then(|v| v.as_bool()).unwrap_or(false),
306 })
307 })
308 .collect()
309}