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//! ```ignore
9//! use std::time::Duration;
10//!
11//! // Install clock mocking
12//! page.clock().install().await?;
13//!
14//! // Set a fixed time
15//! page.clock().set_fixed_time("2024-01-01T00:00:00Z").await?;
16//!
17//! // Check that Date.now() returns the fixed time
18//! use viewpoint_js::js;
19//! let time: f64 = page.evaluate(js!{ Date.now() }).await?;
20//!
21//! // Advance time by 5 seconds, firing any scheduled timers
22//! page.clock().run_for(Duration::from_secs(5)).await?;
23//! ```
24
25use std::sync::Arc;
26use std::time::Duration;
27
28use tracing::{debug, instrument};
29use viewpoint_cdp::CdpConnection;
30use viewpoint_js::js;
31
32use crate::error::PageError;
33
34use super::clock_script::CLOCK_MOCK_SCRIPT;
35
36/// Clock controller for mocking time in a browser page.
37///
38/// The Clock API allows you to control JavaScript's time-related functions
39/// including Date, setTimeout, setInterval, requestAnimationFrame, and
40/// `performance.now()`.
41///
42/// # Example
43///
44/// ```ignore
45/// use std::time::Duration;
46///
47/// // Install clock mocking
48/// page.clock().install().await?;
49///
50/// // Freeze time at a specific moment
51/// page.clock().set_fixed_time("2024-01-01T00:00:00Z").await?;
52///
53/// // Advance time and fire scheduled timers
54/// page.clock().run_for(Duration::from_secs(5)).await?;
55///
56/// // Uninstall when done
57/// page.clock().uninstall().await?;
58/// ```
59#[derive(Debug)]
60pub struct Clock<'a> {
61    connection: &'a Arc<CdpConnection>,
62    session_id: &'a str,
63    installed: bool,
64}
65
66impl<'a> Clock<'a> {
67    /// Create a new clock controller for a page.
68    pub(crate) fn new(connection: &'a Arc<CdpConnection>, session_id: &'a str) -> Self {
69        Self {
70            connection,
71            session_id,
72            installed: false,
73        }
74    }
75
76    /// Install clock mocking on the page.
77    ///
78    /// This replaces Date, setTimeout, setInterval, requestAnimationFrame,
79    /// and `performance.now()` with mocked versions that can be controlled.
80    ///
81    /// # Example
82    ///
83    /// ```ignore
84    /// page.clock().install().await?;
85    /// ```
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the clock cannot be installed.
90    #[instrument(level = "debug", skip(self))]
91    pub async fn install(&mut self) -> Result<(), PageError> {
92        // First inject the clock library
93        self.inject_clock_library().await?;
94        
95        // Then install it
96        self.evaluate(js!{ window.__viewpointClock.install() }).await?;
97        self.installed = true;
98        
99        debug!("Clock installed");
100        Ok(())
101    }
102
103    /// Uninstall clock mocking and restore original functions.
104    ///
105    /// # Example
106    ///
107    /// ```ignore
108    /// page.clock().uninstall().await?;
109    /// ```
110    ///
111    /// # Errors
112    ///
113    /// Returns an error if the clock cannot be uninstalled.
114    #[instrument(level = "debug", skip(self))]
115    pub async fn uninstall(&mut self) -> Result<(), PageError> {
116        self.evaluate(js!{ window.__viewpointClock && window.__viewpointClock.uninstall() })
117            .await?;
118        self.installed = false;
119        
120        debug!("Clock uninstalled");
121        Ok(())
122    }
123
124    /// Set a fixed time that doesn't advance.
125    ///
126    /// All calls to `Date.now()` and new `Date()` will return this time.
127    /// Time remains frozen until you call `run_for`, `fast_forward`,
128    /// `set_system_time`, or `resume`.
129    ///
130    /// # Arguments
131    ///
132    /// * `time` - The time to set, either as an ISO 8601 string (e.g., "2024-01-01T00:00:00Z")
133    ///   or a Unix timestamp in milliseconds.
134    ///
135    /// # Example
136    ///
137    /// ```ignore
138    /// // Using ISO string
139    /// page.clock().set_fixed_time("2024-01-01T00:00:00Z").await?;
140    ///
141    /// // Using timestamp
142    /// page.clock().set_fixed_time(1704067200000i64).await?;
143    /// ```
144    ///
145    /// # Errors
146    ///
147    /// Returns an error if setting the time fails.
148    #[instrument(level = "debug", skip(self, time))]
149    pub async fn set_fixed_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
150        let time_value = time.into();
151        match &time_value {
152            TimeValue::Timestamp(ts) => {
153                self.evaluate(&js!{ window.__viewpointClock.setFixedTime(#{ts}) }).await?;
154            }
155            TimeValue::IsoString(s) => {
156                self.evaluate(&js!{ window.__viewpointClock.setFixedTime(#{s}) }).await?;
157            }
158        }
159        debug!(time = ?time_value, "Fixed time set");
160        Ok(())
161    }
162
163    /// Set the system time that flows normally.
164    ///
165    /// Time starts from the specified value and advances in real time.
166    ///
167    /// # Arguments
168    ///
169    /// * `time` - The starting time, either as an ISO 8601 string or Unix timestamp.
170    ///
171    /// # Example
172    ///
173    /// ```ignore
174    /// page.clock().set_system_time("2024-01-01T00:00:00Z").await?;
175    /// // Time will now flow from 2024-01-01
176    /// ```
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if setting the time fails.
181    #[instrument(level = "debug", skip(self, time))]
182    pub async fn set_system_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
183        let time_value = time.into();
184        match &time_value {
185            TimeValue::Timestamp(ts) => {
186                self.evaluate(&js!{ window.__viewpointClock.setSystemTime(#{ts}) }).await?;
187            }
188            TimeValue::IsoString(s) => {
189                self.evaluate(&js!{ window.__viewpointClock.setSystemTime(#{s}) }).await?;
190            }
191        }
192        debug!(time = ?time_value, "System time set");
193        Ok(())
194    }
195
196    /// Advance time by a duration, firing any scheduled timers.
197    ///
198    /// This advances the clock and executes any setTimeout/setInterval
199    /// callbacks that were scheduled to fire during that period.
200    ///
201    /// # Arguments
202    ///
203    /// * `duration` - The amount of time to advance.
204    ///
205    /// # Returns
206    ///
207    /// The number of timers that were fired.
208    ///
209    /// # Example
210    ///
211    /// ```ignore
212    /// // Advance 5 seconds, firing any timers scheduled in that period
213    /// let fired = page.clock().run_for(Duration::from_secs(5)).await?;
214    /// println!("Fired {} timers", fired);
215    /// ```
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if advancing time fails.
220    #[instrument(level = "debug", skip(self))]
221    pub async fn run_for(&self, duration: Duration) -> Result<u32, PageError> {
222        let ms = duration.as_millis();
223        let result: f64 = self.evaluate_value(&js!{ window.__viewpointClock.runFor(#{ms}) }).await?;
224        debug!(duration_ms = ms, timers_fired = result as u32, "Time advanced");
225        Ok(result as u32)
226    }
227
228    /// Fast-forward time without firing timers.
229    ///
230    /// This advances the clock but does NOT execute scheduled timers.
231    /// Use this when you want to jump ahead in time quickly.
232    ///
233    /// # Arguments
234    ///
235    /// * `duration` - The amount of time to skip.
236    ///
237    /// # Example
238    ///
239    /// ```ignore
240    /// // Skip ahead 1 hour without firing any timers
241    /// page.clock().fast_forward(Duration::from_secs(3600)).await?;
242    /// ```
243    ///
244    /// # Errors
245    ///
246    /// Returns an error if fast-forwarding fails.
247    #[instrument(level = "debug", skip(self))]
248    pub async fn fast_forward(&self, duration: Duration) -> Result<(), PageError> {
249        let ms = duration.as_millis();
250        self.evaluate(&js!{ window.__viewpointClock.fastForward(#{ms}) }).await?;
251        debug!(duration_ms = ms, "Time fast-forwarded");
252        Ok(())
253    }
254
255    /// Pause at a specific time.
256    ///
257    /// This sets the clock to the specified time and pauses it there.
258    ///
259    /// # Arguments
260    ///
261    /// * `time` - The time to pause at, as an ISO string or timestamp.
262    ///
263    /// # Example
264    ///
265    /// ```ignore
266    /// // Pause at noon
267    /// page.clock().pause_at("2024-01-01T12:00:00Z").await?;
268    /// ```
269    ///
270    /// # Errors
271    ///
272    /// Returns an error if pausing fails.
273    #[instrument(level = "debug", skip(self, time))]
274    pub async fn pause_at(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
275        let time_value = time.into();
276        match &time_value {
277            TimeValue::Timestamp(ts) => {
278                self.evaluate(&js!{ window.__viewpointClock.pauseAt(#{ts}) }).await?;
279            }
280            TimeValue::IsoString(s) => {
281                self.evaluate(&js!{ window.__viewpointClock.pauseAt(#{s}) }).await?;
282            }
283        }
284        debug!(time = ?time_value, "Clock paused");
285        Ok(())
286    }
287
288    /// Resume normal time flow.
289    ///
290    /// After calling this, time will advance normally from the current
291    /// mocked time value.
292    ///
293    /// # Example
294    ///
295    /// ```ignore
296    /// page.clock().resume().await?;
297    /// ```
298    ///
299    /// # Errors
300    ///
301    /// Returns an error if resuming fails.
302    #[instrument(level = "debug", skip(self))]
303    pub async fn resume(&self) -> Result<(), PageError> {
304        self.evaluate(js!{ window.__viewpointClock.resume() }).await?;
305        debug!("Clock resumed");
306        Ok(())
307    }
308
309    /// Run all pending timers.
310    ///
311    /// This advances time to execute all scheduled setTimeout and setInterval
312    /// callbacks, as well as requestAnimationFrame callbacks.
313    ///
314    /// # Returns
315    ///
316    /// The number of timers that were fired.
317    ///
318    /// # Example
319    ///
320    /// ```ignore
321    /// let fired = page.clock().run_all_timers().await?;
322    /// ```
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if running timers fails.
327    #[instrument(level = "debug", skip(self))]
328    pub async fn run_all_timers(&self) -> Result<u32, PageError> {
329        let result: f64 = self.evaluate_value(js!{ window.__viewpointClock.runAllTimers() }).await?;
330        debug!(timers_fired = result as u32, "All timers executed");
331        Ok(result as u32)
332    }
333
334    /// Run to the last scheduled timer.
335    ///
336    /// This advances time to the last scheduled timer and executes all
337    /// timers up to that point.
338    ///
339    /// # Returns
340    ///
341    /// The number of timers that were fired.
342    ///
343    /// # Example
344    ///
345    /// ```ignore
346    /// let fired = page.clock().run_to_last().await?;
347    /// ```
348    ///
349    /// # Errors
350    ///
351    /// Returns an error if running timers fails.
352    #[instrument(level = "debug", skip(self))]
353    pub async fn run_to_last(&self) -> Result<u32, PageError> {
354        let result: f64 = self.evaluate_value(js!{ window.__viewpointClock.runToLast() }).await?;
355        debug!(timers_fired = result as u32, "Ran to last timer");
356        Ok(result as u32)
357    }
358
359    /// Get the number of pending timers.
360    ///
361    /// This includes setTimeout, setInterval, and requestAnimationFrame callbacks.
362    ///
363    /// # Example
364    ///
365    /// ```ignore
366    /// let count = page.clock().pending_timer_count().await?;
367    /// println!("{} timers pending", count);
368    /// ```
369    ///
370    /// # Errors
371    ///
372    /// Returns an error if getting the count fails.
373    #[instrument(level = "debug", skip(self))]
374    pub async fn pending_timer_count(&self) -> Result<u32, PageError> {
375        let result: f64 = self.evaluate_value(js!{ window.__viewpointClock.pendingTimerCount() }).await?;
376        Ok(result as u32)
377    }
378
379    /// Check if clock mocking is installed.
380    ///
381    /// # Example
382    ///
383    /// ```ignore
384    /// if page.clock().is_installed().await? {
385    ///     println!("Clock is mocked");
386    /// }
387    /// ```
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if the check fails.
392    pub async fn is_installed(&self) -> Result<bool, PageError> {
393        let result: bool = self.evaluate_value(js!{ window.__viewpointClock && window.__viewpointClock.isInstalled() }).await.unwrap_or(false);
394        Ok(result)
395    }
396
397    /// Inject the clock mocking library into the page.
398    async fn inject_clock_library(&self) -> Result<(), PageError> {
399        self.evaluate(CLOCK_MOCK_SCRIPT).await?;
400        Ok(())
401    }
402
403    /// Evaluate JavaScript and return nothing.
404    async fn evaluate(&self, expression: &str) -> Result<(), PageError> {
405        use viewpoint_cdp::protocol::runtime::EvaluateParams;
406
407        let _: serde_json::Value = self
408            .connection
409            .send_command(
410                "Runtime.evaluate",
411                Some(EvaluateParams {
412                    expression: expression.to_string(),
413                    object_group: None,
414                    include_command_line_api: None,
415                    silent: None,
416                    context_id: None,
417                    return_by_value: Some(true),
418                    await_promise: Some(false),
419                }),
420                Some(self.session_id),
421            )
422            .await?;
423
424        Ok(())
425    }
426
427    /// Evaluate JavaScript and return a value.
428    async fn evaluate_value<T: serde::de::DeserializeOwned>(
429        &self,
430        expression: &str,
431    ) -> Result<T, PageError> {
432        use viewpoint_cdp::protocol::runtime::EvaluateParams;
433
434        #[derive(serde::Deserialize)]
435        struct EvalResult {
436            result: ResultValue,
437        }
438
439        #[derive(serde::Deserialize)]
440        struct ResultValue {
441            value: serde_json::Value,
442        }
443
444        let result: EvalResult = self
445            .connection
446            .send_command(
447                "Runtime.evaluate",
448                Some(EvaluateParams {
449                    expression: expression.to_string(),
450                    object_group: None,
451                    include_command_line_api: None,
452                    silent: None,
453                    context_id: None,
454                    return_by_value: Some(true),
455                    await_promise: Some(false),
456                }),
457                Some(self.session_id),
458            )
459            .await?;
460
461        serde_json::from_value(result.result.value).map_err(|e| {
462            PageError::EvaluationFailed(format!("Failed to deserialize result: {e}"))
463        })
464    }
465}
466
467/// A time value that can be either a timestamp or an ISO string.
468#[derive(Debug, Clone)]
469pub enum TimeValue {
470    /// Unix timestamp in milliseconds.
471    Timestamp(i64),
472    /// ISO 8601 formatted string.
473    IsoString(String),
474}
475
476impl From<i64> for TimeValue {
477    fn from(ts: i64) -> Self {
478        TimeValue::Timestamp(ts)
479    }
480}
481
482impl From<u64> for TimeValue {
483    fn from(ts: u64) -> Self {
484        TimeValue::Timestamp(ts as i64)
485    }
486}
487
488impl From<&str> for TimeValue {
489    fn from(s: &str) -> Self {
490        TimeValue::IsoString(s.to_string())
491    }
492}
493
494impl From<String> for TimeValue {
495    fn from(s: String) -> Self {
496        TimeValue::IsoString(s)
497    }
498}