use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, instrument};
use viewpoint_cdp::CdpConnection;
use viewpoint_js::js;
use crate::error::PageError;
use super::clock_script::CLOCK_MOCK_SCRIPT;
#[derive(Debug)]
pub struct Clock<'a> {
connection: &'a Arc<CdpConnection>,
session_id: &'a str,
installed: bool,
}
impl<'a> Clock<'a> {
pub(crate) fn new(connection: &'a Arc<CdpConnection>, session_id: &'a str) -> Self {
Self {
connection,
session_id,
installed: false,
}
}
#[instrument(level = "debug", skip(self))]
pub async fn install(&mut self) -> Result<(), PageError> {
self.inject_clock_library().await?;
self.evaluate(js! { window.__viewpointClock.install() })
.await?;
self.installed = true;
debug!("Clock installed");
Ok(())
}
#[instrument(level = "debug", skip(self))]
pub async fn uninstall(&mut self) -> Result<(), PageError> {
self.evaluate(js! { window.__viewpointClock && window.__viewpointClock.uninstall() })
.await?;
self.installed = false;
debug!("Clock uninstalled");
Ok(())
}
#[instrument(level = "debug", skip(self, time))]
pub async fn set_fixed_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
let time_value = time.into();
match &time_value {
TimeValue::Timestamp(ts) => {
self.evaluate(&js! { window.__viewpointClock.setFixedTime(#{ts}) })
.await?;
}
TimeValue::IsoString(s) => {
self.evaluate(&js! { window.__viewpointClock.setFixedTime(#{s}) })
.await?;
}
}
debug!(time = ?time_value, "Fixed time set");
Ok(())
}
#[instrument(level = "debug", skip(self, time))]
pub async fn set_system_time(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
let time_value = time.into();
match &time_value {
TimeValue::Timestamp(ts) => {
self.evaluate(&js! { window.__viewpointClock.setSystemTime(#{ts}) })
.await?;
}
TimeValue::IsoString(s) => {
self.evaluate(&js! { window.__viewpointClock.setSystemTime(#{s}) })
.await?;
}
}
debug!(time = ?time_value, "System time set");
Ok(())
}
#[instrument(level = "debug", skip(self))]
pub async fn run_for(&self, duration: Duration) -> Result<u32, PageError> {
let ms = duration.as_millis();
let result: f64 = self
.evaluate_value(&js! { window.__viewpointClock.runFor(#{ms}) })
.await?;
debug!(
duration_ms = ms,
timers_fired = result as u32,
"Time advanced"
);
Ok(result as u32)
}
#[instrument(level = "debug", skip(self))]
pub async fn fast_forward(&self, duration: Duration) -> Result<(), PageError> {
let ms = duration.as_millis();
self.evaluate(&js! { window.__viewpointClock.fastForward(#{ms}) })
.await?;
debug!(duration_ms = ms, "Time fast-forwarded");
Ok(())
}
#[instrument(level = "debug", skip(self, time))]
pub async fn pause_at(&self, time: impl Into<TimeValue>) -> Result<(), PageError> {
let time_value = time.into();
match &time_value {
TimeValue::Timestamp(ts) => {
self.evaluate(&js! { window.__viewpointClock.pauseAt(#{ts}) })
.await?;
}
TimeValue::IsoString(s) => {
self.evaluate(&js! { window.__viewpointClock.pauseAt(#{s}) })
.await?;
}
}
debug!(time = ?time_value, "Clock paused");
Ok(())
}
#[instrument(level = "debug", skip(self))]
pub async fn resume(&self) -> Result<(), PageError> {
self.evaluate(js! { window.__viewpointClock.resume() })
.await?;
debug!("Clock resumed");
Ok(())
}
#[instrument(level = "debug", skip(self))]
pub async fn run_all_timers(&self) -> Result<u32, PageError> {
let result: f64 = self
.evaluate_value(js! { window.__viewpointClock.runAllTimers() })
.await?;
debug!(timers_fired = result as u32, "All timers executed");
Ok(result as u32)
}
#[instrument(level = "debug", skip(self))]
pub async fn run_to_last(&self) -> Result<u32, PageError> {
let result: f64 = self
.evaluate_value(js! { window.__viewpointClock.runToLast() })
.await?;
debug!(timers_fired = result as u32, "Ran to last timer");
Ok(result as u32)
}
#[instrument(level = "debug", skip(self))]
pub async fn pending_timer_count(&self) -> Result<u32, PageError> {
let result: f64 = self
.evaluate_value(js! { window.__viewpointClock.pendingTimerCount() })
.await?;
Ok(result as u32)
}
pub async fn is_installed(&self) -> Result<bool, PageError> {
let result: bool = self
.evaluate_value(
js! { window.__viewpointClock && window.__viewpointClock.isInstalled() },
)
.await
.unwrap_or(false);
Ok(result)
}
async fn inject_clock_library(&self) -> Result<(), PageError> {
self.evaluate(CLOCK_MOCK_SCRIPT).await?;
Ok(())
}
async fn evaluate(&self, expression: &str) -> Result<(), PageError> {
use viewpoint_cdp::protocol::runtime::EvaluateParams;
let _: serde_json::Value = self
.connection
.send_command(
"Runtime.evaluate",
Some(EvaluateParams {
expression: expression.to_string(),
object_group: None,
include_command_line_api: None,
silent: None,
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
}),
Some(self.session_id),
)
.await?;
Ok(())
}
async fn evaluate_value<T: serde::de::DeserializeOwned>(
&self,
expression: &str,
) -> Result<T, PageError> {
use viewpoint_cdp::protocol::runtime::EvaluateParams;
#[derive(serde::Deserialize)]
struct EvalResult {
result: ResultValue,
}
#[derive(serde::Deserialize)]
struct ResultValue {
value: serde_json::Value,
}
let result: EvalResult = self
.connection
.send_command(
"Runtime.evaluate",
Some(EvaluateParams {
expression: expression.to_string(),
object_group: None,
include_command_line_api: None,
silent: None,
context_id: None,
return_by_value: Some(true),
await_promise: Some(false),
}),
Some(self.session_id),
)
.await?;
serde_json::from_value(result.result.value)
.map_err(|e| PageError::EvaluationFailed(format!("Failed to deserialize result: {e}")))
}
}
#[derive(Debug, Clone)]
pub enum TimeValue {
Timestamp(i64),
IsoString(String),
}
impl From<i64> for TimeValue {
fn from(ts: i64) -> Self {
TimeValue::Timestamp(ts)
}
}
impl From<u64> for TimeValue {
fn from(ts: u64) -> Self {
TimeValue::Timestamp(ts as i64)
}
}
impl From<&str> for TimeValue {
fn from(s: &str) -> Self {
TimeValue::IsoString(s.to_string())
}
}
impl From<String> for TimeValue {
fn from(s: String) -> Self {
TimeValue::IsoString(s)
}
}