browser_commander/traces/
page.rs1use std::future::Future;
10use std::sync::Arc;
11use std::time::Duration;
12
13use async_trait::async_trait;
14use futures::stream::BoxStream;
15
16use super::jsonfmt::Json;
17use crate::core::engine::{EngineAdapter, TraceEngineEvent};
18
19#[async_trait]
24pub trait TracePage: Send + Sync {
25 fn require_feature(&self, _feature: &str) -> Result<(), crate::core::engine::EngineError> {
26 Ok(())
27 }
28 fn engine(&self) -> Option<String> {
30 None
31 }
32
33 fn context_key(&self) -> Option<usize> {
35 None
36 }
37
38 fn page_key(&self) -> Option<usize> {
40 None
41 }
42
43 fn has_context(&self) -> bool {
45 false
46 }
47
48 async fn evaluate_function(&self, source: &str, argument: &Json) -> Result<Json, String>;
51
52 async fn screenshot(&self) -> Result<Option<Vec<u8>>, String> {
54 Ok(None)
55 }
56
57 async fn add_init_script(
62 &self,
63 _source: &str,
64 _argument: &Json,
65 ) -> Result<Option<String>, String> {
66 Ok(None)
67 }
68
69 async fn remove_init_script(&self, _identifier: &str) -> Result<(), String> {
71 Ok(())
72 }
73
74 async fn events(&self) -> Option<BoxStream<'static, TraceEngineEvent>> {
76 None
77 }
78}
79
80pub(crate) fn call_expression(source: &str, argument: &Json) -> String {
82 format!("({source})({})", argument.to_compact())
83}
84
85pub(crate) fn json_text_expression(source: &str, argument: &Json) -> String {
91 format!(
92 "Promise.resolve().then(async () => {{ const value = await {}; \
93 return value === undefined ? null : JSON.stringify(value); }})",
94 call_expression(source, argument)
95 )
96}
97
98#[derive(Clone)]
100pub struct AdapterTracePage {
101 adapter: Arc<dyn EngineAdapter>,
102}
103
104impl AdapterTracePage {
105 pub fn new(adapter: Arc<dyn EngineAdapter>) -> Self {
107 Self { adapter }
108 }
109
110 fn key(&self) -> usize {
111 Arc::as_ptr(&self.adapter).cast::<()>() as usize
112 }
113}
114
115#[async_trait]
116impl TracePage for AdapterTracePage {
117 fn require_feature(&self, feature: &str) -> Result<(), crate::core::engine::EngineError> {
118 self.adapter.require_feature(feature)
119 }
120 fn engine(&self) -> Option<String> {
121 Some(self.adapter.engine_type().to_string())
122 }
123
124 fn context_key(&self) -> Option<usize> {
126 Some(self.key())
127 }
128
129 fn page_key(&self) -> Option<usize> {
130 Some(self.key())
131 }
132
133 fn has_context(&self) -> bool {
134 true
135 }
136
137 async fn evaluate_function(&self, source: &str, argument: &Json) -> Result<Json, String> {
138 let value = self
139 .adapter
140 .evaluate(&json_text_expression(source, argument))
141 .await
142 .map_err(|error| error.to_string())?;
143 match value {
144 serde_json::Value::String(text) => {
145 Json::parse(&text).map_err(|error| format!("unreadable page result: {error}"))
146 }
147 serde_json::Value::Null => Ok(Json::Null),
148 other => Ok(Json::from(other)),
149 }
150 }
151
152 async fn screenshot(&self) -> Result<Option<Vec<u8>>, String> {
153 self.adapter
154 .screenshot()
155 .await
156 .map(Some)
157 .map_err(|error| error.to_string())
158 }
159
160 async fn add_init_script(
161 &self,
162 source: &str,
163 argument: &Json,
164 ) -> Result<Option<String>, String> {
165 self.adapter
166 .add_init_script(&call_expression(source, argument))
167 .await
168 .map_err(|error| error.to_string())
169 }
170
171 async fn remove_init_script(&self, identifier: &str) -> Result<(), String> {
172 self.adapter
173 .remove_init_script(identifier)
174 .await
175 .map_err(|error| error.to_string())
176 }
177
178 async fn events(&self) -> Option<BoxStream<'static, TraceEngineEvent>> {
179 self.adapter.trace_events().await
180 }
181}
182
183pub(crate) async fn with_deadline<T>(
185 work: impl Future<Output = Result<T, String>>,
186 timeout_ms: u64,
187 what: &str,
188) -> Result<T, String> {
189 if timeout_ms == 0 {
190 return work.await;
191 }
192 match tokio::time::timeout(Duration::from_millis(timeout_ms), work).await {
193 Ok(outcome) => outcome,
194 Err(_) => Err(format!("{what} timed out after {timeout_ms}ms")),
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use crate::traces::jsonfmt::JsonObject;
202
203 #[test]
204 fn calls_keep_the_argument_order() {
205 let argument = Json::from(JsonObject::new().with("b", 1).with("a", "x"));
206 assert_eq!(
207 call_expression("function f(o) {}", &argument),
208 r#"(function f(o) {})({"b":1,"a":"x"})"#
209 );
210 assert!(json_text_expression("function f() {}", &Json::Null)
211 .contains("await (function f() {})(null);"));
212 }
213
214 #[tokio::test]
215 async fn deadlines_name_what_timed_out() {
216 let slow = async {
217 tokio::time::sleep(Duration::from_secs(5)).await;
218 Ok::<_, String>(())
219 };
220 assert_eq!(
221 with_deadline(slow, 5, "trace screenshot").await,
222 Err("trace screenshot timed out after 5ms".to_string())
223 );
224 assert_eq!(
225 with_deadline(async { Ok::<_, String>(1) }, 0, "x").await,
226 Ok(1)
227 );
228 }
229}