Skip to content
This repository was archived by the owner on Jul 30, 2020. It is now read-only.

Commit 856cec2

Browse files
committed
feat: add the ability to test custom hooks
1 parent 23fd9fb commit 856cec2

13 files changed

+616
-5
lines changed

src/__tests__/helpers.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { getSetImmediate } from '../helpers';
2+
3+
test('if setImmediate is available, use it', () => {
4+
const setImmediateMock = jest.fn();
5+
global.setImmediate = setImmediateMock;
6+
7+
expect(getSetImmediate()).toBe(setImmediateMock);
8+
});
9+
10+
test('if setImmediate is not available, use setTimeout', () => {
11+
const setImmediateMock = {};
12+
const setTimeoutMock = jest.fn();
13+
global.setImmediate = setImmediateMock;
14+
global.setTimeout = setTimeoutMock;
15+
16+
const setImmediate = getSetImmediate();
17+
18+
expect(setImmediate).not.toBe(setImmediateMock);
19+
setImmediate();
20+
expect(setTimeoutMock).toHaveBeenCalledTimes(1);
21+
});

src/__tests__/hooks/async-hook.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { useState, useEffect } from 'react';
2+
3+
import { act, renderHook } from '../../';
4+
5+
const getSomeName = () => Promise.resolve('Betty');
6+
7+
const useName = prefix => {
8+
const [name, setName] = useState('nobody');
9+
10+
useEffect(() => {
11+
getSomeName().then(theName => {
12+
act(() => {
13+
setName(prefix ? `${prefix} ${theName}` : theName);
14+
});
15+
});
16+
}, [prefix]);
17+
18+
return name;
19+
};
20+
21+
test('should wait for next update', async () => {
22+
const { result, waitForNextUpdate } = renderHook(() => useName());
23+
24+
expect(result.current).toBe('nobody');
25+
26+
await waitForNextUpdate();
27+
28+
expect(result.current).toBe('Betty');
29+
});
30+
31+
test('should wait for multiple updates', async () => {
32+
const { result, waitForNextUpdate, rerender } = renderHook(({ prefix }) => useName(prefix), {
33+
initialProps: { prefix: 'Mrs.' },
34+
});
35+
36+
expect(result.current).toBe('nobody');
37+
38+
await waitForNextUpdate();
39+
40+
expect(result.current).toBe('Mrs. Betty');
41+
42+
rerender({ prefix: 'Ms.' });
43+
44+
await waitForNextUpdate();
45+
46+
expect(result.current).toBe('Ms. Betty');
47+
});
48+
49+
test('should resolve all when updating', async () => {
50+
const { result, waitForNextUpdate } = renderHook(({ prefix }) => useName(prefix), {
51+
initialProps: { prefix: 'Mrs.' },
52+
});
53+
54+
expect(result.current).toBe('nobody');
55+
56+
await Promise.all([waitForNextUpdate(), waitForNextUpdate(), waitForNextUpdate()]);
57+
58+
expect(result.current).toBe('Mrs. Betty');
59+
});

src/__tests__/hooks/custom-hook.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useState, useCallback } from 'react';
2+
3+
import { renderHook, act } from '../../';
4+
5+
function useCounter(initialCount = 0) {
6+
const [count, setCount] = useState(initialCount);
7+
8+
const incrementBy = useCallback(n => setCount(count + n), [count]);
9+
const decrementBy = useCallback(n => setCount(count - n), [count]);
10+
11+
return { count, incrementBy, decrementBy };
12+
}
13+
14+
test('should create counter', () => {
15+
const { result } = renderHook(() => useCounter());
16+
17+
expect(result.current.count).toBe(0);
18+
});
19+
20+
test('should increment counter', () => {
21+
const { result } = renderHook(() => useCounter());
22+
23+
act(() => result.current.incrementBy(1));
24+
25+
expect(result.current.count).toBe(1);
26+
27+
act(() => result.current.incrementBy(2));
28+
29+
expect(result.current.count).toBe(3);
30+
});
31+
32+
test('should decrement counter', () => {
33+
const { result } = renderHook(() => useCounter());
34+
35+
act(() => result.current.decrementBy(1));
36+
37+
expect(result.current.count).toBe(-1);
38+
39+
act(() => result.current.decrementBy(2));
40+
41+
expect(result.current.count).toBe(-3);
42+
});

