use crate::cdp::session::CdpSession;
use crate::error::{Error, Result};
use serde_json::{json, Value};
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Clone)]
pub struct Clock {
inner: Arc<ClockInner>,
}
struct ClockInner {
session: CdpSession,
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct ClockInstallOptions {
pub time: Option<i64>,
pub fixed: Option<bool>,
}
impl ClockInstallOptions {
pub fn time(mut self, v: i64) -> Self {
self.time = Some(v);
self
}
pub fn fixed(mut self, v: bool) -> Self {
self.fixed = Some(v);
self
}
}
impl Clock {
pub(crate) fn new(session: CdpSession) -> Self {
Self {
inner: Arc::new(ClockInner { session }),
}
}
pub async fn install(&self, options: Option<ClockInstallOptions>) -> Result<()> {
let opts = options.unwrap_or_default();
let now_ms = opts.time.unwrap_or_else(current_unix_ms);
let _: Value = self
.inner
.session
.send(
"Runtime.evaluate",
json!({
"expression": INJECT_SCRIPT,
"awaitPromise": false,
}),
)
.await?;
let _: Value = self
.inner
.session
.send(
"Runtime.evaluate",
json!({
"expression": format!("(globalThis.__pwcdpClock && globalThis.__pwcdpClock.setNow({now_ms}))"),
"awaitPromise": false,
}),
)
.await?;
if opts.fixed.unwrap_or(false) {
self.set_fixed_time(Some(now_ms)).await?;
}
Ok(())
}
pub async fn set_fixed_time(&self, time: Option<i64>) -> Result<()> {
let _: Value = self
.inner
.session
.send(
"Runtime.evaluate",
json!({
"expression": INJECT_SCRIPT,
"awaitPromise": false,
}),
)
.await?;
let expr = match time {
Some(t) => format!(
"(globalThis.__pwcdpClock && globalThis.__pwcdpClock.setFixed({t}))"
),
None => "(globalThis.__pwcdpClock && globalThis.__pwcdpClock.setFixed())"
.to_string(),
};
let _: Value = self
.inner
.session
.send(
"Runtime.evaluate",
json!({ "expression": expr, "awaitPromise": false }),
)
.await?;
Ok(())
}
pub async fn advance(&self, ms: i64) -> Result<()> {
if ms < 0 {
return Err(Error::InvalidArgument(format!(
"Clock::advance expects a non-negative duration, got {ms}"
)));
}
self.eval_controller_op(&format!("advance({ms})")).await
}
pub async fn tick(&self, ms: i64) -> Result<()> {
if ms < 0 {
return Err(Error::InvalidArgument(format!(
"Clock::tick expects a non-negative duration, got {ms}"
)));
}
self.eval_controller_op(&format!("tick({ms})")).await
}
pub async fn run_all(&self) -> Result<()> {
self.eval_controller_op("runAll()").await
}
pub async fn resume(&self) -> Result<()> {
self.eval_controller_op("resume()").await
}
pub async fn resume_all(&self) -> Result<()> {
self.resume().await
}
async fn eval_controller_op(&self, op: &str) -> Result<()> {
let resp: Value = self
.inner
.session
.send(
"Runtime.evaluate",
json!({
"expression": format!("globalThis.__pwcdpClock && globalThis.__pwcdpClock.{op}"),
"awaitPromise": false,
}),
)
.await?;
let installed = resp
.get("result")
.and_then(|r| r.get("value"))
.map(|v| v.as_bool().unwrap_or(true))
.unwrap_or(true);
if !installed {
return Err(Error::ProtocolError(
"Clock controller is not installed; call Clock::install() first".into(),
));
}
Ok(())
}
}
fn current_unix_ms() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
const INJECT_SCRIPT: &str = r#"(function(){
if (globalThis.__pwcdpClock) return true;
var g = globalThis;
var origDate = g.Date;
// Capture the REAL Date.now before it is overridden, otherwise
// virtualNow()/realAtInstall would recurse into FakeDate.now (stack overflow).
var origDateNow = g.Date.now.bind(g.Date);
var origNow = (typeof performance !== 'undefined' && performance.now) ? performance.now.bind(performance) : null;
var origSetTimeout = g.setTimeout, origClearTimeout = g.clearTimeout;
var origSetInterval = g.setInterval, origClearInterval = g.clearInterval;
var origRAF = g.requestAnimationFrame, origCancelRAF = g.cancelAnimationFrame;
var installed = false;
var fixed = false;
var base = origDateNow();
var realAtInstall = origDateNow();
var nextId = 1;
var timers = {};
function virtualNow() {
if (fixed) return base;
return base + (origDateNow() - realAtInstall);
}
function FakeDate() {
if (!(this instanceof FakeDate)) return new origDate(virtualNow());
if (arguments.length === 0) return new origDate(virtualNow());
return new (Function.prototype.bind.apply(origDate, [null].concat([].slice.call(arguments))));
}
FakeDate.prototype = origDate.prototype;
FakeDate.now = function(){ return virtualNow(); };
FakeDate.parse = origDate.parse;
FakeDate.UTC = origDate.UTC;
FakeDate.toString = function(){ return origDate.toString(); };
function fakeSetTimeout(cb, delay) {
var id = nextId++;
var d = Math.max(0, Number(delay) || 0);
var args = [].slice.call(arguments, 2);
timers[id] = { fireAt: virtualNow() + d, interval: null, cb: cb, args: args };
return id;
}
function fakeSetInterval(cb, delay) {
var id = nextId++;
var d = Math.max(0, Number(delay) || 0);
var args = [].slice.call(arguments, 2);
timers[id] = { fireAt: virtualNow() + d, interval: d, cb: cb, args: args };
return id;
}
function fakeClear(id) {
if (id != null) delete timers[id];
}
function fakeRAF(cb) {
return fakeSetTimeout(function(){ cb(virtualNow()); }, 16);
}
function installGlobals() {
if (installed) return;
g.Date = FakeDate;
g.setTimeout = fakeSetTimeout;
g.clearTimeout = fakeClear;
g.setInterval = fakeSetInterval;
g.clearInterval = fakeClear;
g.requestAnimationFrame = fakeRAF;
g.cancelAnimationFrame = fakeClear;
if (origNow && typeof performance !== 'undefined') {
var startVirtual = virtualNow();
performance.now = function(){ return (virtualNow() - startVirtual); };
}
installed = true;
}
function restoreGlobals() {
if (!installed) return;
g.Date = origDate;
g.setTimeout = origSetTimeout;
g.clearTimeout = origClearTimeout;
g.setInterval = origSetInterval;
g.clearInterval = origClearInterval;
g.requestAnimationFrame = origRAF;
g.cancelAnimationFrame = origCancelRAF;
if (origNow && typeof performance !== 'undefined') performance.now = origNow;
installed = false;
}
function drain(upto) {
var guard = 0;
var fired = true;
while (fired && guard < 100000) {
fired = false;
guard++;
var due = [];
for (var id in timers) {
if (timers[id].fireAt <= upto) due.push([Number(id), timers[id].fireAt]);
}
if (due.length === 0) break;
due.sort(function(a,b){ return a[1] - b[1]; });
var pick = due[0][0];
var t = timers[pick];
if (!t) continue;
try { t.cb.apply(null, t.args || []); } catch (e) { /* swallow */ }
fired = true;
if (t.interval != null) {
t.fireAt = upto + t.interval - ((upto - t.fireAt) % (t.interval || 1));
} else {
delete timers[pick];
}
}
}
g.__pwcdpClock = {
setNow: function(ms){ base = Number(ms) || 0; realAtInstall = origDateNow(); return true; },
setFixed: function(ms){
if (typeof ms === 'number') { base = ms; realAtInstall = origDateNow(); }
fixed = true;
installGlobals();
return true;
},
advance: function(ms){
installGlobals();
fixed = false;
var target = virtualNow() + (Number(ms) || 0);
base = target;
realAtInstall = origDateNow();
drain(target);
return true;
},
tick: function(ms){
installGlobals();
fixed = false;
base = virtualNow() + (Number(ms) || 0);
realAtInstall = origDateNow();
return true;
},
runAll: function(){
installGlobals();
fixed = false;
drain(virtualNow());
return true;
},
resume: function(){
restoreGlobals();
fixed = false;
timers = {};
base = origDateNow();
realAtInstall = origDateNow();
return true;
}
};
return true;
})();
"#;