From f45cd52c7067a15c2143fe322dd89e1ecfa631f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Sat, 3 Aug 2019 19:20:31 +0200 Subject: [PATCH 01/10] Remove ts-ignore flags from previous tests --- src/__tests__/useGetSetState.test.ts | 3 +-- src/__tests__/useMap.test.ts | 21 +++++++-------------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/__tests__/useGetSetState.test.ts b/src/__tests__/useGetSetState.test.ts index e9eb6aef..46a27fbf 100644 --- a/src/__tests__/useGetSetState.test.ts +++ b/src/__tests__/useGetSetState.test.ts @@ -100,8 +100,7 @@ it('should log an error if set with a patch different than an object', () => { const [, set] = result.current; expect(mockConsoleError).not.toHaveBeenCalled(); - // @ts-ignore - act(() => set('not an object')); + act(() => set('not an object' as any)); expect(mockConsoleError).toHaveBeenCalledTimes(1); expect(mockConsoleError).toHaveBeenCalledWith('useGetSetState setter patch must be an object.'); diff --git a/src/__tests__/useMap.test.ts b/src/__tests__/useMap.test.ts index 242df55f..cc9d57a8 100644 --- a/src/__tests__/useMap.test.ts +++ b/src/__tests__/useMap.test.ts @@ -1,7 +1,7 @@ import { act, renderHook } from '@testing-library/react-hooks'; import useMap from '../useMap'; -const setUp = (initialMap?: object) => renderHook(() => useMap(initialMap)); +const setUp = (initialMap?: T) => renderHook(() => useMap(initialMap)); it('should init map and utils', () => { const { result } = setUp({ foo: 'bar', a: 1 }); @@ -28,7 +28,6 @@ it('should get corresponding value for existing provided key', () => { let value; act(() => { - // @ts-ignore value = utils.get('a'); }); @@ -36,12 +35,11 @@ it('should get corresponding value for existing provided key', () => { }); it('should get undefined for non-existing provided key', () => { - const { result } = setUp({ foo: 'bar', a: 1 }); + const { result } = setUp<{ foo: string; a: number; nonExisting?: any }>({ foo: 'bar', a: 1 }); const [, utils] = result.current; let value; act(() => { - // @ts-ignore value = utils.get('nonExisting'); }); @@ -49,11 +47,10 @@ it('should get undefined for non-existing provided key', () => { }); it('should set new key-value pair', () => { - const { result } = setUp({ foo: 'bar', a: 1 }); + const { result } = setUp<{ foo: string; a: number; newKey?: number }>({ foo: 'bar', a: 1 }); const [, utils] = result.current; act(() => { - // @ts-ignore utils.set('newKey', 99); }); @@ -65,11 +62,10 @@ it('should override current value if setting existing key', () => { const [, utils] = result.current; act(() => { - // @ts-ignore - utils.set('foo', 99); + utils.set('foo', 'qux'); }); - expect(result.current[0]).toEqual({ foo: 99, a: 1 }); + expect(result.current[0]).toEqual({ foo: 'qux', a: 1 }); }); it('should remove corresponding key-value pair for existing provided key', () => { @@ -77,7 +73,6 @@ it('should remove corresponding key-value pair for existing provided key', () => const [, utils] = result.current; act(() => { - // @ts-ignore utils.remove('foo'); }); @@ -85,11 +80,10 @@ it('should remove corresponding key-value pair for existing provided key', () => }); it('should do nothing if removing non-existing provided key', () => { - const { result } = setUp({ foo: 'bar', a: 1 }); + const { result } = setUp<{ foo: string; a: number; nonExisting?: any }>({ foo: 'bar', a: 1 }); const [, utils] = result.current; act(() => { - // @ts-ignore utils.remove('nonExisting'); }); @@ -97,11 +91,10 @@ it('should do nothing if removing non-existing provided key', () => { }); it('should reset map to initial object provided', () => { - const { result } = setUp({ foo: 'bar', a: 1 }); + const { result } = setUp<{ foo: string; a: number; z?: number }>({ foo: 'bar', a: 1 }); const [, utils] = result.current; act(() => { - // @ts-ignore utils.set('z', 99); }); From 9e4de349af642efb012db49d578dd724c1ebf796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Sun, 4 Aug 2019 19:19:52 +0200 Subject: [PATCH 02/10] Add useUpdate tests --- src/__tests__/useUpdate.test.ts | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/__tests__/useUpdate.test.ts diff --git a/src/__tests__/useUpdate.test.ts b/src/__tests__/useUpdate.test.ts new file mode 100644 index 00000000..e7d35191 --- /dev/null +++ b/src/__tests__/useUpdate.test.ts @@ -0,0 +1,26 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useUpdate from '../useUpdate'; + +it('should init update function', () => { + const { result } = renderHook(() => useUpdate()); + const update = result.current; + + expect(update).toBeInstanceOf(Function); +}); + +it('should forces a re-render every time update function is called', () => { + let renderCount = 0; + const { result } = renderHook(() => { + renderCount++; + return useUpdate(); + }); + const update = result.current; + + expect(renderCount).toBe(1); + + act(() => update()); + expect(renderCount).toBe(2); + + act(() => update()); + expect(renderCount).toBe(3); +}); From 5bc14d1a302773f1f987dec438e46472c54219ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Sun, 4 Aug 2019 23:44:34 +0200 Subject: [PATCH 03/10] Add useRaf tests --- package.json | 1 + src/__tests__/useRaf.test.ts | 160 +++++++++++++++++++++++++++++++++++ yarn.lock | 5 ++ 3 files changed, 166 insertions(+) create mode 100644 src/__tests__/useRaf.test.ts diff --git a/package.json b/package.json index db090602..c3ed037b 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "lint-staged": "9.2.1", "markdown-loader": "5.1.0", "prettier": "1.17.1", + "raf-stub": "^3.0.0", "react": "16.8.6", "react-dom": "16.8.6", "react-spring": "8.0.27", diff --git a/src/__tests__/useRaf.test.ts b/src/__tests__/useRaf.test.ts new file mode 100644 index 00000000..c66ed639 --- /dev/null +++ b/src/__tests__/useRaf.test.ts @@ -0,0 +1,160 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { replaceRaf } from 'raf-stub'; +import useRaf from '../useRaf'; + +/** + * New requestAnimationFrame after being replaced with raf-stub for testing purposes. + */ +interface RequestAnimationFrame { + reset(): void; + step(): void; +} +declare var requestAnimationFrame: RequestAnimationFrame; + +replaceRaf(); +const fixedStart = 1564949709496; +const dateNowSpy = jest.spyOn(Date, 'now'); + +beforeEach(() => { + jest.useFakeTimers(); + requestAnimationFrame.reset(); +}); + +afterEach(() => { + jest.clearAllTimers(); + requestAnimationFrame.reset(); +}); + +it('should init percentage of time elapsed', () => { + const { result } = renderHook(() => useRaf()); + const timeElapsed = result.current; + + expect(timeElapsed).toBe(0); +}); + +it('should return corresponding percentage of time elapsed for default ms', () => { + dateNowSpy.mockImplementationOnce(() => fixedStart); // start time + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 0.25); // 25% + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 0.5); // 50% + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 0.75); // 75% + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12); // 100% + + const { result } = renderHook(() => useRaf()); + expect(result.current).toBe(0); + + act(() => { + jest.runOnlyPendingTimers(); // start after delay + requestAnimationFrame.step(); + }); + expect(result.current).toBe(0.25); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(0.5); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(0.75); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(1); +}); + +it('should return corresponding percentage of time elapsed for custom ms', () => { + const customMs = 2000; + dateNowSpy.mockImplementationOnce(() => fixedStart); // start time + dateNowSpy.mockImplementationOnce(() => fixedStart + customMs * 0.25); // 25% + dateNowSpy.mockImplementationOnce(() => fixedStart + customMs * 0.5); // 50% + dateNowSpy.mockImplementationOnce(() => fixedStart + customMs * 0.75); // 75% + dateNowSpy.mockImplementationOnce(() => fixedStart + customMs); // 100% + + const { result } = renderHook(() => useRaf(customMs)); + expect(result.current).toBe(0); + + act(() => { + jest.runOnlyPendingTimers(); // start after delay + requestAnimationFrame.step(); + }); + expect(result.current).toBe(0.25); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(0.5); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(0.75); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(1); +}); + +it('should return always 1 after corresponding ms reached', () => { + dateNowSpy.mockImplementationOnce(() => fixedStart); // start time + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12); // 100% + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 1.1); // 110% + dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 3); // 300% + + const { result } = renderHook(() => useRaf()); + expect(result.current).toBe(0); + + act(() => { + jest.runOnlyPendingTimers(); // start after delay + requestAnimationFrame.step(); + }); + expect(result.current).toBe(1); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(1); + + act(() => { + requestAnimationFrame.step(); + }); + expect(result.current).toBe(1); +}); + +it('should wait until delay reached to start calculating elapsed percentage', () => { + const { result } = renderHook(() => useRaf(undefined, 500)); + + expect(result.current).toBe(0); + + act(() => { + jest.runTimersToTime(250); // fast-forward only half of custom delay + }); + expect(result.current).toBe(0); + + act(() => { + jest.runTimersToTime(249); // fast-forward 1ms less than custom delay + }); + expect(result.current).toBe(0); + + act(() => { + jest.runTimersToTime(1); // fast-forward exactly to custom delay + }); + expect(result.current).not.toBe(0); +}); + +it('should clear pending timers on unmount', () => { + const stopSpy = jest.spyOn(global, 'cancelAnimationFrame' as any); + const { unmount } = renderHook(() => useRaf()); + + // @ts-ignore getTimerCount is not defined on jest types + expect(jest.getTimerCount()).toBe(1); + expect(stopSpy).not.toHaveBeenCalled(); + + unmount(); + + // @ts-ignore getTimerCount is not defined on jest types + expect(jest.getTimerCount()).toBe(0); + expect(stopSpy).toHaveBeenCalledTimes(1); +}); diff --git a/yarn.lock b/yarn.lock index e867b975..0ea1dc73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11577,6 +11577,11 @@ raf-schd@^4.0.0: resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.1.tgz#e72f29a96de260dead719f34c29e56fdc1c1473e" integrity sha512-/QTXV4+Tf81CmJgTZac47N63ZzKmaVe+1cQX/grCFeLrs4Mcc6oq+KJfbF3tFjeS1NF91lmTvgmwYjk02UTo9A== +raf-stub@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raf-stub/-/raf-stub-3.0.0.tgz#40e53dc3ad3b241311f914bbd41dc11a2c9ee0a9" + integrity sha512-64wjDTI8NAkplC3WYF3DUBXmdx8AZF0ubxiicZi83BKW5hcdvMtbwDe6gpFBngTo6+XIJbfwmUP8lMa85UPK6A== + raf@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/raf/-/raf-3.4.0.tgz#a28876881b4bc2ca9117d4138163ddb80f781575" From 6b78160553458fa6a6fd1132b837e62bd770e299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Mon, 5 Aug 2019 21:46:09 +0200 Subject: [PATCH 04/10] Add useTimeout tests --- src/__tests__/useRaf.test.ts | 6 +-- src/__tests__/useTimeout.test.ts | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/__tests__/useTimeout.test.ts diff --git a/src/__tests__/useRaf.test.ts b/src/__tests__/useRaf.test.ts index c66ed639..dfc6997d 100644 --- a/src/__tests__/useRaf.test.ts +++ b/src/__tests__/useRaf.test.ts @@ -129,17 +129,17 @@ it('should wait until delay reached to start calculating elapsed percentage', () expect(result.current).toBe(0); act(() => { - jest.runTimersToTime(250); // fast-forward only half of custom delay + jest.advanceTimersByTime(250); // fast-forward only half of custom delay }); expect(result.current).toBe(0); act(() => { - jest.runTimersToTime(249); // fast-forward 1ms less than custom delay + jest.advanceTimersByTime(249); // fast-forward 1ms less than custom delay }); expect(result.current).toBe(0); act(() => { - jest.runTimersToTime(1); // fast-forward exactly to custom delay + jest.advanceTimersByTime(1); // fast-forward exactly to custom delay }); expect(result.current).not.toBe(0); }); diff --git a/src/__tests__/useTimeout.test.ts b/src/__tests__/useTimeout.test.ts new file mode 100644 index 00000000..cd315b2b --- /dev/null +++ b/src/__tests__/useTimeout.test.ts @@ -0,0 +1,85 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useTimeout from '../useTimeout'; + +beforeEach(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.clearAllTimers(); +}); + +it('should init ready bool to false', () => { + const { result } = renderHook(() => useTimeout()); + + expect(result.current).toBe(false); +}); + +it('should return ready as true on default timeout reached', () => { + const { result } = renderHook(() => useTimeout()); + + act(() => { + // default timeout is 0 so we just basically start running scheduled timer + jest.advanceTimersByTime(0); + }); + + expect(result.current).toBe(true); +}); + +it('should return ready as true on custom timeout reached', () => { + const { result } = renderHook(() => useTimeout(200)); + + act(() => { + jest.advanceTimersByTime(200); + }); + + expect(result.current).toBe(true); +}); + +it('should always return ready as false on custom timeout not reached', () => { + const { result } = renderHook(() => useTimeout(200)); + + act(() => { + jest.advanceTimersByTime(100); + }); + expect(result.current).toBe(false); + + act(() => { + jest.advanceTimersByTime(90); + }); + expect(result.current).toBe(false); + + act(() => { + jest.advanceTimersByTime(9); + }); + expect(result.current).toBe(false); +}); + +it('should always return ready as true after custom timeout reached', () => { + const { result } = renderHook(() => useTimeout(200)); + + act(() => { + jest.advanceTimersByTime(200); + }); + expect(result.current).toBe(true); + + act(() => { + jest.advanceTimersByTime(20); + }); + expect(result.current).toBe(true); + + act(() => { + jest.advanceTimersByTime(100); + }); + expect(result.current).toBe(true); +}); + +it('should clear pending timer on unmount', () => { + const { unmount } = renderHook(() => useTimeout()); + // @ts-ignore getTimerCount is not defined on jest types + expect(jest.getTimerCount()).toBe(1); + + unmount(); + // @ts-ignore getTimerCount is not defined on jest types + expect(jest.getTimerCount()).toBe(0); +}); From cecd1adcfab96a13bddf594b7626a014673eb735 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Mon, 5 Aug 2019 22:23:11 +0200 Subject: [PATCH 05/10] Update @types/jest package and remove ts-ignore for getTimerCount --- package.json | 2 +- src/__tests__/useRaf.test.ts | 2 -- src/__tests__/useTimeout.test.ts | 2 -- yarn.lock | 8 ++++---- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index c3ed037b..d0cfee6d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "@storybook/addon-options": "5.1.10", "@storybook/react": "5.1.10", "@testing-library/react-hooks": "^1.1.0", - "@types/jest": "24.0.16", + "@types/jest": "24.0.17", "@types/react": "16.8.23", "babel-core": "6.26.3", "babel-loader": "8.0.6", diff --git a/src/__tests__/useRaf.test.ts b/src/__tests__/useRaf.test.ts index dfc6997d..d535f284 100644 --- a/src/__tests__/useRaf.test.ts +++ b/src/__tests__/useRaf.test.ts @@ -148,13 +148,11 @@ it('should clear pending timers on unmount', () => { const stopSpy = jest.spyOn(global, 'cancelAnimationFrame' as any); const { unmount } = renderHook(() => useRaf()); - // @ts-ignore getTimerCount is not defined on jest types expect(jest.getTimerCount()).toBe(1); expect(stopSpy).not.toHaveBeenCalled(); unmount(); - // @ts-ignore getTimerCount is not defined on jest types expect(jest.getTimerCount()).toBe(0); expect(stopSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/__tests__/useTimeout.test.ts b/src/__tests__/useTimeout.test.ts index cd315b2b..dec42fd9 100644 --- a/src/__tests__/useTimeout.test.ts +++ b/src/__tests__/useTimeout.test.ts @@ -76,10 +76,8 @@ it('should always return ready as true after custom timeout reached', () => { it('should clear pending timer on unmount', () => { const { unmount } = renderHook(() => useTimeout()); - // @ts-ignore getTimerCount is not defined on jest types expect(jest.getTimerCount()).toBe(1); unmount(); - // @ts-ignore getTimerCount is not defined on jest types expect(jest.getTimerCount()).toBe(0); }); diff --git a/yarn.lock b/yarn.lock index 0ea1dc73..dae05fde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2728,10 +2728,10 @@ resolved "https://registry.yarnpkg.com/@types/jest-diff/-/jest-diff-20.0.1.tgz#35cc15b9c4f30a18ef21852e255fdb02f6d59b89" integrity sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA== -"@types/jest@24.0.16": - version "24.0.16" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.16.tgz#8d3e406ec0f0dc1688d6711af3062ff9bd428066" - integrity sha512-JrAiyV+PPGKZzw6uxbI761cHZ0G7QMOHXPhtSpcl08rZH6CswXaaejckn3goFKmF7M3nzEoJ0lwYCbqLMmjziQ== +"@types/jest@24.0.17": + version "24.0.17" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.0.17.tgz#b66ea026efb746eb5db1356ee28518aaff7af416" + integrity sha512-1cy3xkOAfSYn78dsBWy4M3h/QF/HeWPchNFDjysVtp3GHeTdSmtluNnELfCmfNRRHo0OWEcpf+NsEJQvwQfdqQ== dependencies: "@types/jest-diff" "*" From 3c43c4b05dc4f0e1c4e4ff97fd1896f85f1374e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Wed, 21 Aug 2019 23:36:50 +0200 Subject: [PATCH 06/10] Add useSpring tests --- src/__stories__/useSpring.story.tsx | 2 +- src/__tests__/useSpring.test.ts | 119 ++++++++++++++++++++++++++++ src/useSpring.ts | 22 +++-- 3 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 src/__tests__/useSpring.test.ts diff --git a/src/__stories__/useSpring.story.tsx b/src/__stories__/useSpring.story.tsx index cc7a2d9a..0727e5d7 100644 --- a/src/__stories__/useSpring.story.tsx +++ b/src/__stories__/useSpring.story.tsx @@ -1,6 +1,6 @@ import { storiesOf } from '@storybook/react'; import * as React from 'react'; -import { useSpring } from '..'; +import useSpring from '../useSpring'; import ShowDocs from './util/ShowDocs'; const Demo = () => { diff --git a/src/__tests__/useSpring.test.ts b/src/__tests__/useSpring.test.ts new file mode 100644 index 00000000..a6a1c926 --- /dev/null +++ b/src/__tests__/useSpring.test.ts @@ -0,0 +1,119 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import useSpring from '../useSpring'; +import { Spring } from 'rebound'; + +// simulate Spring for testing +const mockSetCurrentValue = jest.fn(); +const mockAddListener = jest.fn(); +const mockSetEndValue = jest.fn(); +const mockRemoveListener = jest.fn(); +let triggerSpringUpdate = () => {}; +let springListener: Listener = { onSpringUpdate: () => {} }; + +interface Listener { + onSpringUpdate: (currentSpring: Spring) => void; +} + +const mockCreateSpring: Spring = jest.fn().mockImplementation(() => { + let currentValue = 0; + let endValue = 0; + + const getCloserValue = (a, b) => Math.round((a + b) / 2); + + const getCurrentValue = () => { + currentValue = getCloserValue(currentValue, endValue); + return currentValue; + }; + + triggerSpringUpdate = () => { + if (currentValue !== endValue) { + springListener.onSpringUpdate({ getCurrentValue } as any); + } + }; + + return { + setCurrentValue: val => { + currentValue = val; + mockSetCurrentValue(val); + }, + addListener: newListener => { + springListener = newListener; + mockAddListener(newListener); + }, + setEndValue: val => { + endValue = val; + mockSetEndValue(val); + }, + removeListener: mockRemoveListener, + }; +}) as any; + +jest.mock('rebound', () => { + return { + Sprint: {}, + SpringSystem: jest.fn().mockImplementation(() => { + return { createSpring: mockCreateSpring }; + }), + }; +}); + +it('should init value to provided target', () => { + const { result } = renderHook(() => useSpring(70)); + + expect(result.current).toBe(70); + expect(mockSetCurrentValue).toHaveBeenCalledTimes(1); + expect(mockSetCurrentValue).toHaveBeenCalledWith(70); + expect(mockCreateSpring).toHaveBeenCalledTimes(1); + expect(mockCreateSpring).toHaveBeenCalledWith(50, 3); +}); + +it('should create spring with custom tension and friction args provided', () => { + renderHook(() => useSpring(500, 20, 7)); + + expect(mockCreateSpring).toHaveBeenCalledTimes(1); + expect(mockCreateSpring).toHaveBeenCalledWith(20, 7); +}); + +it('should subscribe only once', () => { + const { rerender } = renderHook(() => useSpring()); + + expect(mockAddListener).toHaveBeenCalledTimes(1); + expect(mockAddListener).toHaveBeenCalledWith(springListener); + + rerender(); + + expect(mockAddListener).toHaveBeenCalledTimes(1); +}); + +it('should handle spring update', () => { + let targetValue = 70; + let lastSpringValue = targetValue; + const { result, rerender } = renderHook(() => useSpring(targetValue)); + + targetValue = 100; + rerender(); + expect(result.current).toBe(lastSpringValue); + + act(() => { + triggerSpringUpdate(); // simulate new spring value + }); + expect(result.current).toBeGreaterThan(lastSpringValue); + expect(result.current).toBeLessThanOrEqual(targetValue); + + lastSpringValue = result.current; + act(() => { + triggerSpringUpdate(); // simulate another new spring value + }); + expect(result.current).toBeGreaterThan(lastSpringValue); + expect(result.current).toBeLessThanOrEqual(targetValue); +}); + +it('should remove listener on unmount', () => { + const { unmount } = renderHook(() => useSpring()); + expect(mockRemoveListener).not.toHaveBeenCalled(); + + unmount(); + + expect(mockRemoveListener).toHaveBeenCalledTimes(1); + expect(mockRemoveListener).toHaveBeenCalledWith(springListener); +}); diff --git a/src/useSpring.ts b/src/useSpring.ts index 3567f2ee..7f4dee2a 100644 --- a/src/useSpring.ts +++ b/src/useSpring.ts @@ -1,31 +1,37 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useMemo } from 'react'; import { Spring, SpringSystem } from 'rebound'; const useSpring = (targetValue: number = 0, tension: number = 50, friction: number = 3) => { const [spring, setSpring] = useState(null); const [value, setValue] = useState(targetValue); - useEffect(() => { - const listener = { + // memoize listener to being able to unsubscribe later properly, otherwise + // listener fn will be different on each re-render and wouldn't unsubscribe properly. + const listener = useMemo( + () => ({ onSpringUpdate: currentSpring => { const newValue = currentSpring.getCurrentValue(); setValue(newValue); }, - }; + }), + [] + ); + useEffect(() => { if (!spring) { const newSpring = new SpringSystem().createSpring(tension, friction); newSpring.setCurrentValue(targetValue); setSpring(newSpring); newSpring.addListener(listener); - return; } return () => { - spring.removeListener(listener); - setSpring(null); + if (spring) { + spring.removeListener(listener); + setSpring(null); + } }; - }, [tension, friction]); + }, [tension, friction, spring]); useEffect(() => { if (spring) { From 08f524f346b091af980e49d8b889ab76a8f449f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Thu, 22 Aug 2019 01:00:45 +0200 Subject: [PATCH 07/10] Fix useRaf tests After update to react v16.9 and react-testing-library v9 with improved async stuff for testing, timers work slightly different and need to be set inside act. --- src/__tests__/useRaf.test.ts | 39 ++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/__tests__/useRaf.test.ts b/src/__tests__/useRaf.test.ts index d535f284..3d1936e3 100644 --- a/src/__tests__/useRaf.test.ts +++ b/src/__tests__/useRaf.test.ts @@ -13,7 +13,7 @@ declare var requestAnimationFrame: RequestAnimationFrame; replaceRaf(); const fixedStart = 1564949709496; -const dateNowSpy = jest.spyOn(Date, 'now'); +const spyDateNow = jest.spyOn(Date, 'now').mockImplementation(() => fixedStart); beforeEach(() => { jest.useFakeTimers(); @@ -33,32 +33,30 @@ it('should init percentage of time elapsed', () => { }); it('should return corresponding percentage of time elapsed for default ms', () => { - dateNowSpy.mockImplementationOnce(() => fixedStart); // start time - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 0.25); // 25% - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 0.5); // 50% - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 0.75); // 75% - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12); // 100% - const { result } = renderHook(() => useRaf()); expect(result.current).toBe(0); act(() => { jest.runOnlyPendingTimers(); // start after delay + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.25); // 25% requestAnimationFrame.step(); }); expect(result.current).toBe(0.25); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.5); // 50% requestAnimationFrame.step(); }); expect(result.current).toBe(0.5); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 0.75); // 75% requestAnimationFrame.step(); }); expect(result.current).toBe(0.75); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12); // 100% requestAnimationFrame.step(); }); expect(result.current).toBe(1); @@ -66,58 +64,55 @@ it('should return corresponding percentage of time elapsed for default ms', () = it('should return corresponding percentage of time elapsed for custom ms', () => { const customMs = 2000; - dateNowSpy.mockImplementationOnce(() => fixedStart); // start time - dateNowSpy.mockImplementationOnce(() => fixedStart + customMs * 0.25); // 25% - dateNowSpy.mockImplementationOnce(() => fixedStart + customMs * 0.5); // 50% - dateNowSpy.mockImplementationOnce(() => fixedStart + customMs * 0.75); // 75% - dateNowSpy.mockImplementationOnce(() => fixedStart + customMs); // 100% const { result } = renderHook(() => useRaf(customMs)); expect(result.current).toBe(0); act(() => { jest.runOnlyPendingTimers(); // start after delay + spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.25); // 25% requestAnimationFrame.step(); }); expect(result.current).toBe(0.25); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.5); // 50% requestAnimationFrame.step(); }); expect(result.current).toBe(0.5); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + customMs * 0.75); // 75% requestAnimationFrame.step(); }); expect(result.current).toBe(0.75); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + customMs); // 100% requestAnimationFrame.step(); }); expect(result.current).toBe(1); }); it('should return always 1 after corresponding ms reached', () => { - dateNowSpy.mockImplementationOnce(() => fixedStart); // start time - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12); // 100% - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 1.1); // 110% - dateNowSpy.mockImplementationOnce(() => fixedStart + 1e12 * 3); // 300% - const { result } = renderHook(() => useRaf()); expect(result.current).toBe(0); act(() => { jest.runOnlyPendingTimers(); // start after delay + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12); // 100% requestAnimationFrame.step(); }); expect(result.current).toBe(1); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 1.1); // 110% requestAnimationFrame.step(); }); expect(result.current).toBe(1); act(() => { + spyDateNow.mockImplementationOnce(() => fixedStart + 1e12 * 3); // 300% requestAnimationFrame.step(); }); expect(result.current).toBe(1); @@ -145,14 +140,14 @@ it('should wait until delay reached to start calculating elapsed percentage', () }); it('should clear pending timers on unmount', () => { - const stopSpy = jest.spyOn(global, 'cancelAnimationFrame' as any); + const spyRafStop = jest.spyOn(global, 'cancelAnimationFrame' as any); const { unmount } = renderHook(() => useRaf()); - expect(jest.getTimerCount()).toBe(1); - expect(stopSpy).not.toHaveBeenCalled(); + expect(clearTimeout).not.toHaveBeenCalled(); + expect(spyRafStop).not.toHaveBeenCalled(); unmount(); - expect(jest.getTimerCount()).toBe(0); - expect(stopSpy).toHaveBeenCalledTimes(1); + expect(clearTimeout).toHaveBeenCalledTimes(2); + expect(spyRafStop).toHaveBeenCalledTimes(1); }); From a510fedead7da0209a0ed0f0fda975fa0d58739e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Thu, 22 Aug 2019 01:03:02 +0200 Subject: [PATCH 08/10] Fix tslint errors --- src/useBattery.ts | 2 +- src/useLocation.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/useBattery.ts b/src/useBattery.ts index 4837ff9c..8be532a4 100644 --- a/src/useBattery.ts +++ b/src/useBattery.ts @@ -82,4 +82,4 @@ function useBattery(): UseBatteryState { return state; } -export default (isBatteryApiSupported ? useBattery : useBatteryMock); +export default isBatteryApiSupported ? useBattery : useBatteryMock; diff --git a/src/useLocation.ts b/src/useLocation.ts index 4931fd11..4ab872df 100644 --- a/src/useLocation.ts +++ b/src/useLocation.ts @@ -84,4 +84,4 @@ const useLocationBrowser = (): LocationSensorState => { return state; }; -export default (isClient ? useLocationBrowser : useLocationServer); +export default isClient ? useLocationBrowser : useLocationServer; From 115fd7395780d97a25f47b863afe86674b0fafaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Thu, 22 Aug 2019 22:12:03 +0200 Subject: [PATCH 09/10] Get updated useTimeout tests and remove unnecessary outer describe block --- src/__tests__/useTimeout.test.ts | 264 +++++++++++++++---------------- 1 file changed, 131 insertions(+), 133 deletions(-) diff --git a/src/__tests__/useTimeout.test.ts b/src/__tests__/useTimeout.test.ts index 905b4022..5a852f81 100644 --- a/src/__tests__/useTimeout.test.ts +++ b/src/__tests__/useTimeout.test.ts @@ -2,137 +2,135 @@ import { act, renderHook, RenderHookResult } from '@testing-library/react-hooks' import { useTimeout } from '../index'; import { UseTimeoutReturn } from '../useTimeout'; -describe('useTimeout', () => { - beforeAll(() => { - jest.useFakeTimers(); - }); - - afterEach(() => { - jest.clearAllTimers(); - }); - - afterAll(() => { - jest.useRealTimers(); - }); - - it('should be defined', () => { - expect(useTimeout).toBeDefined(); - }); - - it('should return three functions', () => { - const hook = renderHook(() => useTimeout(5)); - - expect(hook.result.current.length).toBe(3); - expect(typeof hook.result.current[0]).toBe('function'); - expect(typeof hook.result.current[1]).toBe('function'); - expect(typeof hook.result.current[2]).toBe('function'); - }); - - function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutReturn>] { - const spy = jest.fn(); - return [ - spy, - renderHook( - ({ delay = 5 }) => { - spy(); - return useTimeout(delay); - }, - { initialProps: { delay: ms } } - ), - ]; - } - - it('should re-render component after given amount of time', done => { - const [spy, hook] = getHook(); - expect(spy).toHaveBeenCalledTimes(1); - hook.waitForNextUpdate().then(() => { - expect(spy).toHaveBeenCalledTimes(2); - done(); - }); - jest.advanceTimersByTime(5); - }); - - it('should cancel timeout on unmount', () => { - const [spy, hook] = getHook(); - - expect(spy).toHaveBeenCalledTimes(1); - hook.unmount(); - jest.advanceTimersByTime(5); - expect(spy).toHaveBeenCalledTimes(1); - }); - - it('first function should return actual state of timeout', done => { - let [, hook] = getHook(); - let [isReady] = hook.result.current; - - expect(isReady()).toBe(false); - hook.unmount(); - expect(isReady()).toBe(null); - - [, hook] = getHook(); - [isReady] = hook.result.current; - hook.waitForNextUpdate().then(() => { - expect(isReady()).toBe(true); - - done(); - }); - jest.advanceTimersByTime(5); - }); - - it('second function should cancel timeout', () => { - const [spy, hook] = getHook(); - const [isReady, cancel] = hook.result.current; - - expect(spy).toHaveBeenCalledTimes(1); - expect(isReady()).toBe(false); - - act(() => { - cancel(); - }); - jest.advanceTimersByTime(5); - - expect(spy).toHaveBeenCalledTimes(1); - expect(isReady()).toBe(null); - }); - - it('third function should reset timeout', done => { - const [spy, hook] = getHook(); - const [isReady, cancel, reset] = hook.result.current; - - expect(isReady()).toBe(false); - - act(() => { - cancel(); - }); - jest.advanceTimersByTime(5); - - expect(isReady()).toBe(null); - - act(() => { - reset(); - }); - expect(isReady()).toBe(false); - - hook.waitForNextUpdate().then(() => { - expect(spy).toHaveBeenCalledTimes(2); - expect(isReady()).toBe(true); - - done(); - }); - jest.advanceTimersByTime(5); - }); - - it('should reset timeout on delay change', done => { - const [spy, hook] = getHook(15); - - expect(spy).toHaveBeenCalledTimes(1); - hook.rerender({ delay: 5 }); - - hook.waitForNextUpdate().then(() => { - expect(spy).toHaveBeenCalledTimes(3); - - done(); - }); - jest.advanceTimersByTime(15); - }); +beforeAll(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.clearAllTimers(); +}); + +afterAll(() => { + jest.useRealTimers(); +}); + +it('should be defined', () => { + expect(useTimeout).toBeDefined(); +}); + +it('should return three functions', () => { + const hook = renderHook(() => useTimeout(5)); + + expect(hook.result.current.length).toBe(3); + expect(typeof hook.result.current[0]).toBe('function'); + expect(typeof hook.result.current[1]).toBe('function'); + expect(typeof hook.result.current[2]).toBe('function'); +}); + +function getHook(ms: number = 5): [jest.Mock, RenderHookResult<{ delay: number }, UseTimeoutReturn>] { + const spy = jest.fn(); + return [ + spy, + renderHook( + ({ delay = 5 }) => { + spy(); + return useTimeout(delay); + }, + { initialProps: { delay: ms } } + ), + ]; +} + +it('should re-render component after given amount of time', done => { + const [spy, hook] = getHook(); + expect(spy).toHaveBeenCalledTimes(1); + hook.waitForNextUpdate().then(() => { + expect(spy).toHaveBeenCalledTimes(2); + done(); + }); + jest.advanceTimersByTime(5); +}); + +it('should cancel timeout on unmount', () => { + const [spy, hook] = getHook(); + + expect(spy).toHaveBeenCalledTimes(1); + hook.unmount(); + jest.advanceTimersByTime(5); + expect(spy).toHaveBeenCalledTimes(1); +}); + +it('first function should return actual state of timeout', done => { + let [, hook] = getHook(); + let [isReady] = hook.result.current; + + expect(isReady()).toBe(false); + hook.unmount(); + expect(isReady()).toBe(null); + + [, hook] = getHook(); + [isReady] = hook.result.current; + hook.waitForNextUpdate().then(() => { + expect(isReady()).toBe(true); + + done(); + }); + jest.advanceTimersByTime(5); +}); + +it('second function should cancel timeout', () => { + const [spy, hook] = getHook(); + const [isReady, cancel] = hook.result.current; + + expect(spy).toHaveBeenCalledTimes(1); + expect(isReady()).toBe(false); + + act(() => { + cancel(); + }); + jest.advanceTimersByTime(5); + + expect(spy).toHaveBeenCalledTimes(1); + expect(isReady()).toBe(null); +}); + +it('third function should reset timeout', done => { + const [spy, hook] = getHook(); + const [isReady, cancel, reset] = hook.result.current; + + expect(isReady()).toBe(false); + + act(() => { + cancel(); + }); + jest.advanceTimersByTime(5); + + expect(isReady()).toBe(null); + + act(() => { + reset(); + }); + expect(isReady()).toBe(false); + + hook.waitForNextUpdate().then(() => { + expect(spy).toHaveBeenCalledTimes(2); + expect(isReady()).toBe(true); + + done(); + }); + jest.advanceTimersByTime(5); +}); + +it('should reset timeout on delay change', done => { + const [spy, hook] = getHook(15); + + expect(spy).toHaveBeenCalledTimes(1); + hook.rerender({ delay: 5 }); + + hook.waitForNextUpdate().then(() => { + expect(spy).toHaveBeenCalledTimes(3); + + done(); + }); + jest.advanceTimersByTime(15); }); From 1f28a76fc389cc0e2120e057a5261c394a7faa8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mario=20Beltra=CC=81n=20Alarco=CC=81n?= Date: Thu, 22 Aug 2019 23:23:12 +0200 Subject: [PATCH 10/10] Add useTween tests --- src/__tests__/useTween.test.ts | 56 ++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/__tests__/useTween.test.ts diff --git a/src/__tests__/useTween.test.ts b/src/__tests__/useTween.test.ts new file mode 100644 index 00000000..18a49833 --- /dev/null +++ b/src/__tests__/useTween.test.ts @@ -0,0 +1,56 @@ +import { renderHook } from '@testing-library/react-hooks'; +import useTween from '../useTween'; +import * as useRaf from '../useRaf'; +import { easing } from 'ts-easing'; + +let spyUseRaf; +let spyEasingInCirc; +let spyEasingOutCirc; + +beforeEach(() => { + spyUseRaf = jest.spyOn(useRaf, 'default').mockReturnValue(17); + spyEasingInCirc = jest.spyOn(easing, 'inCirc').mockReturnValue(999999); + spyEasingOutCirc = jest.spyOn(easing, 'outCirc').mockReturnValue(101010); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +it('should init corresponding utils with default values', () => { + const { result } = renderHook(() => useTween()); + + expect(result.current).toBe(999999); + expect(spyEasingInCirc).toHaveBeenCalledTimes(1); + expect(spyEasingInCirc).toHaveBeenCalledWith(17); + expect(spyUseRaf).toHaveBeenCalledTimes(1); + expect(spyUseRaf).toHaveBeenCalledWith(200, 0); +}); + +it('should init corresponding utils with custom values', () => { + const { result } = renderHook(() => useTween('outCirc', 500, 15)); + + expect(result.current).toBe(101010); + expect(spyEasingOutCirc).toHaveBeenCalledTimes(1); + expect(spyEasingOutCirc).toHaveBeenCalledWith(17); + expect(spyUseRaf).toHaveBeenCalledTimes(1); + expect(spyUseRaf).toHaveBeenCalledWith(500, 15); +}); + +describe('when invalid easing name is provided', () => { + beforeEach(() => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'trace').mockImplementation(() => {}); + }); + + it('should log an error', () => { + const { result } = renderHook(() => useTween('grijanderl')); + + expect(result.current).toBe(0); + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + expect.stringContaining('useTween() expected "easingName" property to be a valid easing function name') + ); + expect(console.trace).toHaveBeenCalledTimes(1); + }); +});