viewpoint_core/page/clock/mod.rs
1//! Clock mocking for controlling time in browser pages.
2//!
3//! The Clock API allows you to control time-related functions in JavaScript,
4//! including Date, setTimeout, setInterval, requestAnimationFrame, and `performance.now()`.
5//!
6//! # Example
7//!
8//! ```
9//! # #[cfg(feature = "integration")]
10//! # tokio_test::block_on(async {
11//! # use viewpoint_core::Browser;
12//! use std::time::Duration;
13//! # let browser = Browser::launch().headless(true).launch().await.unwrap();
14//! # let context = browser.new_context().await.unwrap();
15//! # let page = context.new_page().await.unwrap();
16//! # page.goto("about:blank").goto().await.unwrap();
17//!
18//! // Install clock mocking
19//! page.clock().install().await.unwrap();
20//!
21//! // Set a fixed time
22//! page.clock().set_fixed_time("2024-01-01T00:00:00Z").await.unwrap();
23//!
24//! // Check that Date.now() returns the fixed time
25//! use viewpoint_js::js;
26//! let time: f64 = page.evaluate(js!{ Date.now() }).await.unwrap();
27//!
28//! // Advance time by 5 seconds, firing any scheduled timers
29//! page.clock().run_for(Duration::from_secs(5)).await.unwrap();
30//! # });
31//! ```
32
33mod operations;
34mod time_value;
35
36pub use time_value::TimeValue;
37
38use std::sync::Arc;
39
40use tracing::{debug, instrument};
41use viewpoint_cdp::CdpConnection;
42use viewpoint_js::js;
43
44use crate::error::PageError;
45
46use super::clock_script::CLOCK_MOCK_SCRIPT;
47
48/// Clock controller for mocking time in a browser page.
49///
50/// The Clock API allows you to control JavaScript's time-related functions
51/// including Date, setTimeout, setInterval, requestAnimationFrame, and
52/// `performance.now()`.
53///
54/// # Example
55///
56/// ```
57/// # #[cfg(feature = "integration")]
58/// # tokio_test::block_on(async {
59/// # use viewpoint_core::Browser;
60/// use std::time::Duration;
61/// # let browser = Browser::launch().headless(true).launch().await.unwrap();
62/// # let context = browser.new_context().await.unwrap();
63/// # let page = context.new_page().await.unwrap();
64/// # page.goto("about:blank").goto().await.unwrap();
65///
66/// // Install clock mocking
67/// page.clock().install().await.unwrap();
68///
69/// // Freeze time at a specific moment
70/// page.clock().set_fixed_time("2024-01-01T00:00:00Z").await.unwrap();
71///
72/// // Advance time and fire scheduled timers
73/// page.clock().run_for(Duration::from_secs(5)).await.unwrap();
74///
75/// // Uninstall when done
76/// page.clock().uninstall().await.unwrap();
77/// # });
78/// ```
79#[derive(Debug)]
80pub struct Clock<'a> {
81 connection: &'a Arc<CdpConnection>,
82 session_id: &'a str,
83 installed: bool,
84}
85
86impl<'a> Clock<'a> {
87 /// Create a new clock controller for a page.
88 pub(crate) fn new(connection: &'a Arc<CdpConnection>, session_id: &'a str) -> Self {
89 Self {
90 connection,
91 session_id,
92 installed: false,
93 }
94 }
95
96 /// Install clock mocking on the page.
97 ///
98 /// This replaces Date, setTimeout, setInterval, requestAnimationFrame,
99 /// and `performance.now()` with mocked versions that can be controlled.
100 ///
101 /// # Example
102 ///
103 /// ```no_run
104 /// use viewpoint_core::Page;
105 ///
106 /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
107 /// page.clock().install().await?;
108 /// # Ok(())
109 /// # }
110 /// ```
111 ///
112 /// # Errors
113 ///
114 /// Returns an error if the clock cannot be installed.
115 #[instrument(level = "debug", skip(self))]
116 pub async fn install(&mut self) -> Result<(), PageError> {
117 // First inject the clock library
118 self.inject_clock_library().await?;
119
120 // Then install it
121 self.evaluate(js! { window.__viewpointClock.install() })
122 .await?;
123 self.installed = true;
124
125 debug!("Clock installed");
126 Ok(())
127 }
128
129 /// Uninstall clock mocking and restore original functions.
130 ///
131 /// # Example
132 ///
133 /// ```no_run
134 /// use viewpoint_core::Page;
135 ///
136 /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
137 /// page.clock().uninstall().await?;
138 /// # Ok(())
139 /// # }
140 /// ```
141 ///
142 /// # Errors
143 ///
144 /// Returns an error if the clock cannot be uninstalled.
145 #[instrument(level = "debug", skip(self))]
146 pub async fn uninstall(&mut self) -> Result<(), PageError> {
147 self.evaluate(js! { window.__viewpointClock && window.__viewpointClock.uninstall() })
148 .await?;
149 self.installed = false;
150
151 debug!("Clock uninstalled");
152 Ok(())
153 }
154
155 /// Inject the clock mocking library into the page.
156 async fn inject_clock_library(&self) -> Result<(), PageError> {
157 self.evaluate(CLOCK_MOCK_SCRIPT).await?;
158 Ok(())
159 }
160
161 /// Evaluate JavaScript and return nothing.
162 pub(super) async fn evaluate(&self, expression: &str) -> Result<(), PageError> {
163 use viewpoint_cdp::protocol::runtime::EvaluateParams;
164
165 let _: serde_json::Value = self
166 .connection
167 .send_command(
168 "Runtime.evaluate",
169 Some(EvaluateParams {
170 expression: expression.to_string(),
171 object_group: None,
172 include_command_line_api: None,
173 silent: None,
174 context_id: None,
175 return_by_value: Some(true),
176 await_promise: Some(false),
177 }),
178 Some(self.session_id),
179 )
180 .await?;
181
182 Ok(())
183 }
184
185 /// Evaluate JavaScript and return a value.
186 pub(super) async fn evaluate_value<T: serde::de::DeserializeOwned>(
187 &self,
188 expression: &str,
189 ) -> Result<T, PageError> {
190 use viewpoint_cdp::protocol::runtime::EvaluateParams;
191
192 #[derive(serde::Deserialize)]
193 struct EvalResult {
194 result: ResultValue,
195 }
196
197 #[derive(serde::Deserialize)]
198 struct ResultValue {
199 value: serde_json::Value,
200 }
201
202 let result: EvalResult = self
203 .connection
204 .send_command(
205 "Runtime.evaluate",
206 Some(EvaluateParams {
207 expression: expression.to_string(),
208 object_group: None,
209 include_command_line_api: None,
210 silent: None,
211 context_id: None,
212 return_by_value: Some(true),
213 await_promise: Some(false),
214 }),
215 Some(self.session_id),
216 )
217 .await?;
218
219 serde_json::from_value(result.result.value)
220 .map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize result: {e}")))
221 }
222}