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}