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}