Skip to content

Commit

Permalink
feat(vitest): add vi.advanceTimersToNextFrame (#6347)
Browse files Browse the repository at this point in the history
Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>
  • Loading branch information
bnjm and sheremet-va committed Sep 12, 2024
1 parent 80c553a commit 0d89aeb
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 0 deletions.
18 changes: 18 additions & 0 deletions docs/api/vi.md
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,24 @@ await vi.advanceTimersToNextTimerAsync() // log: 2
await vi.advanceTimersToNextTimerAsync() // log: 3
```

### vi.advanceTimersToNextFrame <Version>2.1.0</Version> {#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`
Expand Down
6 changes: 6 additions & 0 deletions packages/vitest/src/integrations/mock/timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/vitest/src/integrations/vi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VitestUtils>
/**
* 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.
*/
Expand Down Expand Up @@ -511,6 +515,11 @@ function createVitest(): VitestUtils {
return utils
},

advanceTimersToNextFrame() {
timers().advanceTimersToNextFrame()
return utils
},

getTimerCount() {
return timers().getTimerCount()
},
Expand Down
196 changes: 196 additions & 0 deletions test/core/test/fixtures/timers.suite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> = []
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<string> = []
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<string> = []
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<string> = []

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<string> = []

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 }
Expand Down

0 comments on commit 0d89aeb

Please sign in to comment.