src/__tests__/hooks/error-hook.js

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useState, useEffect } from 'react';
2+
3+
import { act, renderHook } from '../../';
4+
5+
function useError(throwError) {
6+
if (throwError) {
7+
throw new Error('expected');
8+
}
9+
return true;
10+
}
11+
12+
const somePromise = () => Promise.resolve();
13+
14+
function useAsyncError(throwError) {
15+
const [value, setValue] = useState();
16+
useEffect(() => {
17+
somePromise().then(() => {
18+
act(() => {
19+
setValue(throwError);
20+
});
21+
});
22+
}, [throwError]);
23+
return useError(value);
24+
}
25+
26+
test('should raise error', () => {
27+
const { result } = renderHook(() => useError(true));
28+
29+
expect(() => {
30+
expect(result.current).not.toBe(undefined);
31+
}).toThrow(Error('expected'));
32+
});
33+
34+
test('should capture error', () => {
35+
const { result } = renderHook(() => useError(true));
36+
37+
expect(result.error).toEqual(Error('expected'));
38+
});
39+
40+
test('should not capture error', () => {
41+
const { result } = renderHook(() => useError(false));
42+
43+
expect(result.current).not.toBe(undefined);
44+
expect(result.error).toBe(undefined);
45+
});
46+
47+
test('should reset error', () => {
48+
const { result, rerender } = renderHook(throwError => useError(throwError), {
49+
initialProps: true,
50+
});
51+
52+
expect(result.error).not.toBe(undefined);
53+
54+
rerender(false);
55+
56+
expect(result.current).not.toBe(undefined);
57+
expect(result.error).toBe(undefined);
58+
});
59+
60+
test('should raise async error', async () => {
61+
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true));
62+
63+
await waitForNextUpdate();
64+
65+
expect(() => {
66+
expect(result.current).not.toBe(undefined);
67+
}).toThrow(Error('expected'));
68+
});
69+
70+
test('should capture async error', async () => {
71+
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(true));
72+
73+
await waitForNextUpdate();
74+
75+
expect(result.error).toEqual(Error('expected'));
76+
});
77+
78+
test('should not capture async error', async () => {
79+
const { result, waitForNextUpdate } = renderHook(() => useAsyncError(false));
80+
81+
await waitForNextUpdate();
82+
83+
expect(result.current).not.toBe(undefined);
84+
expect(result.error).toBe(undefined);
85+
});
86+
87+
test('should reset async error', async () => {
88+
const { result, waitForNextUpdate, rerender } = renderHook(
89+
throwError => useAsyncError(throwError),
90+
{
91+
initialProps: true,
92+
},
93+
);
94+
95+
await waitForNextUpdate();
96+
97+
expect(result.error).not.toBe(undefined);
98+
99+
rerender(false);
100+
101+
await waitForNextUpdate();
102+
103+
expect(result.current).not.toBe(undefined);
104+
expect(result.error).toBe(undefined);
105+
});

src/__tests__/hooks/test-hook.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import React, { useState, useEffect } from 'react';
2+
3+
import { act, renderHook } from '../../';
4+
5+
test('renderHook calls the callback', () => {
6+
const spy = jest.fn();
7+
renderHook(spy);
8+
expect(spy).toHaveBeenCalledTimes(1);
9+
});
10+
11+
test('confirm we can safely call a React Hook from within the callback', () => {
12+
renderHook(() => useState());
13+
});
14+
15+
test('returns a function to unmount component', () => {
16+
let isMounted;
17+
const { unmount } = renderHook(() => {
18+
useEffect(() => {
19+
isMounted = true;
20+
return () => {
21+
isMounted = false;
22+
};
23+
});
24+
});
25+
expect(isMounted).toBe(true);
26+
unmount();
27+
expect(isMounted).toBe(false);
28+
});
29+
30+
test('returns a function to rerender component', () => {
31+
let renderCount = 0;
32+
const { rerender } = renderHook(() => {
33+
useEffect(() => {
34+
renderCount++;
35+
});
36+
});
37+
38+
expect(renderCount).toBe(1);
39+
rerender();
40+
expect(renderCount).toBe(2);
41+
});
42+
43+
test('accepts wrapper option to wrap rendered hook with', () => {
44+
const ctxA = React.createContext();
45+
const ctxB = React.createContext();
46+
const useHook = () => {
47+
return React.useContext(ctxA) * React.useContext(ctxB);
48+
};
49+
let actual;
50+
renderHook(
51+
() => {
52+
actual = useHook();
53+
},
54+
{
55+
// eslint-disable-next-line react/display-name
56+
wrapper: props => (
57+
<ctxA.Provider value={3}>
58+
<ctxB.Provider value={4} {...props} />
59+
</ctxA.Provider>
60+
),
61+
},
62+
);
63+
expect(actual).toBe(12);
64+
});
65+
66+
test('returns result ref with latest result from hook execution', () => {
67+
function useCounter({ initialCount = 0, step = 1 } = {}) {
68+
const [count, setCount] = React.useState(initialCount);
69+
const increment = () => setCount(c => c + step);
70+
const decrement = () => setCount(c => c - step);
71+
return { count, increment, decrement };
72+
}
73+
74+
const { result } = renderHook(useCounter);
75+
expect(result.current.count).toBe(0);
76+
act(() => {
77+
result.current.increment();
78+
});
79+
expect(result.current.count).toBe(1);
80+
});

src/__tests__/hooks/use-context.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import React, { createContext, useContext } from 'react';
2+
3+
import { renderHook } from '../../';
4+
5+
test('should get default value from context', () => {
6+
const TestContext = createContext('foo');
7+
8+
const { result } = renderHook(() => useContext(TestContext));
9+
10+
const value = result.current;
11+
12+
expect(value).toBe('foo');
13+
});
14+
15+
test('should get value from context provider', () => {
16+
const TestContext = createContext('foo');
17+
18+
const wrapper = ({ children }) => (
19+
<TestContext.Provider value="bar">{children}</TestContext.Provider>
20+
);
21+
22+
const { result } = renderHook(() => useContext(TestContext), { wrapper });
23+
24+
expect(result.current).toBe('bar');
25+
});
26+
27+
test('should update value in context', () => {
28+
const TestContext = createContext('foo');
29+
30+
const value = { current: 'bar' };
31+
32+
const wrapper = ({ children }) => (
33+
<TestContext.Provider value={value.current}>{children}</TestContext.Provider>
34+
);
35+
36+
const { result, rerender } = renderHook(() => useContext(TestContext), { wrapper });
37+
38+
value.current = 'baz';
39+
40+
rerender();
41+
42+
expect(result.current).toBe('baz');
43+
});

0 commit comments

Comments
 (0)