Skip to main content

playwright_rs/protocol/
local_utils.rs

1// Copyright 2026 Paul Adamson
2// Licensed under the Apache License, Version 2.0
3
4use crate::error::Result;
5use crate::server::channel::Channel;
6use crate::server::channel_owner::{
7    ChannelOwner, ChannelOwnerImpl, DisposeReason, ParentOrConnection,
8};
9use crate::server::connection::ConnectionLike;
10use base64::Engine;
11use serde::Deserialize;
12use serde_json::Value;
13use std::any::Any;
14use std::sync::Arc;
15
16/// LocalUtils protocol object
17///
18/// Provides client-side utility operations: HAR file replay, zip, tracing helpers.
19#[derive(Clone)]
20pub struct LocalUtils {
21    base: ChannelOwnerImpl,
22}
23
24impl LocalUtils {
25    pub fn new(
26        parent: ParentOrConnection,
27        type_name: String,
28        guid: Arc<str>,
29        initializer: Value,
30    ) -> Result<Self> {
31        Ok(Self {
32            base: ChannelOwnerImpl::new(parent, type_name, guid, initializer),
33        })
34    }
35
36    /// Opens a HAR file and returns an opaque HAR ID for subsequent lookups.
37    ///
38    /// Sends `"harOpen"` RPC with the file path.
39    pub async fn har_open(&self, file: &str) -> Result<String> {
40        #[derive(Deserialize)]
41        struct HarOpenResult {
42            #[serde(rename = "harId")]
43            har_id: String,
44        }
45        let result: HarOpenResult = self
46            .channel()
47            .send("harOpen", serde_json::json!({ "file": file }))
48            .await?;
49        Ok(result.har_id)
50    }
51
52    /// Looks up a request in the opened HAR archive.
53    ///
54    /// Returns an action object describing how the request should be fulfilled,
55    /// redirected, or allowed to fall through.
56    pub async fn har_lookup(
57        &self,
58        har_id: &str,
59        url: &str,
60        method: &str,
61        headers: Vec<serde_json::Value>,
62        post_data: Option<&[u8]>,
63        is_navigation_request: bool,
64    ) -> Result<HarLookupResult> {
65        let mut params = serde_json::json!({
66            "harId": har_id,
67            "url": url,
68            "method": method,
69            "headers": headers,
70            "isNavigationRequest": is_navigation_request,
71        });
72
73        // Only include postData when present — the server rejects null.
74        if let Some(data) = post_data {
75            let encoded = base64::engine::general_purpose::STANDARD.encode(data);
76            params["postData"] = serde_json::Value::String(encoded);
77        }
78
79        self.channel().send("harLookup", params).await
80    }
81
82    /// Closes a previously opened HAR archive.
83    pub async fn har_close(&self, har_id: &str) -> Result<()> {
84        self.channel()
85            .send_no_result("harClose", serde_json::json!({ "harId": har_id }))
86            .await
87    }
88
89    /// Extracts the `.har` JSON (and any attached resources) from a HAR `.zip`
90    /// archive into `har_file`. Used when exporting a HAR to a non-`.zip` path.
91    pub async fn har_unzip(
92        &self,
93        zip_file: &str,
94        har_file: &str,
95        resources_dir: Option<&str>,
96    ) -> Result<()> {
97        let mut params = serde_json::json!({ "zipFile": zip_file, "harFile": har_file });
98        if let Some(dir) = resources_dir {
99            params["resourcesDir"] = serde_json::json!(dir);
100        }
101        self.channel().send_no_result("harUnzip", params).await
102    }
103}
104
105/// Result from a `harLookup` RPC call.
106///
107/// Describes whether the request was found in the HAR and how to respond.
108#[derive(Debug, Deserialize)]
109#[non_exhaustive]
110pub struct HarLookupResult {
111    /// Action to take: `"fulfill"`, `"redirect"`, `"fallback"`, or `"error"`.
112    pub action: String,
113
114    /// For `"redirect"`: the URL to redirect to.
115    #[serde(rename = "redirectURL")]
116    pub redirect_url: Option<String>,
117
118    /// For `"fulfill"`: HTTP status code.
119    pub status: Option<u16>,
120
121    /// For `"fulfill"`: HTTP headers as `[{"name": ..., "value": ...}]`.
122    pub headers: Option<Vec<serde_json::Value>>,
123
124    /// For `"fulfill"`: Base64-encoded response body.
125    pub body: Option<String>,
126}
127
128impl ChannelOwner for LocalUtils {
129    fn guid(&self) -> &str {
130        self.base.guid()
131    }
132
133    fn type_name(&self) -> &str {
134        self.base.type_name()
135    }
136
137    fn parent(&self) -> Option<Arc<dyn ChannelOwner>> {
138        self.base.parent()
139    }
140
141    fn connection(&self) -> Arc<dyn ConnectionLike> {
142        self.base.connection()
143    }
144
145    fn initializer(&self) -> &Value {
146        self.base.initializer()
147    }
148
149    fn channel(&self) -> &Channel {
150        self.base.channel()
151    }
152
153    fn dispose(&self, reason: DisposeReason) {
154        self.base.dispose(reason)
155    }
156
157    fn adopt(&self, child: Arc<dyn ChannelOwner>) {
158        self.base.adopt(child)
159    }
160
161    fn add_child(&self, guid: Arc<str>, child: Arc<dyn ChannelOwner>) {
162        self.base.add_child(guid, child)
163    }
164
165    fn remove_child(&self, guid: &str) {
166        self.base.remove_child(guid)
167    }
168
169    fn on_event(&self, method: &str, params: Value) {
170        self.base.on_event(method, params)
171    }
172
173    fn was_collected(&self) -> bool {
174        self.base.was_collected()
175    }
176
177    fn as_any(&self) -> &dyn Any {
178        self
179    }
180}
181
182impl std::fmt::Debug for LocalUtils {
183    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184        f.debug_struct("LocalUtils")
185            .field("guid", &self.guid())
186            .finish()
187    }
188}