Skip to content

Commit b2236c8

Browse files
authored
fix(clock): do not advance into the past (#37820)
1 parent 3afe7ba commit b2236c8

File tree

2 files changed

+47
-0
lines changed

2 files changed

+47
-0
lines changed

packages/injected/src/clock.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export class ClockController {
9090

9191
now(): number {
9292
this._replayLogOnce();
93+
// Sync real time to support calling Date.now() in a loop.
9394
this._syncRealTime();
9495
return this._now.time;
9596
}
@@ -111,6 +112,7 @@ export class ClockController {
111112

112113
performanceNow(): DOMHighResTimeStamp {
113114
this._replayLogOnce();
115+
// Sync real time to support calling performance.now() in a loop.
114116
this._syncRealTime();
115117
return this._now.ticks;
116118
}
@@ -139,6 +141,12 @@ export class ClockController {
139141
}
140142

141143
private _advanceNow(to: Ticks) {
144+
if (this._now.ticks > to) {
145+
// While running timers, `now` can advance by syncing with real time
146+
// from within now() or performance.now().
147+
// This makes it possible for `now` to be ahead of where we want to advance it.
148+
return;
149+
}
142150
if (!this._now.isFixedTime)
143151
this._now.time = asWallTime(this._now.time + to - this._now.ticks);
144152
this._now.ticks = to;
@@ -172,6 +180,7 @@ export class ClockController {
172180
}
173181

174182
this._advanceNow(to);
183+
175184
if (firstException)
176185
throw firstException;
177186
}
@@ -375,6 +384,9 @@ export class ClockController {
375384
}
376385

377386
getTimeToNextFrame() {
387+
// When `window.requestAnimationFrame` is the first call in the page,
388+
// this place is the first API call, so replay the log.
389+
this._replayLogOnce();
378390
return 16 - this._now.ticks % 16;
379391
}
380392

tests/library/unit/clock.spec.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,29 @@ it.describe('runFor', () => {
730730

731731
expect(spies[0].calledBefore(spies[1])).toBeTruthy();
732732
});
733+
734+
it('does not rewind back in time', async ({ clock }) => {
735+
const stub = createStub();
736+
const gotTime = await new Promise<number>(done => {
737+
clock.setTimeout(() => {
738+
stub(clock.Date.now());
739+
}, 10);
740+
clock.setTimeout(() => {
741+
stub(clock.Date.now());
742+
}, 10);
743+
clock.resume();
744+
setTimeout(async () => {
745+
// Call fast-forward right after the real time sync happens,
746+
// but before all the callbacks are processed.
747+
await clock.runFor(1000);
748+
setTimeout(() => {
749+
done(clock.Date.now());
750+
}, 20);
751+
}, 10);
752+
});
753+
expect(stub.callCount).toBe(2);
754+
expect(gotTime).toBeGreaterThan(1010);
755+
});
733756
});
734757

735758
it.describe('clearTimeout', () => {
@@ -1419,6 +1442,18 @@ it.describe('fastForward', () => {
14191442
expect(stub.callCount).toBe(1);
14201443
expect(stub.calledWith(2000)).toBeTruthy();
14211444
});
1445+
1446+
it('error does not pause forever', async ({ clock }) => {
1447+
const stub = createStub();
1448+
clock.setTimeout(() => {
1449+
stub(clock.Date.now());
1450+
}, 1000);
1451+
clock.resume();
1452+
const error = await clock.fastForward(-1000).catch(e => e);
1453+
expect(error.message).toContain('Cannot fast-forward to the past');
1454+
await new Promise(f => setTimeout(f, 1500));
1455+
expect(stub.callCount).toBe(1);
1456+
});
14221457
});
14231458

14241459
it.describe('pauseAt', () => {

0 commit comments

Comments
 (0)