Skip to content

Commit 8ff6356

Browse files
bnjmsheremet-va
andauthored
feat(vitest): add vi.advanceTimersToNextFrame (#6347)
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
1 parent 85fb94a commit 8ff6356

File tree

4 files changed

+229
-0
lines changed

4 files changed

+229
-0
lines changed

docs/api/vi.md

+18
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,24 @@ await vi.advanceTimersToNextTimerAsync() // log: 2
603603
await vi.advanceTimersToNextTimerAsync() // log: 3
604604
```
605605

606+
### vi.advanceTimersToNextFrame <Version>2.1.0</Version> {#vi-advancetimerstonextframe}
607+
608+
- **Type:** `() => Vitest`
609+
610+
Similar to [`vi.advanceTimersByTime`](https://vitest.dev/api/vi#vi-advancetimersbytime), but will advance timers by the milliseconds needed to execute callbacks currently scheduled with `requestAnimationFrame`.
611+
612+
```ts
613+
let frameRendered = false
614+
615+
requestAnimationFrame(() => {
616+
frameRendered = true
617+
})
618+
619+
vi.advanceTimersToNextFrame()
620+
621+
expect(frameRendered).toBe(true)
622+
```
623+
606624
### vi.getTimerCount
607625

608626
- **Type:** `() => number`

packages/vitest/src/integrations/mock/timers.ts

+6
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ export class FakeTimers {
113113
}
114114
}
115115

116+
advanceTimersToNextFrame(): void {
117+
if (this._checkFakeTimers()) {
118+
this._clock.runToFrame()
119+
}
120+
}
121+
116122
runAllTicks(): void {
117123
if (this._checkFakeTimers()) {
118124
// @ts-expect-error method not exposed

packages/vitest/src/integrations/vi.ts

+9
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ export interface VitestUtils {
7373
* Will call next available timer and wait until it's resolved if it was set asynchronously. Useful to make assertions between each timer call.
7474
*/
7575
advanceTimersToNextTimerAsync: () => Promise<VitestUtils>
76+
/**
77+
* Similar to [`vi.advanceTimersByTime`](https://vitest.dev/api/vi#vi-advancetimersbytime), but will advance timers by the milliseconds needed to execute callbacks currently scheduled with `requestAnimationFrame`.
78+
*/
79+
advanceTimersToNextFrame: () => VitestUtils
7680
/**
7781
* Get the number of waiting timers.
7882
*/
@@ -511,6 +515,11 @@ function createVitest(): VitestUtils {
511515
return utils
512516
},
513517

518+
advanceTimersToNextFrame() {
519+
timers().advanceTimersToNextFrame()
520+
return utils
521+
},
522+
514523
getTimerCount() {
515524
return timers().getTimerCount()
516525
},

test/core/test/fixtures/timers.suite.ts

+196
Original file line numberDiff line numberDiff line change
@@ -811,6 +811,202 @@ describe('FakeTimers', () => {
811811
})
812812
})
813813

814+
describe('advanceTimersToNextFrame', () => {
815+
it('runs scheduled animation frame callbacks in order', () => {
816+
const global = {
817+
Date,
818+
clearTimeout,
819+
process,
820+
requestAnimationFrame: () => -1,
821+
setTimeout,
822+
} as unknown as typeof globalThis
823+
824+
const timers = new FakeTimers({ global })
825+
timers.useFakeTimers()
826+
827+
const runOrder: Array<string> = []
828+
const mock1 = vi.fn(() => runOrder.push('mock1'))
829+
const mock2 = vi.fn(() => runOrder.push('mock2'))
830+
const mock3 = vi.fn(() => runOrder.push('mock3'))
831+
832+
global.requestAnimationFrame(mock1)
833+
global.requestAnimationFrame(mock2)
834+
global.requestAnimationFrame(mock3)
835+
836+
timers.advanceTimersToNextFrame()
837+
838+
expect(runOrder).toEqual(['mock1', 'mock2', 'mock3'])
839+
})
840+
841+
it('should only run currently scheduled animation frame callbacks', () => {
842+
const global = {
843+
Date,
844+
clearTimeout,
845+
process,
846+
requestAnimationFrame: () => -1,
847+
setTimeout,
848+
} as unknown as typeof globalThis
849+
850+
const timers = new FakeTimers({ global })
851+
timers.useFakeTimers()
852+
853+
const runOrder: Array<string> = []
854+
function run() {
855+
runOrder.push('first-frame')
856+
857+
// scheduling another animation frame in the first frame
858+
global.requestAnimationFrame(() => runOrder.push('second-frame'))
859+
}
860+
861+
global.requestAnimationFrame(run)
862+
863+
// only the first frame should be executed
864+
timers.advanceTimersToNextFrame()
865+
866+
expect(runOrder).toEqual(['first-frame'])
867+
868+
timers.advanceTimersToNextFrame()
869+
870+
expect(runOrder).toEqual(['first-frame', 'second-frame'])
871+
})
872+
873+
it('should allow cancelling of scheduled animation frame callbacks', () => {
874+
const global = {
875+
Date,
876+
cancelAnimationFrame: () => {},
877+
clearTimeout,
878+
process,
879+
requestAnimationFrame: () => -1,
880+
setTimeout,
881+
} as unknown as typeof globalThis
882+
883+
const timers = new FakeTimers({ global })
884+
const callback = vi.fn()
885+
timers.useFakeTimers()
886+
887+
const timerId = global.requestAnimationFrame(callback)
888+
global.cancelAnimationFrame(timerId)
889+
890+
timers.advanceTimersToNextFrame()
891+
892+
expect(callback).not.toHaveBeenCalled()
893+
})
894+
895+
it('should only advance as much time is needed to get to the next frame', () => {
896+
const global = {
897+
Date,
898+
cancelAnimationFrame: () => {},
899+
clearTimeout,
900+
process,
901+
requestAnimationFrame: () => -1,
902+
setTimeout,
903+
} as unknown as typeof globalThis
904+
905+
const timers = new FakeTimers({ global })
906+
timers.useFakeTimers()
907+
908+
const runOrder: Array<string> = []
909+
const start = global.Date.now()
910+
911+
const callback = () => runOrder.push('frame')
912+
global.requestAnimationFrame(callback)
913+
914+
// Advancing timers less than a frame (which is 16ms)
915+
timers.advanceTimersByTime(6)
916+
expect(global.Date.now()).toEqual(start + 6)
917+
918+
// frame not yet executed
919+
expect(runOrder).toEqual([])
920+
921+
// move timers forward to execute frame
922+
timers.advanceTimersToNextFrame()
923+
924+
// frame has executed as time has moved forward 10ms to get to the 16ms frame time
925+
expect(runOrder).toEqual(['frame'])
926+
expect(global.Date.now()).toEqual(start + 16)
927+
})
928+
929+
it('should execute any timers on the way to the animation frame', () => {
930+
const global = {
931+
Date,
932+
cancelAnimationFrame: () => {},
933+
clearTimeout,
934+
process,
935+
requestAnimationFrame: () => -1,
936+
setTimeout,
937+
} as unknown as typeof globalThis
938+
939+
const timers = new FakeTimers({ global })
940+
timers.useFakeTimers()
941+
942+
const runOrder: Array<string> = []
943+
944+
global.requestAnimationFrame(() => runOrder.push('frame'))
945+
946+
// scheduling a timeout that will be executed on the way to the frame
947+
global.setTimeout(() => runOrder.push('timeout'), 10)
948+
949+
// move timers forward to execute frame
950+
timers.advanceTimersToNextFrame()
951+
952+
expect(runOrder).toEqual(['timeout', 'frame'])
953+
})
954+
955+
it('should not execute any timers scheduled inside of an animation frame callback', () => {
956+
const global = {
957+
Date,
958+
cancelAnimationFrame: () => {},
959+
clearTimeout,
960+
process,
961+
requestAnimationFrame: () => -1,
962+
setTimeout,
963+
} as unknown as typeof globalThis
964+
965+
const timers = new FakeTimers({ global })
966+
timers.useFakeTimers()
967+
968+
const runOrder: Array<string> = []
969+
970+
global.requestAnimationFrame(() => {
971+
runOrder.push('frame')
972+
// scheduling a timer inside of a frame
973+
global.setTimeout(() => runOrder.push('timeout'), 1)
974+
})
975+
976+
timers.advanceTimersToNextFrame()
977+
978+
// timeout not yet executed
979+
expect(runOrder).toEqual(['frame'])
980+
981+
// validating that the timer will still be executed
982+
timers.advanceTimersByTime(1)
983+
expect(runOrder).toEqual(['frame', 'timeout'])
984+
})
985+
986+
it('should call animation frame callbacks with the latest system time', () => {
987+
const global = {
988+
Date,
989+
clearTimeout,
990+
performance,
991+
process,
992+
requestAnimationFrame: () => -1,
993+
setTimeout,
994+
} as unknown as typeof globalThis
995+
996+
const timers = new FakeTimers({ global })
997+
timers.useFakeTimers()
998+
999+
const callback = vi.fn()
1000+
1001+
global.requestAnimationFrame(callback)
1002+
1003+
timers.advanceTimersToNextFrame()
1004+
1005+
// `requestAnimationFrame` callbacks are called with a `DOMHighResTimeStamp`
1006+
expect(callback).toHaveBeenCalledWith(global.performance.now())
1007+
})
1008+
})
1009+
8141010
describe('reset', () => {
8151011
it('resets all pending setTimeouts', () => {
8161012
const global = { Date: FakeDate, clearTimeout, process, setTimeout }

0 commit comments

Comments
 (0)