From 949ffbf0ef5eb79dafcefbbc892914e7fc324139 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 14 Jun 2021 11:01:16 +0200 Subject: [PATCH 1/5] feat: Flush animation frames every 16ms when using legacy timers --- CHANGELOG.md | 2 + .../src/__tests__/legacyFakeTimers.test.ts | 282 ++++++++++++++++-- .../jest-fake-timers/src/legacyFakeTimers.ts | 49 ++- 3 files changed, 313 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 502e1cdd3274..5bbaef8f1cc9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ### Features +- `[jest-fake-timers]` Flush callbacks scheduled with `requestAnimationFrame` every 16ms when using legacy timers. + ### Fixes - `[jest-reporter]` Allow `node-notifier@10` as peer dependency ([#11523](https://github.com/facebook/jest/pull/11523)) diff --git a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts index d5fec2bae04f..bb8fe71adf35 100644 --- a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts @@ -142,6 +142,68 @@ describe('FakeTimers', () => { timers.useFakeTimers(); expect(global.clearImmediate).not.toBe(origClearImmediate); }); + + it('does not mock requestAnimationFrame if not available', () => { + const global = { + process, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + expect(global.requestAnimationFrame).toBe(undefined); + }); + + it('mocks requestAnimationFrame if available on global', () => { + const origRequestAnimationFrame = () => {}; + const global = { + process, + requestAnimationFrame: origRequestAnimationFrame, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + expect(global.requestAnimationFrame).not.toBe(undefined); + expect(global.requestAnimationFrame).not.toBe(origRequestAnimationFrame); + }); + + it('does not mock cancelAnimationFrame if not available on global', () => { + const global = { + process, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + expect(global.cancelAnimationFrame).toBe(undefined); + }); + + it('mocks cancelAnimationFrame if available on global', () => { + const origCancelAnimationFrame = () => {}; + const global = { + cancelAnimationFrame: origCancelAnimationFrame, + process, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + expect(global.cancelAnimationFrame).not.toBe(undefined); + expect(global.cancelAnimationFrame).not.toBe(origCancelAnimationFrame); + }); }); describe('runAllTicks', () => { @@ -399,7 +461,11 @@ describe('FakeTimers', () => { describe('runAllTimers', () => { it('runs all timers in order', () => { - const global = {process} as unknown as NodeJS.Global; + const global = { + cancelAnimationFrame: () => {}, + process, + requestAnimationFrame: () => {}, + } as unknown as NodeJS.Global & Window; const timers = new FakeTimers({ config, global, @@ -415,6 +481,7 @@ describe('FakeTimers', () => { const mock4 = jest.fn(() => runOrder.push('mock4')); const mock5 = jest.fn(() => runOrder.push('mock5')); const mock6 = jest.fn(() => runOrder.push('mock6')); + const mockAnimatioNFrame = jest.fn(() => runOrder.push('animationFrame')); global.setTimeout(mock1, 100); global.setTimeout(mock2, NaN); @@ -425,6 +492,7 @@ describe('FakeTimers', () => { }, 200); global.setTimeout(mock5, Infinity); global.setTimeout(mock6, -Infinity); + global.requestAnimationFrame(mockAnimatioNFrame); timers.runAllTimers(); expect(runOrder).toEqual([ @@ -432,6 +500,7 @@ describe('FakeTimers', () => { 'mock3', 'mock5', 'mock6', + 'animationFrame', 'mock1', 'mock4', ]); @@ -585,7 +654,11 @@ describe('FakeTimers', () => { describe('advanceTimersByTime', () => { it('runs timers in order', () => { - const global = {process} as unknown as NodeJS.Global; + const global = { + cancelAnimationFrame: () => {}, + process, + requestAnimationFrame: () => {}, + } as unknown as NodeJS.Global & Window; const timers = new FakeTimers({ config, global, @@ -599,6 +672,7 @@ describe('FakeTimers', () => { const mock2 = jest.fn(() => runOrder.push('mock2')); const mock3 = jest.fn(() => runOrder.push('mock3')); const mock4 = jest.fn(() => runOrder.push('mock4')); + const mockAnimationFrame = jest.fn(() => runOrder.push('animationFrame')); global.setTimeout(mock1, 100); global.setTimeout(mock2, 0); @@ -606,26 +680,44 @@ describe('FakeTimers', () => { global.setInterval(() => { mock4(); }, 200); + global.requestAnimationFrame(mockAnimationFrame); - // Move forward to t=50 - timers.advanceTimersByTime(50); + // Move forward to t=15 + timers.advanceTimersByTime(15); expect(runOrder).toEqual(['mock2', 'mock3']); + // Move forward to t=17 + timers.advanceTimersByTime(2); + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame']); + // Move forward to t=60 - timers.advanceTimersByTime(10); - expect(runOrder).toEqual(['mock2', 'mock3']); + timers.advanceTimersByTime(43); + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame']); // Move forward to t=100 timers.advanceTimersByTime(40); - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame', 'mock1']); // Move forward to t=200 timers.advanceTimersByTime(100); - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'animationFrame', + 'mock1', + 'mock4', + ]); // Move forward to t=400 timers.advanceTimersByTime(200); - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'animationFrame', + 'mock1', + 'mock4', + 'mock4', + ]); }); it('does nothing when no timers have been scheduled', () => { @@ -668,7 +760,11 @@ describe('FakeTimers', () => { describe('advanceTimersToNextTimer', () => { it('runs timers in order', () => { - const global = {process} as unknown as NodeJS.Global; + const global = { + cancelAnimationFrame: () => {}, + process, + requestAnimationFrame: () => {}, + } as unknown as NodeJS.Global & Window; const timers = new FakeTimers({ config, global, @@ -682,6 +778,7 @@ describe('FakeTimers', () => { const mock2 = jest.fn(() => runOrder.push('mock2')); const mock3 = jest.fn(() => runOrder.push('mock3')); const mock4 = jest.fn(() => runOrder.push('mock4')); + const mockAnimationFrame = jest.fn(() => runOrder.push('animationFrame')); global.setTimeout(mock1, 100); global.setTimeout(mock2, 0); @@ -689,26 +786,48 @@ describe('FakeTimers', () => { global.setInterval(() => { mock4(); }, 200); + global.requestAnimationFrame(mockAnimationFrame); timers.advanceTimersToNextTimer(); // Move forward to t=0 expect(runOrder).toEqual(['mock2', 'mock3']); + timers.advanceTimersToNextTimer(); + // Move forward to t=17 + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame']); + timers.advanceTimersToNextTimer(); // Move forward to t=100 - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame', 'mock1']); timers.advanceTimersToNextTimer(); // Move forward to t=200 - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4']); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'animationFrame', + 'mock1', + 'mock4', + ]); timers.advanceTimersToNextTimer(); // Move forward to t=400 - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1', 'mock4', 'mock4']); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + 'animationFrame', + 'mock1', + 'mock4', + 'mock4', + ]); }); it('run correct amount of steps', () => { - const global = {process} as unknown as NodeJS.Global; + const global = { + cancelAnimationFrame: () => {}, + process, + requestAnimationFrame: () => {}, + } as unknown as NodeJS.Global & Window; const timers = new FakeTimers({ config, global, @@ -722,6 +841,7 @@ describe('FakeTimers', () => { const mock2 = jest.fn(() => runOrder.push('mock2')); const mock3 = jest.fn(() => runOrder.push('mock3')); const mock4 = jest.fn(() => runOrder.push('mock4')); + const mockAnimationFrame = jest.fn(() => runOrder.push('animationFrame')); global.setTimeout(mock1, 100); global.setTimeout(mock2, 0); @@ -729,16 +849,22 @@ describe('FakeTimers', () => { global.setInterval(() => { mock4(); }, 200); + global.requestAnimationFrame(mockAnimationFrame); - // Move forward to t=100 + // Move forward to t=17 timers.advanceTimersToNextTimer(2); - expect(runOrder).toEqual(['mock2', 'mock3', 'mock1']); + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame']); + + // Move forward to t=100 + timers.advanceTimersToNextTimer(1); + expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame', 'mock1']); // Move forward to t=600 timers.advanceTimersToNextTimer(3); expect(runOrder).toEqual([ 'mock2', 'mock3', + 'animationFrame', 'mock1', 'mock4', 'mock4', @@ -825,6 +951,28 @@ describe('FakeTimers', () => { expect(mock1).toHaveBeenCalledTimes(0); }); + it('resets all pending animation frames', () => { + const global = { + cancelAnimationFrame: () => {}, + process, + requestAnimationFrame: () => {}, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + + const mock1 = jest.fn(); + global.requestAnimationFrame(mock1); + + timers.reset(); + timers.runAllTimers(); + expect(mock1).toHaveBeenCalledTimes(0); + }); + it('resets all pending ticks callbacks & immediates', () => { const global = { process: { @@ -877,9 +1025,11 @@ describe('FakeTimers', () => { const nativeSetImmediate = jest.fn(); const global = { + cancelAnimationFrame: () => {}, process, + requestAnimationFrame: () => {}, setImmediate: nativeSetImmediate, - } as unknown as NodeJS.Global; + } as unknown as NodeJS.Global & Window; const timers = new FakeTimers({ config, @@ -914,18 +1064,32 @@ describe('FakeTimers', () => { global.setTimeout(cb, 400); }); + global.requestAnimationFrame(function cb() { + runOrder.push('animationFrame'); + global.requestAnimationFrame(cb); + }); + timers.runOnlyPendingTimers(); - expect(runOrder).toEqual(['mock4', 'mock5', 'mock2', 'mock1', 'mock3']); + expect(runOrder).toEqual([ + 'mock4', + 'mock5', + 'mock2', + 'animationFrame', + 'mock1', + 'mock3', + ]); timers.runOnlyPendingTimers(); expect(runOrder).toEqual([ 'mock4', 'mock5', 'mock2', + 'animationFrame', 'mock1', 'mock3', 'mock2', + 'animationFrame', 'mock1', 'mock3', 'mock5', @@ -1186,6 +1350,36 @@ describe('FakeTimers', () => { expect(global.setImmediate).toBe(nativeSetImmediate); expect(global.clearImmediate).toBe(nativeClearImmediate); }); + + it('resets native requestAnimationFrame when present', () => { + const nativeCancelAnimationFrame = jest.fn(); + const nativeRequestAnimationFrame = jest.fn(); + + const global = { + cancelAnimationFrame: nativeCancelAnimationFrame, + process, + requestAnimationFrame: nativeRequestAnimationFrame, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + timers.useFakeTimers(); + + // Ensure that timers has overridden the native timer APIs + // (because if it didn't, this test might pass when it shouldn't) + expect(global.cancelAnimationFrame).not.toBe(nativeCancelAnimationFrame); + expect(global.requestAnimationFrame).not.toBe( + nativeRequestAnimationFrame, + ); + + timers.useRealTimers(); + + expect(global.cancelAnimationFrame).toBe(nativeCancelAnimationFrame); + expect(global.requestAnimationFrame).toBe(nativeRequestAnimationFrame); + }); }); describe('useFakeTimers', () => { @@ -1275,6 +1469,36 @@ describe('FakeTimers', () => { expect(global.setImmediate).not.toBe(nativeSetImmediate); expect(global.clearImmediate).not.toBe(nativeClearImmediate); }); + + it('resets mock requestAnimationFrame when present', () => { + const nativeCancelAnimationFrame = jest.fn(); + const nativeRequestAnimationFrame = jest.fn(); + + const global = { + cancelAnimationFrame: nativeCancelAnimationFrame, + process, + requestAnimationFrame: nativeRequestAnimationFrame, + } as unknown as NodeJS.Global & Window; + const fakeTimers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + fakeTimers.useRealTimers(); + + // Ensure that the real timers are installed at this point + // (because if they aren't, this test might pass when it shouldn't) + expect(global.cancelAnimationFrame).toBe(nativeCancelAnimationFrame); + expect(global.requestAnimationFrame).toBe(nativeRequestAnimationFrame); + + fakeTimers.useFakeTimers(); + + expect(global.cancelAnimationFrame).not.toBe(nativeCancelAnimationFrame); + expect(global.requestAnimationFrame).not.toBe( + nativeRequestAnimationFrame, + ); + }); }); describe('getTimerCount', () => { @@ -1336,5 +1560,27 @@ describe('FakeTimers', () => { expect(timers.getTimerCount()).toEqual(0); }); + + it('includes animation frames', () => { + const global = { + cancelAnimationFrame: () => {}, + process, + requestAnimationFrame: () => {}, + } as unknown as NodeJS.Global & Window; + const timers = new FakeTimers({ + config, + global, + moduleMocker, + timerConfig, + }); + + timers.useFakeTimers(); + + global.requestAnimationFrame(() => {}); + expect(timers.getTimerCount()).toEqual(1); + timers.clearAllTimers(); + + expect(timers.getTimerCount()).toEqual(0); + }); }); }); diff --git a/packages/jest-fake-timers/src/legacyFakeTimers.ts b/packages/jest-fake-timers/src/legacyFakeTimers.ts index 55d9121ed405..8c0e894f1fe4 100644 --- a/packages/jest-fake-timers/src/legacyFakeTimers.ts +++ b/packages/jest-fake-timers/src/legacyFakeTimers.ts @@ -29,11 +29,13 @@ type Timer = { }; type TimerAPI = { + cancelAnimationFrame: typeof window.cancelAnimationFrame | undefined; clearImmediate: typeof global.clearImmediate; clearInterval: typeof global.clearInterval; clearTimeout: typeof global.clearTimeout; nextTick: typeof process.nextTick; + requestAnimationFrame: typeof window.requestAnimationFrame | undefined; setImmediate: typeof global.setImmediate; setInterval: typeof global.setInterval; setTimeout: typeof global.setTimeout; @@ -46,12 +48,17 @@ type TimerConfig = { const MS_IN_A_YEAR = 31536000000; +interface FakeTimersGlobal extends NodeJS.Global { + cancelAnimationFrame?: typeof window.cancelAnimationFrame; + requestAnimationFrame?: typeof window.requestAnimationFrame; +} + export default class FakeTimers { private _cancelledTicks!: Record; private _config: StackTraceConfig; private _disposed?: boolean; private _fakeTimerAPIs!: TimerAPI; - private _global: NodeJS.Global; + private _global: FakeTimersGlobal; private _immediates!: Array; private _maxLoops: number; private _moduleMocker: ModuleMocker; @@ -69,7 +76,7 @@ export default class FakeTimers { config, maxLoops, }: { - global: NodeJS.Global; + global: FakeTimersGlobal; moduleMocker: ModuleMocker; timerConfig: TimerConfig; config: StackTraceConfig; @@ -84,10 +91,12 @@ export default class FakeTimers { // Store original timer APIs for future reference this._timerAPIs = { + cancelAnimationFrame: global.cancelAnimationFrame, clearImmediate: global.clearImmediate, clearInterval: global.clearInterval, clearTimeout: global.clearTimeout, nextTick: global.process && global.process.nextTick, + requestAnimationFrame: global.requestAnimationFrame, setImmediate: global.setImmediate, setInterval: global.setInterval, setTimeout: global.setTimeout, @@ -317,9 +326,24 @@ export default class FakeTimers { useRealTimers(): void { const global = this._global; + + if (typeof global.cancelAnimationFrame === 'function') { + setGlobal( + global, + 'cancelAnimationFrame', + this._timerAPIs.cancelAnimationFrame, + ); + } setGlobal(global, 'clearImmediate', this._timerAPIs.clearImmediate); setGlobal(global, 'clearInterval', this._timerAPIs.clearInterval); setGlobal(global, 'clearTimeout', this._timerAPIs.clearTimeout); + if (typeof global.requestAnimationFrame === 'function') { + setGlobal( + global, + 'requestAnimationFrame', + this._timerAPIs.requestAnimationFrame, + ); + } setGlobal(global, 'setImmediate', this._timerAPIs.setImmediate); setGlobal(global, 'setInterval', this._timerAPIs.setInterval); setGlobal(global, 'setTimeout', this._timerAPIs.setTimeout); @@ -331,9 +355,23 @@ export default class FakeTimers { this._createMocks(); const global = this._global; + if (typeof global.cancelAnimationFrame === 'function') { + setGlobal( + global, + 'cancelAnimationFrame', + this._fakeTimerAPIs.cancelAnimationFrame, + ); + } setGlobal(global, 'clearImmediate', this._fakeTimerAPIs.clearImmediate); setGlobal(global, 'clearInterval', this._fakeTimerAPIs.clearInterval); setGlobal(global, 'clearTimeout', this._fakeTimerAPIs.clearTimeout); + if (typeof global.requestAnimationFrame === 'function') { + setGlobal( + global, + 'requestAnimationFrame', + this._fakeTimerAPIs.requestAnimationFrame, + ); + } setGlobal(global, 'setImmediate', this._fakeTimerAPIs.setImmediate); setGlobal(global, 'setInterval', this._fakeTimerAPIs.setInterval); setGlobal(global, 'setTimeout', this._fakeTimerAPIs.setTimeout); @@ -380,11 +418,14 @@ export default class FakeTimers { // TODO: add better typings; these are mocks, but typed as regular timers this._fakeTimerAPIs = { + cancelAnimationFrame: fn(this._fakeClearTimer.bind(this)), clearImmediate: fn(this._fakeClearImmediate.bind(this)), clearInterval: fn(this._fakeClearTimer.bind(this)), clearTimeout: fn(this._fakeClearTimer.bind(this)), nextTick: fn(this._fakeNextTick.bind(this)), // @ts-expect-error TODO: figure out better typings here + requestAnimationFrame: fn(this._fakeRequestAnimationFrame.bind(this)), + // @ts-expect-error TODO: figure out better typings here setImmediate: fn(this._fakeSetImmediate.bind(this)), // @ts-expect-error TODO: figure out better typings here setInterval: fn(this._fakeSetInterval.bind(this)), @@ -429,6 +470,10 @@ export default class FakeTimers { }); } + private _fakeRequestAnimationFrame(callback: Callback) { + return this._fakeSetTimeout(callback, 1000 / 60); + } + private _fakeSetImmediate(callback: Callback, ...args: Array) { if (this._disposed) { return null; From 617e3400df6c3052e532a5af17f3d8b5482053d5 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 14 Jun 2021 11:09:42 +0200 Subject: [PATCH 2/5] Add integration test --- .../requestAnimationFrame/__tests__/test.js | 26 +++++++++++++++++++ .../legacy/requestAnimationFrame/package.json | 5 ++++ .../requestAnimationFrame/__tests__/test.js | 26 +++++++++++++++++++ .../modern/requestAnimationFrame/package.json | 5 ++++ 4 files changed, 62 insertions(+) create mode 100644 e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js create mode 100644 e2e/fake-time/legacy/requestAnimationFrame/package.json create mode 100644 e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js create mode 100644 e2e/fake-time/modern/requestAnimationFrame/package.json diff --git a/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js b/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js new file mode 100644 index 000000000000..fa5021409d60 --- /dev/null +++ b/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* global requestAnimationFrame */ + +'use strict'; + +test('requestAnimationFrame', () => { + jest.useFakeTimers('legacy'); + let exited = false; + requestAnimationFrame(() => { + exited = true; + }); + + jest.advanceTimersByTime(15); + + expect(exited).toBe(false); + + jest.advanceTimersByTime(1); + + expect(exited).toBe(true); +}); diff --git a/e2e/fake-time/legacy/requestAnimationFrame/package.json b/e2e/fake-time/legacy/requestAnimationFrame/package.json new file mode 100644 index 000000000000..0ded940b7cb7 --- /dev/null +++ b/e2e/fake-time/legacy/requestAnimationFrame/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "jsdom" + } +} diff --git a/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js b/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js new file mode 100644 index 000000000000..fa5021409d60 --- /dev/null +++ b/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js @@ -0,0 +1,26 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/* global requestAnimationFrame */ + +'use strict'; + +test('requestAnimationFrame', () => { + jest.useFakeTimers('legacy'); + let exited = false; + requestAnimationFrame(() => { + exited = true; + }); + + jest.advanceTimersByTime(15); + + expect(exited).toBe(false); + + jest.advanceTimersByTime(1); + + expect(exited).toBe(true); +}); diff --git a/e2e/fake-time/modern/requestAnimationFrame/package.json b/e2e/fake-time/modern/requestAnimationFrame/package.json new file mode 100644 index 000000000000..0ded940b7cb7 --- /dev/null +++ b/e2e/fake-time/modern/requestAnimationFrame/package.json @@ -0,0 +1,5 @@ +{ + "jest": { + "testEnvironment": "jsdom" + } +} From 26dc18c62d5844effff3c3dee8562a5cf1fbd3b3 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 14 Jun 2021 11:10:29 +0200 Subject: [PATCH 3/5] docs: Link PR --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bbaef8f1cc9..8eb8e757c644 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ### Features -- `[jest-fake-timers]` Flush callbacks scheduled with `requestAnimationFrame` every 16ms when using legacy timers. +- `[jest-fake-timers]` Flush callbacks scheduled with `requestAnimationFrame` every 16ms when using legacy timers. ([#11523](https://github.com/facebook/jest/pull/11567)) ### Fixes From aa748d14a1b041c363919f1d9686763ef44bab99 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 14 Jun 2021 11:55:19 +0200 Subject: [PATCH 4/5] Call with timestamp --- .../requestAnimationFrame/__tests__/test.js | 10 +++---- .../requestAnimationFrame/__tests__/test.js | 12 ++++----- .../src/__tests__/legacyFakeTimers.test.ts | 27 ++++++++++++------- .../jest-fake-timers/src/legacyFakeTimers.ts | 5 +++- 4 files changed, 32 insertions(+), 22 deletions(-) diff --git a/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js b/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js index fa5021409d60..42c638bbf191 100644 --- a/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js +++ b/e2e/fake-time/legacy/requestAnimationFrame/__tests__/test.js @@ -11,16 +11,16 @@ test('requestAnimationFrame', () => { jest.useFakeTimers('legacy'); - let exited = false; - requestAnimationFrame(() => { - exited = true; + let frameTimestamp = -1; + requestAnimationFrame(timestamp => { + frameTimestamp = timestamp; }); jest.advanceTimersByTime(15); - expect(exited).toBe(false); + expect(frameTimestamp).toBe(-1); jest.advanceTimersByTime(1); - expect(exited).toBe(true); + expect(frameTimestamp).toBeGreaterThan(15); }); diff --git a/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js b/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js index fa5021409d60..17e29447c67a 100644 --- a/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js +++ b/e2e/fake-time/modern/requestAnimationFrame/__tests__/test.js @@ -10,17 +10,17 @@ 'use strict'; test('requestAnimationFrame', () => { - jest.useFakeTimers('legacy'); - let exited = false; - requestAnimationFrame(() => { - exited = true; + jest.useFakeTimers('modern'); + let frameTimestamp = -1; + requestAnimationFrame(timestamp => { + frameTimestamp = timestamp; }); jest.advanceTimersByTime(15); - expect(exited).toBe(false); + expect(frameTimestamp).toBe(-1); jest.advanceTimersByTime(1); - expect(exited).toBe(true); + expect(frameTimestamp).toBeGreaterThan(15); }); diff --git a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts index bb8fe71adf35..d6da7dd1336b 100644 --- a/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts +++ b/packages/jest-fake-timers/src/__tests__/legacyFakeTimers.test.ts @@ -667,12 +667,14 @@ describe('FakeTimers', () => { }); timers.useFakeTimers(); - const runOrder: Array = []; + const runOrder: Array = []; const mock1 = jest.fn(() => runOrder.push('mock1')); const mock2 = jest.fn(() => runOrder.push('mock2')); const mock3 = jest.fn(() => runOrder.push('mock3')); const mock4 = jest.fn(() => runOrder.push('mock4')); - const mockAnimationFrame = jest.fn(() => runOrder.push('animationFrame')); + const mockAnimationFrame = jest.fn(timestamp => + runOrder.push(['animationFrame', timestamp]), + ); global.setTimeout(mock1, 100); global.setTimeout(mock2, 0); @@ -686,24 +688,29 @@ describe('FakeTimers', () => { timers.advanceTimersByTime(15); expect(runOrder).toEqual(['mock2', 'mock3']); - // Move forward to t=17 - timers.advanceTimersByTime(2); - expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame']); + // Move forward to t=16 + timers.advanceTimersByTime(1); + expect(runOrder).toEqual(['mock2', 'mock3', ['animationFrame', 16]]); // Move forward to t=60 - timers.advanceTimersByTime(43); - expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame']); + timers.advanceTimersByTime(44); + expect(runOrder).toEqual(['mock2', 'mock3', ['animationFrame', 16]]); // Move forward to t=100 timers.advanceTimersByTime(40); - expect(runOrder).toEqual(['mock2', 'mock3', 'animationFrame', 'mock1']); + expect(runOrder).toEqual([ + 'mock2', + 'mock3', + ['animationFrame', 16], + 'mock1', + ]); // Move forward to t=200 timers.advanceTimersByTime(100); expect(runOrder).toEqual([ 'mock2', 'mock3', - 'animationFrame', + ['animationFrame', 16], 'mock1', 'mock4', ]); @@ -713,7 +720,7 @@ describe('FakeTimers', () => { expect(runOrder).toEqual([ 'mock2', 'mock3', - 'animationFrame', + ['animationFrame', 16], 'mock1', 'mock4', 'mock4', diff --git a/packages/jest-fake-timers/src/legacyFakeTimers.ts b/packages/jest-fake-timers/src/legacyFakeTimers.ts index 8c0e894f1fe4..1029a376472b 100644 --- a/packages/jest-fake-timers/src/legacyFakeTimers.ts +++ b/packages/jest-fake-timers/src/legacyFakeTimers.ts @@ -471,7 +471,10 @@ export default class FakeTimers { } private _fakeRequestAnimationFrame(callback: Callback) { - return this._fakeSetTimeout(callback, 1000 / 60); + return this._fakeSetTimeout(() => { + // TODO: Use performance.now() once it's mocked + callback(this._now); + }, 1000 / 60); } private _fakeSetImmediate(callback: Callback, ...args: Array) { From 806e5fa5c22a9783b4e7f3eb150c3bf4638eaa15 Mon Sep 17 00:00:00 2001 From: eps1lon Date: Mon, 14 Jun 2021 12:18:00 +0200 Subject: [PATCH 5/5] Inline required DOM typings --- packages/jest-fake-timers/src/legacyFakeTimers.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/jest-fake-timers/src/legacyFakeTimers.ts b/packages/jest-fake-timers/src/legacyFakeTimers.ts index 1029a376472b..34486ead5076 100644 --- a/packages/jest-fake-timers/src/legacyFakeTimers.ts +++ b/packages/jest-fake-timers/src/legacyFakeTimers.ts @@ -29,13 +29,13 @@ type Timer = { }; type TimerAPI = { - cancelAnimationFrame: typeof window.cancelAnimationFrame | undefined; + cancelAnimationFrame: FakeTimersGlobal['cancelAnimationFrame']; clearImmediate: typeof global.clearImmediate; clearInterval: typeof global.clearInterval; clearTimeout: typeof global.clearTimeout; nextTick: typeof process.nextTick; - requestAnimationFrame: typeof window.requestAnimationFrame | undefined; + requestAnimationFrame: FakeTimersGlobal['requestAnimationFrame']; setImmediate: typeof global.setImmediate; setInterval: typeof global.setInterval; setTimeout: typeof global.setTimeout; @@ -49,8 +49,8 @@ type TimerConfig = { const MS_IN_A_YEAR = 31536000000; interface FakeTimersGlobal extends NodeJS.Global { - cancelAnimationFrame?: typeof window.cancelAnimationFrame; - requestAnimationFrame?: typeof window.requestAnimationFrame; + cancelAnimationFrame?: (handle: number) => void; + requestAnimationFrame?: (callback: (time: number) => void) => number; } export default class FakeTimers {