1use crate::cdp::session::CdpSession;
24use crate::error::{Error, Result};
25use serde_json::{json, Value};
26use std::sync::Arc;
27use std::time::{SystemTime, UNIX_EPOCH};
28
29#[derive(Clone)]
33pub struct Clock {
34 inner: Arc<ClockInner>,
35}
36
37struct ClockInner {
38 session: CdpSession,
40}
41
42#[derive(Debug, Clone, Default)]
44#[non_exhaustive]
45pub struct ClockInstallOptions {
46 pub time: Option<i64>,
49 pub fixed: Option<bool>,
52}
53
54impl ClockInstallOptions {
55 pub fn time(mut self, v: i64) -> Self {
56 self.time = Some(v);
57 self
58 }
59 pub fn fixed(mut self, v: bool) -> Self {
60 self.fixed = Some(v);
61 self
62 }
63}
64
65impl Clock {
66 pub(crate) fn new(session: CdpSession) -> Self {
67 Self {
68 inner: Arc::new(ClockInner { session }),
69 }
70 }
71
72 pub async fn install(&self, options: Option<ClockInstallOptions>) -> Result<()> {
76 let opts = options.unwrap_or_default();
77 let now_ms = opts.time.unwrap_or_else(current_unix_ms);
78
79 let _: Value = self
81 .inner
82 .session
83 .send(
84 "Runtime.evaluate",
85 json!({
86 "expression": INJECT_SCRIPT,
87 "awaitPromise": false,
88 }),
89 )
90 .await?;
91
92 let _: Value = self
94 .inner
95 .session
96 .send(
97 "Runtime.evaluate",
98 json!({
99 "expression": format!("(globalThis.__pwcdpClock && globalThis.__pwcdpClock.setNow({now_ms}))"),
100 "awaitPromise": false,
101 }),
102 )
103 .await?;
104
105 if opts.fixed.unwrap_or(false) {
106 self.set_fixed_time(Some(now_ms)).await?;
107 }
108 Ok(())
109 }
110
111 pub async fn set_fixed_time(&self, time: Option<i64>) -> Result<()> {
117 let _: Value = self
119 .inner
120 .session
121 .send(
122 "Runtime.evaluate",
123 json!({
124 "expression": INJECT_SCRIPT,
125 "awaitPromise": false,
126 }),
127 )
128 .await?;
129
130 let expr = match time {
131 Some(t) => format!(
132 "(globalThis.__pwcdpClock && globalThis.__pwcdpClock.setFixed({t}))"
133 ),
134 None => "(globalThis.__pwcdpClock && globalThis.__pwcdpClock.setFixed())"
135 .to_string(),
136 };
137 let _: Value = self
138 .inner
139 .session
140 .send(
141 "Runtime.evaluate",
142 json!({ "expression": expr, "awaitPromise": false }),
143 )
144 .await?;
145 Ok(())
146 }
147
148 pub async fn advance(&self, ms: i64) -> Result<()> {
152 if ms < 0 {
153 return Err(Error::InvalidArgument(format!(
154 "Clock::advance expects a non-negative duration, got {ms}"
155 )));
156 }
157 self.eval_controller_op(&format!("advance({ms})")).await
158 }
159
160 pub async fn tick(&self, ms: i64) -> Result<()> {
164 if ms < 0 {
165 return Err(Error::InvalidArgument(format!(
166 "Clock::tick expects a non-negative duration, got {ms}"
167 )));
168 }
169 self.eval_controller_op(&format!("tick({ms})")).await
170 }
171
172 pub async fn run_all(&self) -> Result<()> {
175 self.eval_controller_op("runAll()").await
176 }
177
178 pub async fn resume(&self) -> Result<()> {
182 self.eval_controller_op("resume()").await
183 }
184
185 pub async fn resume_all(&self) -> Result<()> {
187 self.resume().await
188 }
189
190 async fn eval_controller_op(&self, op: &str) -> Result<()> {
194 let resp: Value = self
195 .inner
196 .session
197 .send(
198 "Runtime.evaluate",
199 json!({
200 "expression": format!("globalThis.__pwcdpClock && globalThis.__pwcdpClock.{op}"),
201 "awaitPromise": false,
202 }),
203 )
204 .await?;
205
206 let installed = resp
209 .get("result")
210 .and_then(|r| r.get("value"))
211 .map(|v| v.as_bool().unwrap_or(true))
212 .unwrap_or(true);
213 if !installed {
214 return Err(Error::ProtocolError(
215 "Clock controller is not installed; call Clock::install() first".into(),
216 ));
217 }
218 Ok(())
219 }
220}
221
222fn current_unix_ms() -> i64 {
224 SystemTime::now()
225 .duration_since(UNIX_EPOCH)
226 .map(|d| d.as_millis() as i64)
227 .unwrap_or(0)
228}
229
230const INJECT_SCRIPT: &str = r#"(function(){
240 if (globalThis.__pwcdpClock) return true;
241 var g = globalThis;
242 var origDate = g.Date;
243 // Capture the REAL Date.now before it is overridden, otherwise
244 // virtualNow()/realAtInstall would recurse into FakeDate.now (stack overflow).
245 var origDateNow = g.Date.now.bind(g.Date);
246 var origNow = (typeof performance !== 'undefined' && performance.now) ? performance.now.bind(performance) : null;
247 var origSetTimeout = g.setTimeout, origClearTimeout = g.clearTimeout;
248 var origSetInterval = g.setInterval, origClearInterval = g.clearInterval;
249 var origRAF = g.requestAnimationFrame, origCancelRAF = g.cancelAnimationFrame;
250
251 var installed = false;
252 var fixed = false;
253 var base = origDateNow();
254 var realAtInstall = origDateNow();
255 var nextId = 1;
256 var timers = {};
257
258 function virtualNow() {
259 if (fixed) return base;
260 return base + (origDateNow() - realAtInstall);
261 }
262
263 function FakeDate() {
264 if (!(this instanceof FakeDate)) return new origDate(virtualNow());
265 if (arguments.length === 0) return new origDate(virtualNow());
266 return new (Function.prototype.bind.apply(origDate, [null].concat([].slice.call(arguments))));
267 }
268 FakeDate.prototype = origDate.prototype;
269 FakeDate.now = function(){ return virtualNow(); };
270 FakeDate.parse = origDate.parse;
271 FakeDate.UTC = origDate.UTC;
272 FakeDate.toString = function(){ return origDate.toString(); };
273
274 function fakeSetTimeout(cb, delay) {
275 var id = nextId++;
276 var d = Math.max(0, Number(delay) || 0);
277 var args = [].slice.call(arguments, 2);
278 timers[id] = { fireAt: virtualNow() + d, interval: null, cb: cb, args: args };
279 return id;
280 }
281 function fakeSetInterval(cb, delay) {
282 var id = nextId++;
283 var d = Math.max(0, Number(delay) || 0);
284 var args = [].slice.call(arguments, 2);
285 timers[id] = { fireAt: virtualNow() + d, interval: d, cb: cb, args: args };
286 return id;
287 }
288 function fakeClear(id) {
289 if (id != null) delete timers[id];
290 }
291 function fakeRAF(cb) {
292 return fakeSetTimeout(function(){ cb(virtualNow()); }, 16);
293 }
294
295 function installGlobals() {
296 if (installed) return;
297 g.Date = FakeDate;
298 g.setTimeout = fakeSetTimeout;
299 g.clearTimeout = fakeClear;
300 g.setInterval = fakeSetInterval;
301 g.clearInterval = fakeClear;
302 g.requestAnimationFrame = fakeRAF;
303 g.cancelAnimationFrame = fakeClear;
304 if (origNow && typeof performance !== 'undefined') {
305 var startVirtual = virtualNow();
306 performance.now = function(){ return (virtualNow() - startVirtual); };
307 }
308 installed = true;
309 }
310 function restoreGlobals() {
311 if (!installed) return;
312 g.Date = origDate;
313 g.setTimeout = origSetTimeout;
314 g.clearTimeout = origClearTimeout;
315 g.setInterval = origSetInterval;
316 g.clearInterval = origClearInterval;
317 g.requestAnimationFrame = origRAF;
318 g.cancelAnimationFrame = origCancelRAF;
319 if (origNow && typeof performance !== 'undefined') performance.now = origNow;
320 installed = false;
321 }
322
323 function drain(upto) {
324 var guard = 0;
325 var fired = true;
326 while (fired && guard < 100000) {
327 fired = false;
328 guard++;
329 var due = [];
330 for (var id in timers) {
331 if (timers[id].fireAt <= upto) due.push([Number(id), timers[id].fireAt]);
332 }
333 if (due.length === 0) break;
334 due.sort(function(a,b){ return a[1] - b[1]; });
335 var pick = due[0][0];
336 var t = timers[pick];
337 if (!t) continue;
338 try { t.cb.apply(null, t.args || []); } catch (e) { /* swallow */ }
339 fired = true;
340 if (t.interval != null) {
341 t.fireAt = upto + t.interval - ((upto - t.fireAt) % (t.interval || 1));
342 } else {
343 delete timers[pick];
344 }
345 }
346 }
347
348 g.__pwcdpClock = {
349 setNow: function(ms){ base = Number(ms) || 0; realAtInstall = origDateNow(); return true; },
350 setFixed: function(ms){
351 if (typeof ms === 'number') { base = ms; realAtInstall = origDateNow(); }
352 fixed = true;
353 installGlobals();
354 return true;
355 },
356 advance: function(ms){
357 installGlobals();
358 fixed = false;
359 var target = virtualNow() + (Number(ms) || 0);
360 base = target;
361 realAtInstall = origDateNow();
362 drain(target);
363 return true;
364 },
365 tick: function(ms){
366 installGlobals();
367 fixed = false;
368 base = virtualNow() + (Number(ms) || 0);
369 realAtInstall = origDateNow();
370 return true;
371 },
372 runAll: function(){
373 installGlobals();
374 fixed = false;
375 drain(virtualNow());
376 return true;
377 },
378 resume: function(){
379 restoreGlobals();
380 fixed = false;
381 timers = {};
382 base = origDateNow();
383 realAtInstall = origDateNow();
384 return true;
385 }
386 };
387 return true;
388})();
389"#;