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}