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}