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    /// ```no_run
100    /// use viewpoint_core::Page;
101    ///
102    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
103    /// page.clock().install().await?;
104    /// # Ok(())
105    /// # }
106    /// ```
107    ///
108    /// # Errors
109    ///
110    /// Returns an error if the clock cannot be installed.
111    #[instrument(level = "debug", skip(self))]
112    pub async fn install(&mut self) -> Result<(), PageError> {
113        // First inject the clock library
114        self.inject_clock_library().await?;
115
116        // Then install it
117        self.evaluate(js! { window.__viewpointClock.install() })
118            .await?;
119        self.installed = true;
120
121        debug!("Clock installed");
122        Ok(())
123    }
124
125    /// Uninstall clock mocking and restore original functions.
126    ///
127    /// # Example
128    ///
129    /// ```no_run
130    /// use viewpoint_core::Page;
131    ///
132    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
133    /// page.clock().uninstall().await?;
134    /// # Ok(())
135    /// # }
136    /// ```
137    ///
138    /// # Errors
139    ///
140    /// Returns an error if the clock cannot be uninstalled.
141    #[instrument(level = "debug", skip(self))]
142    pub async fn uninstall(&mut self) -> Result<(), PageError> {
143        self.evaluate(js! { window.__viewpointClock && window.__viewpointClock.uninstall() })
144            .await?;
145        self.installed = false;
146
147        debug!("Clock uninstalled");
148        Ok(())
149    }
150
151    /// Set a fixed time that doesn't advance.
152    ///
153    /// All calls to `Date.now()` and new `Date()` will return this time.
154    /// Time remains frozen until you call `run_for`, `fast_forward`,
155    /// `set_system_time`, or `resume`.
156    ///
157    /// # Arguments
158    ///
159    /// * `time` - The time to set, either as an ISO 8601 string (e.g., "2024-01-01T00:00:00Z")
160    ///   or a Unix timestamp in milliseconds.
161    ///
162    /// # Example
163    ///
164    /// ```no_run
165    /// use viewpoint_core::Page;
166    ///
167    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
168    /// // Using ISO string
169    /// page.clock().set_fixed_time("2024-01-01T00:00:00Z").await?;
170    ///
171    /// // Using timestamp
172    /// page.clock().set_fixed_time(1704067200000i64).await?;
173    /// # Ok(())
174    /// # }
175    /// ```
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if setting the time fails.
180    #[instrument(level = "debug", skip(self, time))]
181    pub async fn set_fixed_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
182        let time_value = time.into();
183        match &time_value {
184            TimeValue::Timestamp(ts) => {
185                self.evaluate(&js! { window.__viewpointClock.setFixedTime(#{ts}) })
186                    .await?;
187            }
188            TimeValue::IsoString(s) => {
189                self.evaluate(&js! { window.__viewpointClock.setFixedTime(#{s}) })
190                    .await?;
191            }
192        }
193        debug!(time = ?time_value, "Fixed time set");
194        Ok(())
195    }
196
197    /// Set the system time that flows normally.
198    ///
199    /// Time starts from the specified value and advances in real time.
200    ///
201    /// # Arguments
202    ///
203    /// * `time` - The starting time, either as an ISO 8601 string or Unix timestamp.
204    ///
205    /// # Example
206    ///
207    /// ```no_run
208    /// use viewpoint_core::Page;
209    ///
210    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
211    /// page.clock().set_system_time("2024-01-01T00:00:00Z").await?;
212    /// // Time will now flow from 2024-01-01
213    /// # Ok(())
214    /// # }
215    /// ```
216    ///
217    /// # Errors
218    ///
219    /// Returns an error if setting the time fails.
220    #[instrument(level = "debug", skip(self, time))]
221    pub async fn set_system_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
222        let time_value = time.into();
223        match &time_value {
224            TimeValue::Timestamp(ts) => {
225                self.evaluate(&js! { window.__viewpointClock.setSystemTime(#{ts}) })
226                    .await?;
227            }
228            TimeValue::IsoString(s) => {
229                self.evaluate(&js! { window.__viewpointClock.setSystemTime(#{s}) })
230                    .await?;
231            }
232        }
233        debug!(time = ?time_value, "System time set");
234        Ok(())
235    }
236
237    /// Advance time by a duration, firing any scheduled timers.
238    ///
239    /// This advances the clock and executes any setTimeout/setInterval
240    /// callbacks that were scheduled to fire during that period.
241    ///
242    /// # Arguments
243    ///
244    /// * `duration` - The amount of time to advance.
245    ///
246    /// # Returns
247    ///
248    /// The number of timers that were fired.
249    ///
250    /// # Example
251    ///
252    /// ```no_run
253    /// use viewpoint_core::Page;
254    /// use std::time::Duration;
255    ///
256    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
257    /// // Advance 5 seconds, firing any timers scheduled in that period
258    /// let fired = page.clock().run_for(Duration::from_secs(5)).await?;
259    /// println!("Fired {} timers", fired);
260    /// # Ok(())
261    /// # }
262    /// ```
263    ///
264    /// # Errors
265    ///
266    /// Returns an error if advancing time fails.
267    #[instrument(level = "debug", skip(self))]
268    pub async fn run_for(&self, duration: Duration) -> Result<u32, PageError> {
269        let ms = duration.as_millis();
270        let result: f64 = self
271            .evaluate_value(&js! { window.__viewpointClock.runFor(#{ms}) })
272            .await?;
273        debug!(
274            duration_ms = ms,
275            timers_fired = result as u32,
276            "Time advanced"
277        );
278        Ok(result as u32)
279    }
280
281    /// Fast-forward time without firing timers.
282    ///
283    /// This advances the clock but does NOT execute scheduled timers.
284    /// Use this when you want to jump ahead in time quickly.
285    ///
286    /// # Arguments
287    ///
288    /// * `duration` - The amount of time to skip.
289    ///
290    /// # Example
291    ///
292    /// ```no_run
293    /// use viewpoint_core::Page;
294    /// use std::time::Duration;
295    ///
296    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
297    /// // Skip ahead 1 hour without firing any timers
298    /// page.clock().fast_forward(Duration::from_secs(3600)).await?;
299    /// # Ok(())
300    /// # }
301    /// ```
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if fast-forwarding fails.
306    #[instrument(level = "debug", skip(self))]
307    pub async fn fast_forward(&self, duration: Duration) -> Result<(), PageError> {
308        let ms = duration.as_millis();
309        self.evaluate(&js! { window.__viewpointClock.fastForward(#{ms}) })
310            .await?;
311        debug!(duration_ms = ms, "Time fast-forwarded");
312        Ok(())
313    }
314
315    /// Pause at a specific time.
316    ///
317    /// This sets the clock to the specified time and pauses it there.
318    ///
319    /// # Arguments
320    ///
321    /// * `time` - The time to pause at, as an ISO string or timestamp.
322    ///
323    /// # Example
324    ///
325    /// ```no_run
326    /// use viewpoint_core::Page;
327    ///
328    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
329    /// // Pause at noon
330    /// page.clock().pause_at("2024-01-01T12:00:00Z").await?;
331    /// # Ok(())
332    /// # }
333    /// ```
334    ///
335    /// # Errors
336    ///
337    /// Returns an error if pausing fails.
338    #[instrument(level = "debug", skip(self, time))]
339    pub async fn pause_at(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
340        let time_value = time.into();
341        match &time_value {
342            TimeValue::Timestamp(ts) => {
343                self.evaluate(&js! { window.__viewpointClock.pauseAt(#{ts}) })
344                    .await?;
345            }
346            TimeValue::IsoString(s) => {
347                self.evaluate(&js! { window.__viewpointClock.pauseAt(#{s}) })
348                    .await?;
349            }
350        }
351        debug!(time = ?time_value, "Clock paused");
352        Ok(())
353    }
354
355    /// Resume normal time flow.
356    ///
357    /// After calling this, time will advance normally from the current
358    /// mocked time value.
359    ///
360    /// # Example
361    ///
362    /// ```no_run
363    /// use viewpoint_core::Page;
364    ///
365    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
366    /// page.clock().resume().await?;
367    /// # Ok(())
368    /// # }
369    /// ```
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if resuming fails.
374    #[instrument(level = "debug", skip(self))]
375    pub async fn resume(&self) -> Result<(), PageError> {
376        self.evaluate(js! { window.__viewpointClock.resume() })
377            .await?;
378        debug!("Clock resumed");
379        Ok(())
380    }
381
382    /// Run all pending timers.
383    ///
384    /// This advances time to execute all scheduled setTimeout and setInterval
385    /// callbacks, as well as requestAnimationFrame callbacks.
386    ///
387    /// # Returns
388    ///
389    /// The number of timers that were fired.
390    ///
391    /// # Example
392    ///
393    /// ```no_run
394    /// use viewpoint_core::Page;
395    ///
396    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
397    /// let fired = page.clock().run_all_timers().await?;
398    /// # Ok(())
399    /// # }
400    /// ```
401    ///
402    /// # Errors
403    ///
404    /// Returns an error if running timers fails.
405    #[instrument(level = "debug", skip(self))]
406    pub async fn run_all_timers(&self) -> Result<u32, PageError> {
407        let result: f64 = self
408            .evaluate_value(js! { window.__viewpointClock.runAllTimers() })
409            .await?;
410        debug!(timers_fired = result as u32, "All timers executed");
411        Ok(result as u32)
412    }
413
414    /// Run to the last scheduled timer.
415    ///
416    /// This advances time to the last scheduled timer and executes all
417    /// timers up to that point.
418    ///
419    /// # Returns
420    ///
421    /// The number of timers that were fired.
422    ///
423    /// # Example
424    ///
425    /// ```no_run
426    /// use viewpoint_core::Page;
427    ///
428    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
429    /// let fired = page.clock().run_to_last().await?;
430    /// # Ok(())
431    /// # }
432    /// ```
433    ///
434    /// # Errors
435    ///
436    /// Returns an error if running timers fails.
437    #[instrument(level = "debug", skip(self))]
438    pub async fn run_to_last(&self) -> Result<u32, PageError> {
439        let result: f64 = self
440            .evaluate_value(js! { window.__viewpointClock.runToLast() })
441            .await?;
442        debug!(timers_fired = result as u32, "Ran to last timer");
443        Ok(result as u32)
444    }
445
446    /// Get the number of pending timers.
447    ///
448    /// This includes setTimeout, setInterval, and requestAnimationFrame callbacks.
449    ///
450    /// # Example
451    ///
452    /// ```no_run
453    /// use viewpoint_core::Page;
454    ///
455    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
456    /// let count = page.clock().pending_timer_count().await?;
457    /// println!("{} timers pending", count);
458    /// # Ok(())
459    /// # }
460    /// ```
461    ///
462    /// # Errors
463    ///
464    /// Returns an error if getting the count fails.
465    #[instrument(level = "debug", skip(self))]
466    pub async fn pending_timer_count(&self) -> Result<u32, PageError> {
467        let result: f64 = self
468            .evaluate_value(js! { window.__viewpointClock.pendingTimerCount() })
469            .await?;
470        Ok(result as u32)
471    }
472
473    /// Check if clock mocking is installed.
474    ///
475    /// # Example
476    ///
477    /// ```no_run
478    /// use viewpoint_core::Page;
479    ///
480    /// # async fn example(page: Page) -> Result<(), viewpoint_core::CoreError> {
481    /// if page.clock().is_installed().await? {
482    ///     println!("Clock is mocked");
483    /// }
484    /// # Ok(())
485    /// # }
486    /// ```
487    ///
488    /// # Errors
489    ///
490    /// Returns an error if the check fails.
491    pub async fn is_installed(&self) -> Result<bool, PageError> {
492        let result: bool = self
493            .evaluate_value(
494                js! { window.__viewpointClock && window.__viewpointClock.isInstalled() },
495            )
496            .await
497            .unwrap_or(false);
498        Ok(result)
499    }
500
501    /// Inject the clock mocking library into the page.
502    async fn inject_clock_library(&self) -> Result<(), PageError> {
503        self.evaluate(CLOCK_MOCK_SCRIPT).await?;
504        Ok(())
505    }
506
507    /// Evaluate JavaScript and return nothing.
508    async fn evaluate(&self, expression: &str) -> Result<(), PageError> {
509        use viewpoint_cdp::protocol::runtime::EvaluateParams;
510
511        let _: serde_json::Value = self
512            .connection
513            .send_command(
514                "Runtime.evaluate",
515                Some(EvaluateParams {
516                    expression: expression.to_string(),
517                    object_group: None,
518                    include_command_line_api: None,
519                    silent: None,
520                    context_id: None,
521                    return_by_value: Some(true),
522                    await_promise: Some(false),
523                }),
524                Some(self.session_id),
525            )
526            .await?;
527
528        Ok(())
529    }
530
531    /// Evaluate JavaScript and return a value.
532    async fn evaluate_value<T: serde::de::DeserializeOwned>(
533        &self,
534        expression: &str,
535    ) -> Result<T, PageError> {
536        use viewpoint_cdp::protocol::runtime::EvaluateParams;
537
538        #[derive(serde::Deserialize)]
539        struct EvalResult {
540            result: ResultValue,
541        }
542
543        #[derive(serde::Deserialize)]
544        struct ResultValue {
545            value: serde_json::Value,
546        }
547
548        let result: EvalResult = self
549            .connection
550            .send_command(
551                "Runtime.evaluate",
552                Some(EvaluateParams {
553                    expression: expression.to_string(),
554                    object_group: None,
555                    include_command_line_api: None,
556                    silent: None,
557                    context_id: None,
558                    return_by_value: Some(true),
559                    await_promise: Some(false),
560                }),
561                Some(self.session_id),
562            )
563            .await?;
564
565        serde_json::from_value(result.result.value)
566            .map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize result: {e}")))
567    }
568}
569
570/// A time value that can be either a timestamp or an ISO string.
571#[derive(Debug, Clone)]
572pub enum TimeValue {
573    /// Unix timestamp in milliseconds.
574    Timestamp(i64),
575    /// ISO 8601 formatted string.
576    IsoString(String),
577}
578
579impl From<i64> for TimeValue {
580    fn from(ts: i64) -> Self {
581        TimeValue::Timestamp(ts)
582    }
583}
584
585impl From<u64> for TimeValue {
586    fn from(ts: u64) -> Self {
587        TimeValue::Timestamp(ts as i64)
588    }
589}
590
591impl From<&str> for TimeValue {
592    fn from(s: &str) -> Self {
593        TimeValue::IsoString(s.to_string())
594    }
595}
596
597impl From<String> for TimeValue {
598    fn from(s: String) -> Self {
599        TimeValue::IsoString(s)
600    }
601}