Skip to content

Commit

Permalink
fix: correct useSpring() hook behaviour
Browse files Browse the repository at this point in the history
Add tests for "Animation" block.
  • Loading branch information
streamich authored Aug 23, 2019
2 parents c73e92f + 1f28a76 commit d7a622d
Show file tree
Hide file tree
Showing 9 changed files with 477 additions and 127 deletions.
2 changes: 1 addition & 1 deletion src/__stories__/useSpring.story.tsx
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down
3 changes: 1 addition & 2 deletions src/__tests__/useGetSetState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
Expand Down
21 changes: 7 additions & 14 deletions src/__tests__/useMap.test.ts
Original file line number Diff line number Diff line change
@@ -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 = <T extends object>(initialMap?: T) => renderHook(() => useMap(initialMap));

it('should init map and utils', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
Expand All @@ -28,32 +28,29 @@ it('should get corresponding value for existing provided key', () => {

let value;
act(() => {
// @ts-ignore
value = utils.get('a');
});

expect(value).toBe(1);
});

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');
});

expect(value).toBeUndefined();
});

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);
});

Expand All @@ -65,43 +62,39 @@ 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', () => {
const { result } = setUp({ foo: 'bar', a: 1 });
const [, utils] = result.current;

act(() => {
// @ts-ignore
utils.remove('foo');
});

expect(result.current[0]).toEqual({ a: 1 });
});

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');
});

expect(result.current[0]).toEqual({ foo: 'bar', a: 1 });
});

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);
});

Expand Down
153 changes: 153 additions & 0 deletions src/__tests__/useRaf.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
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 spyDateNow = jest.spyOn(Date, 'now').mockImplementation(() => fixedStart);

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', () => {
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);
});

it('should return corresponding percentage of time elapsed for custom ms', () => {
const customMs = 2000;

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', () => {
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);
});

it('should wait until delay reached to start calculating elapsed percentage', () => {
const { result } = renderHook(() => useRaf(undefined, 500));

expect(result.current).toBe(0);

act(() => {
jest.advanceTimersByTime(250); // fast-forward only half of custom delay
});
expect(result.current).toBe(0);

act(() => {
jest.advanceTimersByTime(249); // fast-forward 1ms less than custom delay
});
expect(result.current).toBe(0);

act(() => {
jest.advanceTimersByTime(1); // fast-forward exactly to custom delay
});
expect(result.current).not.toBe(0);
});

it('should clear pending timers on unmount', () => {
const spyRafStop = jest.spyOn(global, 'cancelAnimationFrame' as any);
const { unmount } = renderHook(() => useRaf());

expect(clearTimeout).not.toHaveBeenCalled();
expect(spyRafStop).not.toHaveBeenCalled();

unmount();

expect(clearTimeout).toHaveBeenCalledTimes(2);
expect(spyRafStop).toHaveBeenCalledTimes(1);
});
119 changes: 119 additions & 0 deletions src/__tests__/useSpring.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading

0 comments on commit d7a622d

Please sign in to comment.