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