diff --git a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
index 3bb53c877..4aafb882a 100644
--- a/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ReactWrapper-spec.jsx
@@ -1047,9 +1047,13 @@ describeWithDOM('mount', () => {
describeHooks(
{ Wrap, Wrapper },
'useCallback',
+ 'useContext',
'useEffect',
'useLayoutEffect',
'useMemo',
+ 'useReducer',
+ 'useState',
+ 'custom',
);
describeIf(is('>= 16.6'), 'Suspense & lazy', () => {
diff --git a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
index 780147f25..0541bb5eb 100644
--- a/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
+++ b/packages/enzyme-test-suite/test/ShallowWrapper-spec.jsx
@@ -1228,9 +1228,13 @@ describe('shallow', () => {
describeHooks(
{ Wrap, Wrapper },
'useCallback',
+ 'useContext',
'useEffect',
'useLayoutEffect',
'useMemo',
+ 'useReducer',
+ 'useState',
+ 'custom',
);
describe('.shallow()', () => {
diff --git a/packages/enzyme-test-suite/test/shared/hooks/_hook.template b/packages/enzyme-test-suite/test/shared/hooks/_hook.template
index 92484f389..dcf04751f 100644
--- a/packages/enzyme-test-suite/test/shared/hooks/_hook.template
+++ b/packages/enzyme-test-suite/test/shared/hooks/_hook.template
@@ -19,6 +19,7 @@ import {
import {
useCallback,
+ useContext,
useEffect,
useLayoutEffect,
useMemo,
diff --git a/packages/enzyme-test-suite/test/shared/hooks/custom.jsx b/packages/enzyme-test-suite/test/shared/hooks/custom.jsx
new file mode 100644
index 000000000..8aad509d3
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/hooks/custom.jsx
@@ -0,0 +1,150 @@
+import React from 'react';
+import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
+
+import {
+ describeIf,
+} from '../../_helpers';
+
+import {
+ useEffect,
+ useState,
+} from '../../_helpers/react-compat';
+
+export default function describeCustomHooks({
+ hasHooks,
+ Wrap,
+ isShallow,
+}) {
+ describeIf(hasHooks, 'hooks: custom', () => {
+ describe('custom hook : useCounter', () => {
+ function useCounter({ initialCount = 0, step = 1 } = {}) {
+ const [count, setCount] = useState(initialCount);
+ const increment = () => setCount(c => c + step);
+ const decrement = () => setCount(c => c - step);
+ return { count, increment, decrement };
+ }
+ // testing custom hooks with renderProps
+ // may be we can think of adding in utils
+ // will be repeated
+ const Counter = ({ children, ...rest }) => children(useCounter(rest));
+
+ function setup(props) {
+ const returnVal = {};
+ Wrap(
+
+ {(val) => {
+ Object.assign(returnVal, val);
+ return null;
+ }}
+ ,
+ );
+ return returnVal;
+ }
+
+ it('useCounter', () => {
+ const counterData = setup();
+ counterData.increment();
+ expect(counterData).to.have.property('count', 1);
+ counterData.decrement();
+ expect(counterData).to.have.property('count', 0);
+ });
+
+ it('useCounter with initialCount', () => {
+ const counterData = setup({ initialCount: 2 });
+ counterData.increment();
+ expect(counterData).to.have.property('count', 3);
+ counterData.decrement();
+ expect(counterData).to.have.property('count', 2);
+ });
+
+ it('useCounter with step', () => {
+ const counterData = setup({ step: 2 });
+ counterData.increment();
+ expect(counterData).to.have.property('count', 2);
+ counterData.decrement();
+ expect(counterData).to.have.property('count', 0);
+ });
+
+ it('useCounter with step and initialCount', () => {
+ const counterData = setup({ step: 2, initialCount: 5 });
+ counterData.increment();
+ expect(counterData).to.have.property('count', 7);
+ counterData.decrement();
+ expect(counterData).to.have.property('count', 5);
+ });
+ });
+
+ // todo: enable shallow when useEffect works in the shallow renderer. see https://github.com/facebook/react/issues/15275
+ describeIf(!isShallow, 'custom hook: formInput invoke props', () => {
+ function useFormInput(initialValue = '') {
+ const [value, setValue] = useState(initialValue);
+
+ return {
+ value,
+ onChange(e) {
+ setValue(e.target.value);
+ },
+ };
+ }
+
+ function Input(props) {
+ return (
+
+
+
+ );
+ }
+
+ function ControlledInputWithEnhancedInput({ searchSomething }) {
+ const search = useFormInput();
+
+ useEffect(
+ () => {
+ searchSomething(search.value);
+ },
+ [search.value],
+ );
+
+ return ;
+ }
+
+ function ControlledInputWithNativeInput({ searchSomething }) {
+ const search = useFormInput();
+
+ useEffect(
+ () => {
+ searchSomething(search.value);
+ },
+ [search.value],
+ );
+
+ return ;
+ }
+
+ it('work with native input', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap();
+ wrapper.find('input').invoke('onChange')({ target: { value: 'foo' } });
+
+ expect(spy.withArgs('foo')).to.have.property('callCount', 1);
+ });
+
+ it('work with custom wrapped Input', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap();
+ const input = wrapper.find('Input');
+ input.invoke('onChange')({ target: { value: 'foo' } });
+ expect(spy.withArgs('foo')).to.have.property('callCount', 1);
+ });
+
+ it('work with custom wrapped input', () => {
+ const spy = sinon.spy();
+ const wrapper = Wrap();
+ const input = wrapper.find('input');
+ input.invoke('onChange')({ target: { value: 'foo' } });
+ expect(spy.withArgs('foo')).to.have.property('callCount', 1);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/hooks/useContext.jsx b/packages/enzyme-test-suite/test/shared/hooks/useContext.jsx
new file mode 100644
index 000000000..1d7f13e50
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/hooks/useContext.jsx
@@ -0,0 +1,122 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+
+import {
+ useContext,
+ useState,
+ createContext,
+} from '../../_helpers/react-compat';
+
+export default function describeUseContext({
+ hasHooks,
+ Wrap,
+ isShallow,
+}) {
+ describeIf(hasHooks, 'hooks: useContext', () => {
+ describe('simple example', () => {
+ const initialTitle = 'initialTitle';
+ const TitleContext = createContext && createContext(initialTitle);
+
+ function UiComponent() {
+ const title = useContext(TitleContext);
+ return (
+
+ {title}
+
+ );
+ }
+
+ const customTitle = 'CustomTitle';
+
+ function App() {
+ return (
+
+
+
+ );
+ }
+
+ it('render ui component with initial context value', () => {
+ const wrapper = Wrap();
+ expect(wrapper.text()).to.equal(initialTitle);
+ });
+
+ // TODO: useContext: enable when shallow dive supports createContext
+ itIf(!isShallow, 'render ui component with value from outer provider', () => {
+ const wrapper = Wrap();
+ const subWrapper = isShallow ? wrapper.dive().dive() : wrapper;
+ expect(subWrapper.text()).to.equal(customTitle);
+ });
+ });
+
+ // TODO: useContext: enable when shallow dive supports createContext
+ describeIf(!isShallow, 'useContext: with Setting', () => {
+ const initialState = 10;
+ const context = createContext && createContext(null);
+
+ function MyGrandChild() {
+ const myContextVal = useContext(context);
+
+ const increment = () => {
+ myContextVal.setState(myContextVal.state + 1);
+ };
+
+ return (
+
+
+
+ {myContextVal.state}
+
+
+ );
+ }
+
+ function MyChild() {
+ return (
+
+
+
+ );
+ }
+
+ function App() {
+ const [state, setState] = useState(initialState);
+
+ return (
+
+
+
+
+
+ );
+ }
+
+ it('test render, get and set context value ', () => {
+ const wrapper = Wrap();
+
+ function getChild() {
+ const child = wrapper.find(MyChild);
+ return isShallow ? child.dive() : child;
+ }
+ function getGrandChild() {
+ const grandchild = getChild().find(MyGrandChild);
+ return isShallow ? grandchild.dive() : grandchild;
+ }
+ expect(getGrandChild().find('.grandChildState').debug()).to.equal(`
+ ${String(initialState)}
+`);
+
+ getGrandChild().find('button').props().onClick();
+ wrapper.update();
+ expect(getGrandChild().find('.grandChildState').debug()).to.equal(`
+ ${String(initialState + 1)}
+`);
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/hooks/useEffect.jsx b/packages/enzyme-test-suite/test/shared/hooks/useEffect.jsx
index 66776695e..9f49d818d 100644
--- a/packages/enzyme-test-suite/test/shared/hooks/useEffect.jsx
+++ b/packages/enzyme-test-suite/test/shared/hooks/useEffect.jsx
@@ -1,14 +1,18 @@
import React from 'react';
import { expect } from 'chai';
+import sinon from 'sinon-sandbox';
import {
describeIf,
itIf,
} from '../../_helpers';
-
+import {
+ is,
+} from '../../_helpers/version';
import {
useEffect,
useState,
+ Fragment,
} from '../../_helpers/react-compat';
export default function describeUseEffect({
@@ -16,7 +20,8 @@ export default function describeUseEffect({
Wrap,
isShallow,
}) {
- describeIf(hasHooks, 'hooks: useEffect', () => {
+ // TODO: enable when the shallow renderer fixes its bug, see https://github.com/facebook/react/issues/15275.
+ describeIf(hasHooks && !isShallow, 'hooks: useEffect', () => {
const timeout = 100;
function ComponentUsingEffectHook() {
const [ctr, setCtr] = useState(0);
@@ -33,8 +38,7 @@ export default function describeUseEffect({
);
}
- // TODO: enable when the shallow renderer fixes its bug
- itIf(!isShallow, 'works with `useEffect`', (done) => {
+ it('works', (done) => {
const wrapper = Wrap();
expect(wrapper.debug()).to.equal(
@@ -65,5 +69,185 @@ export default function describeUseEffect({
done();
}, timeout + 1);
});
+
+ describe('with mount effect', () => {
+ const didMountCount = 9;
+
+ function FooCounterWithMountEffect({ initialCount = 0 }) {
+ const [count, setCount] = useState(+initialCount);
+
+ useEffect(() => {
+ setCount(didMountCount);
+ }, []);
+ return (
+
+
+ {count}
+
+
+ );
+ }
+
+ it('initial render after did mount effect', () => {
+ const wrapper = Wrap();
+ expect(wrapper.find('.counter').text()).to.equal(String(didMountCount));
+ });
+ });
+
+ describe('with async effect', () => {
+ it('works with `useEffect`', (done) => {
+ const wrapper = Wrap();
+
+ expect(wrapper.debug()).to.equal(
+ isShallow
+ ? `
+ 1
+
`
+ : `
+
+ 1
+
+`,
+ );
+
+ setTimeout(() => {
+ wrapper.update();
+ expect(wrapper.debug()).to.equal(
+ isShallow
+ ? `
+ 2
+
`
+ : `
+
+ 2
+
+`,
+ );
+ done();
+ }, timeout + 1);
+ });
+ });
+
+ describe('on componentDidUpdate & componentDidMount', () => {
+ const expectedCountString = x => `You clicked ${x} times`;
+
+ let setDocumentTitle;
+ function ClickCounterPage() {
+ const [count, setCount] = useState(0);
+
+ useEffect(() => {
+ setDocumentTitle(expectedCountString(count));
+ }, [count]);
+
+ return (
+
+
You clicked {count} times
+
+
+ );
+ }
+
+ beforeEach(() => {
+ setDocumentTitle = sinon.stub();
+ });
+
+ it('on mount initial render', () => {
+ const wrapper = Wrap();
+
+ expect(wrapper.find('p').text()).to.eq(expectedCountString(0));
+ expect(setDocumentTitle).to.have.property('callCount', 1);
+ expect(setDocumentTitle.args).to.deep.equal([[expectedCountString(0)]]);
+ });
+
+ it('on didupdate', () => {
+ const wrapper = Wrap();
+
+ expect(setDocumentTitle).to.have.property('callCount', 1);
+ const [firstCall] = setDocumentTitle.args;
+ expect(firstCall).to.deep.equal([expectedCountString(0)]);
+ expect(wrapper.find('p').text()).to.equal(expectedCountString(0));
+
+ wrapper.find('button').invoke('onClick')();
+
+ expect(setDocumentTitle).to.have.property('callCount', 2);
+ const [, secondCall] = setDocumentTitle.args;
+ expect(secondCall).to.deep.equal([expectedCountString(1)]);
+ expect(wrapper.find('p').text()).to.equal(expectedCountString(1));
+
+ wrapper.find('button').invoke('onClick')();
+ wrapper.find('button').invoke('onClick')();
+
+ expect(setDocumentTitle).to.have.property('callCount', 4);
+ const [,,, fourthCall] = setDocumentTitle.args;
+ expect(fourthCall).to.deep.equal([expectedCountString(3)]);
+ expect(wrapper.find('p').text()).to.equal(expectedCountString(3));
+ });
+ });
+
+ describe('with cleanup Effect', () => {
+ let ChatAPI;
+
+ beforeEach(() => {
+ ChatAPI = {
+ subscribeToFriendStatus: sinon.stub(),
+ unsubscribeFromFriendStatus: sinon.stub(),
+ };
+ });
+
+ function FriendStatus({ friend = {} }) {
+ const [isOnline, setIsOnline] = useState(null);
+
+ function handleStatusChange(status) {
+ setIsOnline(status.isOnline);
+ }
+
+ useEffect(() => {
+ ChatAPI.subscribeToFriendStatus(friend.id, handleStatusChange);
+ return function cleanup() {
+ ChatAPI.unsubscribeFromFriendStatus(friend.id, handleStatusChange);
+ };
+ }, [isOnline]);
+
+ if (isOnline === null) {
+ return 'Loading...';
+ }
+ return isOnline ? 'Online' : 'Offline';
+ }
+
+ const friend = { id: 'enzyme' };
+
+ it('on initial mount', () => {
+ const wrapper = Wrap();
+ expect(wrapper.debug()).to.equal(
+ `
+ Loading...
+`,
+ );
+ expect(wrapper.html()).to.eql('Loading...');
+ expect(ChatAPI.subscribeToFriendStatus.calledOnceWith(friend.id)).to.equal(true);
+ });
+
+ it('simulate status Change', () => {
+ const wrapper = Wrap();
+ const [[, simulateChange]] = ChatAPI.subscribeToFriendStatus.args;
+
+ simulateChange({ isOnline: true });
+
+ wrapper.update();
+ expect(wrapper.html()).to.eql('Online');
+ });
+
+ itIf(is('> 16.8.3'), 'cleanup on unmount', () => {
+ const wrapper = Wrap();
+
+ wrapper.unmount();
+
+ expect(ChatAPI.unsubscribeFromFriendStatus).to.have.property('callCount', 1);
+ const [[firstArg]] = ChatAPI.unsubscribeFromFriendStatus.args;
+ expect(firstArg).to.equal(friend.id);
+ });
+ });
});
}
diff --git a/packages/enzyme-test-suite/test/shared/hooks/useReducer.jsx b/packages/enzyme-test-suite/test/shared/hooks/useReducer.jsx
new file mode 100644
index 000000000..6e0670858
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/hooks/useReducer.jsx
@@ -0,0 +1,78 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import { describeIf } from '../../_helpers';
+
+import { useReducer } from '../../_helpers/react-compat';
+
+export default function describeUseReducer({
+ hasHooks,
+ Wrap,
+}) {
+ describeIf(hasHooks, 'hooks: useReducer', () => {
+ describe('with custom dispatch', () => {
+ const initialState = [];
+
+ function Child({ dispatch, text }) {
+ function fire() {
+ dispatch({
+ type: 'ADD_TEXT',
+ payload: text,
+ });
+ }
+
+ return ;
+ }
+
+ function reducer(state, action) {
+ switch (action.type) {
+ case 'ADD_TEXT':
+ return [...state, action.payload];
+ default:
+ throw new Error();
+ }
+ }
+
+ function FooBarTextList() {
+ const [state, dispatch] = useReducer(reducer, initialState);
+
+ return (
+
+
+
+ {state.map(text => (
+
{text}
+ ))}
+
+ );
+ }
+
+ it('render with initial state from useReducer', () => {
+ const wrapper = Wrap();
+ expect(wrapper.find('p')).to.have.lengthOf(0);
+ });
+
+ it('Test with Add Foo & Bar tex', () => {
+ const wrapper = Wrap();
+ expect(wrapper.find('p')).to.have.lengthOf(0);
+ wrapper.find('Child').at(0).props().dispatch({
+ type: 'ADD_TEXT',
+ payload: 'foo',
+ });
+ wrapper.update();
+
+ expect(wrapper.find('p')).to.have.lengthOf(1);
+ expect(wrapper.find('p').at(0).text()).to.equal('foo');
+
+ wrapper.find('Child').at(1).props().dispatch({
+ type: 'ADD_TEXT',
+ payload: 'bar',
+ });
+ wrapper.update();
+ expect(wrapper.find('p')).to.have.length(2);
+ expect(wrapper.find('p').at(0).text()).to.equal('foo');
+ expect(wrapper.find('p').at(1).text()).to.equal('bar');
+ });
+ });
+ });
+}
diff --git a/packages/enzyme-test-suite/test/shared/hooks/useState.jsx b/packages/enzyme-test-suite/test/shared/hooks/useState.jsx
new file mode 100644
index 000000000..30569d6cb
--- /dev/null
+++ b/packages/enzyme-test-suite/test/shared/hooks/useState.jsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import { expect } from 'chai';
+
+import {
+ describeIf,
+ itIf,
+} from '../../_helpers';
+
+import {
+ useState,
+ useEffect,
+ Fragment,
+} from '../../_helpers/react-compat';
+
+export default function describeUseState({
+ hasHooks,
+ Wrap,
+ isShallow,
+}) {
+ describeIf(hasHooks, 'hooks: useState', () => {
+ function FooCounter({ initialCount: initial = 0 }) {
+ const [count, setCount] = useState(+initial);
+
+ return (
+
+
+
+ {count}
+
+
+
+ );
+ }
+
+ const initialCount = 5;
+
+ it('initial render', () => {
+ const wrapper = Wrap();
+ expect(wrapper.find('.counter').text()).to.equal(String(initialCount));
+ });
+
+ it('lets increment', () => {
+ const wrapper = Wrap();
+
+ wrapper.find('.increment').props().onClick();
+
+ expect(wrapper.find('.counter').text()).to.equal(String(initialCount + 1));
+ });
+
+ it('now decrement', () => {
+ const wrapper = Wrap();
+
+ wrapper.find('.decrement').props().onClick();
+
+ expect(wrapper.find('.counter').text()).to.equal(String(initialCount - 1));
+ });
+
+ it('handles useState', () => {
+ function ComponentUsingStateHook() {
+ const [count] = useState(0);
+ return {count}
;
+ }
+
+ const wrapper = Wrap();
+
+ expect(wrapper.find('div').length).to.equal(1);
+ expect(wrapper.find('div').text()).to.equal('0');
+ });
+
+ it('handles setState returned from useState', () => {
+ function ComponentUsingStateHook() {
+ const [count, setCount] = useState(0);
+ return setCount(count + 1)}>{count}
;
+ }
+
+ const wrapper = Wrap();
+ const div = wrapper.find('div');
+ const setCount = div.prop('onClick');
+ setCount();
+ wrapper.update();
+
+ expect(wrapper.find('div').text()).to.equal('1');
+ });
+
+ describe('useState with willReceive prop effect / simulate getDerivedStateFromProp', () => {
+ const newPropCount = 10;
+
+ function FooCounterWithEffect({ initialCount: initial = 0 }) {
+ const [count, setCount] = useState(+initial);
+
+ useEffect(() => {
+ setCount(initial);
+ }, [initial]);
+
+ return (
+
+
+ {count}
+
+
+ );
+ }
+
+ // TODO: fixme when useEffect works in the shallow renderer, see https://github.com/facebook/react/issues/15275
+ itIf(!isShallow, 'initial render & new Props', () => {
+ const wrapper = Wrap();
+ expect(wrapper.find('.counter').text()).to.equal(String(initialCount));
+
+ wrapper.setProps({ initialCount: newPropCount });
+ expect(wrapper.find('.counter').text()).to.equal(String(newPropCount));
+ });
+ });
+ });
+}