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