diff --git a/docs/api/vi.md b/docs/api/vi.md
index 0c7e89530413..3d534a785cdd 100644
--- a/docs/api/vi.md
+++ b/docs/api/vi.md
@@ -603,6 +603,24 @@ await vi.advanceTimersToNextTimerAsync() // log: 2
await vi.advanceTimersToNextTimerAsync() // log: 3
```
+### vi.advanceTimersToNextFrame 2.1.0 {#vi-advancetimerstonextframe}
+
+- **Type:** `() => Vitest`
+
+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`.
+
+```ts
+let frameRendered = false
+
+requestAnimationFrame(() => {
+ frameRendered = true
+})
+
+vi.advanceTimersToNextFrame()
+
+expect(frameRendered).toBe(true)
+```
+
### vi.getTimerCount
- **Type:** `() => number`
diff --git a/packages/vitest/src/integrations/mock/timers.ts b/packages/vitest/src/integrations/mock/timers.ts
index 12a082c3ec6e..d985d049a7a1 100644
--- a/packages/vitest/src/integrations/mock/timers.ts
+++ b/packages/vitest/src/integrations/mock/timers.ts
@@ -113,6 +113,12 @@ export class FakeTimers {
}
}
+ advanceTimersToNextFrame(): void {
+ if (this._checkFakeTimers()) {
+ this._clock.runToFrame()
+ }
+ }
+
runAllTicks(): void {
if (this._checkFakeTimers()) {
// @ts-expect-error method not exposed
diff --git a/packages/vitest/src/integrations/vi.ts b/packages/vitest/src/integrations/vi.ts
index 7c3133188bdc..611d0281fc0b 100644
--- a/packages/vitest/src/integrations/vi.ts
+++ b/packages/vitest/src/integrations/vi.ts
@@ -73,6 +73,10 @@ export interface VitestUtils {
* Will call next available timer and wait until it's resolved if it was set asynchronously. Useful to make assertions between each timer call.
*/
advanceTimersToNextTimerAsync: () => Promise
+ /**
+ * 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`.
+ */
+ advanceTimersToNextFrame: () => VitestUtils
/**
* Get the number of waiting timers.
*/
@@ -511,6 +515,11 @@ function createVitest(): VitestUtils {
return utils
},
+ advanceTimersToNextFrame() {
+ timers().advanceTimersToNextFrame()
+ return utils
+ },
+
getTimerCount() {
return timers().getTimerCount()
},
diff --git a/test/core/test/fixtures/timers.suite.ts b/test/core/test/fixtures/timers.suite.ts
index 3afc1e894cf7..f82a687c0427 100644
--- a/test/core/test/fixtures/timers.suite.ts
+++ b/test/core/test/fixtures/timers.suite.ts
@@ -811,6 +811,202 @@ describe('FakeTimers', () => {
})
})
+ describe('advanceTimersToNextFrame', () => {
+ it('runs scheduled animation frame callbacks in order', () => {
+ const global = {
+ Date,
+ clearTimeout,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ timers.useFakeTimers()
+
+ const runOrder: Array = []
+ const mock1 = vi.fn(() => runOrder.push('mock1'))
+ const mock2 = vi.fn(() => runOrder.push('mock2'))
+ const mock3 = vi.fn(() => runOrder.push('mock3'))
+
+ global.requestAnimationFrame(mock1)
+ global.requestAnimationFrame(mock2)
+ global.requestAnimationFrame(mock3)
+
+ timers.advanceTimersToNextFrame()
+
+ expect(runOrder).toEqual(['mock1', 'mock2', 'mock3'])
+ })
+
+ it('should only run currently scheduled animation frame callbacks', () => {
+ const global = {
+ Date,
+ clearTimeout,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ timers.useFakeTimers()
+
+ const runOrder: Array = []
+ function run() {
+ runOrder.push('first-frame')
+
+ // scheduling another animation frame in the first frame
+ global.requestAnimationFrame(() => runOrder.push('second-frame'))
+ }
+
+ global.requestAnimationFrame(run)
+
+ // only the first frame should be executed
+ timers.advanceTimersToNextFrame()
+
+ expect(runOrder).toEqual(['first-frame'])
+
+ timers.advanceTimersToNextFrame()
+
+ expect(runOrder).toEqual(['first-frame', 'second-frame'])
+ })
+
+ it('should allow cancelling of scheduled animation frame callbacks', () => {
+ const global = {
+ Date,
+ cancelAnimationFrame: () => {},
+ clearTimeout,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ const callback = vi.fn()
+ timers.useFakeTimers()
+
+ const timerId = global.requestAnimationFrame(callback)
+ global.cancelAnimationFrame(timerId)
+
+ timers.advanceTimersToNextFrame()
+
+ expect(callback).not.toHaveBeenCalled()
+ })
+
+ it('should only advance as much time is needed to get to the next frame', () => {
+ const global = {
+ Date,
+ cancelAnimationFrame: () => {},
+ clearTimeout,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ timers.useFakeTimers()
+
+ const runOrder: Array = []
+ const start = global.Date.now()
+
+ const callback = () => runOrder.push('frame')
+ global.requestAnimationFrame(callback)
+
+ // Advancing timers less than a frame (which is 16ms)
+ timers.advanceTimersByTime(6)
+ expect(global.Date.now()).toEqual(start + 6)
+
+ // frame not yet executed
+ expect(runOrder).toEqual([])
+
+ // move timers forward to execute frame
+ timers.advanceTimersToNextFrame()
+
+ // frame has executed as time has moved forward 10ms to get to the 16ms frame time
+ expect(runOrder).toEqual(['frame'])
+ expect(global.Date.now()).toEqual(start + 16)
+ })
+
+ it('should execute any timers on the way to the animation frame', () => {
+ const global = {
+ Date,
+ cancelAnimationFrame: () => {},
+ clearTimeout,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ timers.useFakeTimers()
+
+ const runOrder: Array = []
+
+ global.requestAnimationFrame(() => runOrder.push('frame'))
+
+ // scheduling a timeout that will be executed on the way to the frame
+ global.setTimeout(() => runOrder.push('timeout'), 10)
+
+ // move timers forward to execute frame
+ timers.advanceTimersToNextFrame()
+
+ expect(runOrder).toEqual(['timeout', 'frame'])
+ })
+
+ it('should not execute any timers scheduled inside of an animation frame callback', () => {
+ const global = {
+ Date,
+ cancelAnimationFrame: () => {},
+ clearTimeout,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ timers.useFakeTimers()
+
+ const runOrder: Array = []
+
+ global.requestAnimationFrame(() => {
+ runOrder.push('frame')
+ // scheduling a timer inside of a frame
+ global.setTimeout(() => runOrder.push('timeout'), 1)
+ })
+
+ timers.advanceTimersToNextFrame()
+
+ // timeout not yet executed
+ expect(runOrder).toEqual(['frame'])
+
+ // validating that the timer will still be executed
+ timers.advanceTimersByTime(1)
+ expect(runOrder).toEqual(['frame', 'timeout'])
+ })
+
+ it('should call animation frame callbacks with the latest system time', () => {
+ const global = {
+ Date,
+ clearTimeout,
+ performance,
+ process,
+ requestAnimationFrame: () => -1,
+ setTimeout,
+ } as unknown as typeof globalThis
+
+ const timers = new FakeTimers({ global })
+ timers.useFakeTimers()
+
+ const callback = vi.fn()
+
+ global.requestAnimationFrame(callback)
+
+ timers.advanceTimersToNextFrame()
+
+ // `requestAnimationFrame` callbacks are called with a `DOMHighResTimeStamp`
+ expect(callback).toHaveBeenCalledWith(global.performance.now())
+ })
+ })
+
describe('reset', () => {
it('resets all pending setTimeouts', () => {
const global = { Date: FakeDate, clearTimeout, process, setTimeout }