viewpoint_core/page/clock/
operations.rs

1//! Clock operations for time manipulation.
2
3use std::time::Duration;
4
5use tracing::{debug, instrument};
6use viewpoint_js::js;
7
8use super::{Clock, TimeValue};
9use crate::error::PageError;
10
11impl Clock<'_> {
12    /// Set a fixed time that doesn't advance.
13    ///
14    /// All calls to `Date.now()` and new `Date()` will return this time.
15    /// Time remains frozen until you call `run_for`, `fast_forward`,
16    /// `set_system_time`, or `resume`.
17    ///
18    /// # Arguments
19    ///
20    /// * `time` - The time to set, either as an ISO 8601 string (e.g., "2024-01-01T00:00:00Z")
21    ///   or a Unix timestamp in milliseconds.
22    ///
23    /// # Errors
24    ///
25    /// Returns an error if setting the time fails.
26    #[instrument(level = "debug", skip(self, time))]
27    pub async fn set_fixed_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
28        let time_value = time.into();
29        match &time_value {
30            TimeValue::Timestamp(ts) => {
31                self.evaluate(&js! { window.__viewpointClock.setFixedTime(#{ts}) })
32                    .await?;
33            }
34            TimeValue::IsoString(s) => {
35                self.evaluate(&js! { window.__viewpointClock.setFixedTime(#{s}) })
36                    .await?;
37            }
38        }
39        debug!(time = ?time_value, "Fixed time set");
40        Ok(())
41    }
42
43    /// Set the system time that flows normally.
44    ///
45    /// Time starts from the specified value and advances in real time.
46    ///
47    /// # Arguments
48    ///
49    /// * `time` - The starting time, either as an ISO 8601 string or Unix timestamp.
50    ///
51    /// # Errors
52    ///
53    /// Returns an error if setting the time fails.
54    #[instrument(level = "debug", skip(self, time))]
55    pub async fn set_system_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
56        let time_value = time.into();
57        match &time_value {
58            TimeValue::Timestamp(ts) => {
59                self.evaluate(&js! { window.__viewpointClock.setSystemTime(#{ts}) })
60                    .await?;
61            }
62            TimeValue::IsoString(s) => {
63                self.evaluate(&js! { window.__viewpointClock.setSystemTime(#{s}) })
64                    .await?;
65            }
66        }
67        debug!(time = ?time_value, "System time set");
68        Ok(())
69    }
70
71    /// Advance time by a duration, firing any scheduled timers.
72    ///
73    /// This advances the clock and executes any setTimeout/setInterval
74    /// callbacks that were scheduled to fire during that period.
75    ///
76    /// # Arguments
77    ///
78    /// * `duration` - The amount of time to advance.
79    ///
80    /// # Returns
81    ///
82    /// The number of timers that were fired.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error if advancing time fails.
87    #[instrument(level = "debug", skip(self))]
88    pub async fn run_for(&self, duration: Duration) -> Result<u32, PageError> {
89        let ms = duration.as_millis();
90        let result: f64 = self
91            .evaluate_value(&js! { window.__viewpointClock.runFor(#{ms}) })
92            .await?;
93        debug!(
94            duration_ms = ms,
95            timers_fired = result as u32,
96            "Time advanced"
97        );
98        Ok(result as u32)
99    }
100
101    /// Fast-forward time without firing timers.
102    ///
103    /// This advances the clock but does NOT execute scheduled timers.
104    /// Use this when you want to jump ahead in time quickly.
105    ///
106    /// # Arguments
107    ///
108    /// * `duration` - The amount of time to skip.
109    ///
110    /// # Errors
111    ///
112    /// Returns an error if fast-forwarding fails.
113    #[instrument(level = "debug", skip(self))]
114    pub async fn fast_forward(&self, duration: Duration) -> Result<(), PageError> {
115        let ms = duration.as_millis();
116        self.evaluate(&js! { window.__viewpointClock.fastForward(#{ms}) })
117            .await?;
118        debug!(duration_ms = ms, "Time fast-forwarded");
119        Ok(())
120    }
121
122    /// Pause at a specific time.
123    ///
124    /// This sets the clock to the specified time and pauses it there.
125    ///
126    /// # Arguments
127    ///
128    /// * `time` - The time to pause at, as an ISO string or timestamp.
129    ///
130    /// # Errors
131    ///
132    /// Returns an error if pausing fails.
133    #[instrument(level = "debug", skip(self, time))]
134    pub async fn pause_at(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
135        let time_value = time.into();
136        match &time_value {
137            TimeValue::Timestamp(ts) => {
138                self.evaluate(&js! { window.__viewpointClock.pauseAt(#{ts}) })
139                    .await?;
140            }
141            TimeValue::IsoString(s) => {
142                self.evaluate(&js! { window.__viewpointClock.pauseAt(#{s}) })
143                    .await?;
144            }
145        }
146        debug!(time = ?time_value, "Clock paused");
147        Ok(())
148    }
149
150    /// Resume normal time flow.
151    ///
152    /// After calling this, time will advance normally from the current
153    /// mocked time value.
154    ///
155    /// # Errors
156    ///
157    /// Returns an error if resuming fails.
158    #[instrument(level = "debug", skip(self))]
159    pub async fn resume(&self) -> Result<(), PageError> {
160        self.evaluate(js! { window.__viewpointClock.resume() })
161            .await?;
162        debug!("Clock resumed");
163        Ok(())
164    }
165
166    /// Run all pending timers.
167    ///
168    /// This advances time to execute all scheduled setTimeout and setInterval
169    /// callbacks, as well as requestAnimationFrame callbacks.
170    ///
171    /// # Returns
172    ///
173    /// The number of timers that were fired.
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if running timers fails.
178    #[instrument(level = "debug", skip(self))]
179    pub async fn run_all_timers(&self) -> Result<u32, PageError> {
180        let result: f64 = self
181            .evaluate_value(js! { window.__viewpointClock.runAllTimers() })
182            .await?;
183        debug!(timers_fired = result as u32, "All timers executed");
184        Ok(result as u32)
185    }
186
187    /// Run to the last scheduled timer.
188    ///
189    /// This advances time to the last scheduled timer and executes all
190    /// timers up to that point.
191    ///
192    /// # Returns
193    ///
194    /// The number of timers that were fired.
195    ///
196    /// # Errors
197    ///
198    /// Returns an error if running timers fails.
199    #[instrument(level = "debug", skip(self))]
200    pub async fn run_to_last(&self) -> Result<u32, PageError> {
201        let result: f64 = self
202            .evaluate_value(js! { window.__viewpointClock.runToLast() })
203            .await?;
204        debug!(timers_fired = result as u32, "Ran to last timer");
205        Ok(result as u32)
206    }
207
208    /// Get the number of pending timers.
209    ///
210    /// This includes setTimeout, setInterval, and requestAnimationFrame callbacks.
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if getting the count fails.
215    #[instrument(level = "debug", skip(self))]
216    pub async fn pending_timer_count(&self) -> Result<u32, PageError> {
217        let result: f64 = self
218            .evaluate_value(js! { window.__viewpointClock.pendingTimerCount() })
219            .await?;
220        Ok(result as u32)
221    }
222
223    /// Check if clock mocking is installed.
224    ///
225    /// # Errors
226    ///
227    /// Returns an error if the check fails.
228    pub async fn is_installed(&self) -> Result<bool, PageError> {
229        let result: bool = self
230            .evaluate_value(
231                js! { window.__viewpointClock && window.__viewpointClock.isInstalled() },
232            )
233            .await
234            .unwrap_or(false);
235        Ok(result)
236    }
237}