Skip to main content

playwright_rs/protocol/
har_options.rs

1// HAR recording options for Tracing::start_har.
2//
3// Kept in its own module (rather than in tracing.rs) so the pure
4// RecordHarOptions serialization is covered by mutation testing, while the
5// integration-only start_har/stop_har stay out of scope.
6
7use serde::Serialize;
8use serde_json::Value;
9
10/// How resource bodies are stored in a recorded HAR.
11///
12/// See: <https://playwright.dev/docs/api/class-tracing#tracing-start-har-option-content>
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "lowercase")]
15#[non_exhaustive]
16pub enum HarContent {
17    /// Do not store bodies (smallest HAR).
18    Omit,
19    /// Inline bodies into the HAR as base64 (the default for a non-`.zip` path).
20    Embed,
21    /// Store bodies as separate files / zip entries (the default for a `.zip` path).
22    Attach,
23}
24
25/// Level of detail recorded in a HAR.
26///
27/// See: <https://playwright.dev/docs/api/class-tracing#tracing-start-har-option-mode>
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
29#[serde(rename_all = "lowercase")]
30#[non_exhaustive]
31pub enum HarMode {
32    /// Record everything (default).
33    Full,
34    /// Record only essentials (size, timing) and omit headers/bodies/cookies.
35    Minimal,
36}
37
38/// Options for [`Tracing::start_har`](crate::protocol::Tracing::start_har).
39///
40/// # Example
41///
42/// ```
43/// use playwright_rs::{StartHarOptions, HarContent, HarMode};
44///
45/// let opts = StartHarOptions::default()
46///     .content(HarContent::Attach)
47///     .mode(HarMode::Minimal)
48///     .url_filter("**/api/**");
49/// ```
50///
51/// See: <https://playwright.dev/docs/api/class-tracing#tracing-start-har>
52#[derive(Debug, Clone, Default)]
53#[non_exhaustive]
54pub struct StartHarOptions {
55    /// How resource bodies are stored. Defaults to `Attach` for a `.zip` path,
56    /// `Embed` otherwise.
57    pub content: Option<HarContent>,
58    /// Level of detail. Defaults to [`HarMode::Full`].
59    pub mode: Option<HarMode>,
60    /// Glob pattern; only requests whose URL matches are recorded.
61    pub url_filter: Option<String>,
62    /// Directory to store `attach`-mode resource files in (for non-zip paths).
63    pub resources_dir: Option<String>,
64}
65
66impl StartHarOptions {
67    /// How resource bodies are stored (`Attach` / `Embed` / `Omit`).
68    pub fn content(mut self, content: HarContent) -> Self {
69        self.content = Some(content);
70        self
71    }
72    /// Level of detail (`Full` / `Minimal`).
73    pub fn mode(mut self, mode: HarMode) -> Self {
74        self.mode = Some(mode);
75        self
76    }
77    /// Glob pattern; only requests whose URL matches are recorded.
78    pub fn url_filter(mut self, url_filter: impl Into<String>) -> Self {
79        self.url_filter = Some(url_filter.into());
80        self
81    }
82    /// Directory to store `attach`-mode resource files in (for non-zip paths).
83    pub fn resources_dir(mut self, resources_dir: impl Into<String>) -> Self {
84        self.resources_dir = Some(resources_dir.into());
85        self
86    }
87
88    /// Build the protocol `RecordHarOptions` object for the given output path.
89    ///
90    /// `harPath` is intentionally omitted: setting it makes the driver write its
91    /// own archive at that path (appending `.zip`), which would duplicate the
92    /// file we already produce via `harExport` + unzip in `stop_har`. The path
93    /// here only selects the default `content` mode.
94    pub(crate) fn to_record_har_json(&self, path: &str) -> Value {
95        let is_zip = path.ends_with(".zip");
96        let content = self.content.unwrap_or(if is_zip {
97            HarContent::Attach
98        } else {
99            HarContent::Embed
100        });
101        let mode = self.mode.unwrap_or(HarMode::Full);
102
103        let mut o = serde_json::json!({});
104        o["content"] = serde_json::to_value(content).expect("serialize HarContent cannot fail");
105        o["mode"] = serde_json::to_value(mode).expect("serialize HarMode cannot fail");
106        if let Some(glob) = &self.url_filter {
107            o["urlGlob"] = serde_json::json!(glob);
108        }
109        if let Some(dir) = &self.resources_dir {
110            o["resourcesDir"] = serde_json::json!(dir);
111        }
112        o
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_start_har_options_zip_defaults_to_attach() {
122        let json = StartHarOptions::default().to_record_har_json("run.har.zip");
123        assert_eq!(json["content"], "attach");
124        assert_eq!(json["mode"], "full");
125        // harPath is deliberately not sent (avoids the driver double-writing).
126        assert!(json.get("harPath").is_none());
127    }
128
129    #[test]
130    fn test_start_har_options_plain_defaults_to_embed() {
131        let json = StartHarOptions::default().to_record_har_json("run.har");
132        assert_eq!(json["content"], "embed");
133    }
134
135    #[test]
136    fn test_start_har_options_setters() {
137        let opts = StartHarOptions::default()
138            .content(HarContent::Omit)
139            .mode(HarMode::Minimal)
140            .url_filter("**/api/**");
141        let json = opts.to_record_har_json("run.har");
142        assert_eq!(json["content"], "omit");
143        assert_eq!(json["mode"], "minimal");
144        assert_eq!(json["urlGlob"], "**/api/**");
145        assert!(json.get("resourcesDir").is_none());
146    }
147
148    #[test]
149    fn test_start_har_options_resources_dir_setter() {
150        let json = StartHarOptions::default()
151            .resources_dir("/tmp/har-resources")
152            .to_record_har_json("run.har");
153        assert_eq!(json["resourcesDir"], "/tmp/har-resources");
154    }
155}