From b9420dabb38415d60310f2effe0964d77ec3f24b Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Fri, 10 Jun 2022 00:44:44 -0400 Subject: [PATCH] chore: unenroll confirm modal tests --- .../__snapshots__/index.test.jsx.snap | 97 +++++++++ src/containers/UnenrollConfirmModal/hooks.js | 31 ++- .../UnenrollConfirmModal/hooks.test.js | 206 ++++++++++++++++++ src/containers/UnenrollConfirmModal/index.jsx | 4 +- .../UnenrollConfirmModal/index.test.jsx | 58 +++++ src/setupTest.js | 1 + 6 files changed, 384 insertions(+), 13 deletions(-) create mode 100644 src/containers/UnenrollConfirmModal/__snapshots__/index.test.jsx.snap create mode 100644 src/containers/UnenrollConfirmModal/hooks.test.js create mode 100644 src/containers/UnenrollConfirmModal/index.test.jsx diff --git a/src/containers/UnenrollConfirmModal/__snapshots__/index.test.jsx.snap b/src/containers/UnenrollConfirmModal/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..565896c --- /dev/null +++ b/src/containers/UnenrollConfirmModal/__snapshots__/index.test.jsx.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`UnenrollConfirmModal component snapshot: modalStates.confirm 1`] = ` + +
+ +
+
+`; + +exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason given 1`] = ` + +
+ +
+
+`; + +exports[`UnenrollConfirmModal component snapshot: modalStates.finished, reason skipped 1`] = ` + +
+ +
+
+`; + +exports[`UnenrollConfirmModal component snapshot: modalStates.reason 1`] = ` + +
+ +
+
+`; diff --git a/src/containers/UnenrollConfirmModal/hooks.js b/src/containers/UnenrollConfirmModal/hooks.js index dd674a3..8d58e13 100644 --- a/src/containers/UnenrollConfirmModal/hooks.js +++ b/src/containers/UnenrollConfirmModal/hooks.js @@ -7,7 +7,7 @@ import * as module from './hooks'; export const state = StrictDict({ confirmed: (val) => React.useState(val), - customReason: (val) => React.useState(val), + customOption: (val) => React.useState(val), isSkipped: (val) => React.useState(val), selectedReason: (val) => React.useState(val), submittedReason: (val) => React.useState(val), @@ -19,11 +19,15 @@ export const modalStates = StrictDict({ finished: 'finished', }); +export const valueCallback = (cb, prereqs = []) => ( + React.useCallback(e => cb(e.target.value), prereqs) +); + export const unenrollReasons = () => { const [selectedReason, setSelectedReason] = module.state.selectedReason(null); const [submittedReason, setSubmittedReason] = module.state.submittedReason(null); const [isSkipped, setIsSkipped] = module.state.isSkipped(false); - const [customOption, setCustomOption] = module.state.customReason(''); + const [customOption, setCustomOption] = module.state.customOption(''); return { clear: React.useCallback(() => { @@ -32,15 +36,21 @@ export const unenrollReasons = () => { setCustomOption(''); setIsSkipped(false); }, []), + + value: submittedReason, + customOption: { value: customOption, - onChange: React.useCallback((e) => setCustomOption(e.target.value), []), + onChange: module.valueCallback(setCustomOption), }, - isSkipped, - isSubmitted: submittedReason !== null || isSkipped, + selected: selectedReason, - selectOption: React.useCallback((e) => setSelectedReason(e.target.value), []), + selectOption: module.valueCallback(setSelectedReason), + + isSkipped, skip: React.useCallback(() => setIsSkipped(true), [isSkipped]), + + isSubmitted: submittedReason !== null || isSkipped, submit: React.useCallback(() => { if (selectedReason === 'custom') { setSubmittedReason(customOption); @@ -48,7 +58,6 @@ export const unenrollReasons = () => { setSubmittedReason(selectedReason); } }, [customOption, selectedReason]), - value: submittedReason, }; }; @@ -57,7 +66,7 @@ export const modalHooks = ({ closeModal, dispatch }) => { const confirm = React.useCallback(() => setIsConfirmed(true), []); - const reason = unenrollReasons(); + const reason = module.unenrollReasons(); const close = () => { closeModal(); setIsConfirmed(false); @@ -66,7 +75,7 @@ export const modalHooks = ({ closeModal, dispatch }) => { let modalState; if (isConfirmed) { - modalState = reason.isSubmitted ? modalStates.finished : modalState.reason; + modalState = reason.isSubmitted ? modalStates.finished : modalStates.reason; } else { modalState = modalStates.confirm; } @@ -74,14 +83,14 @@ export const modalHooks = ({ closeModal, dispatch }) => { const closeAndRefresh = React.useCallback(() => { dispatch(thunkActions.app.refreshList()); close(); - }, []); + }, [reason, isConfirmed]); return { isConfirmed, confirm, reason, + close: React.useCallback(close, [reason, isConfirmed]), closeAndRefresh, - close, modalState, }; }; diff --git a/src/containers/UnenrollConfirmModal/hooks.test.js b/src/containers/UnenrollConfirmModal/hooks.test.js new file mode 100644 index 0000000..3355dac --- /dev/null +++ b/src/containers/UnenrollConfirmModal/hooks.test.js @@ -0,0 +1,206 @@ +import React from 'react'; + +import { MockUseState, testCardValues } from 'testUtils'; +import * as hooks from './hooks'; +import { thunkActions } from 'data/redux'; + +jest.mock('data/redux/thunkActions/app', () => ({ + refreshList: jest.fn((args) => ({ refreshList: args })), +})); + +const state = new MockUseState(hooks); +const testValue = 'test-value'; +let out; +let hook; + +describe('UnenrollConfirmModal hooks', () => { + describe('state fields', () => { + state.testGetter(state.keys.confirmed); + state.testGetter(state.keys.customOption); + state.testGetter(state.keys.isSkipped); + state.testGetter(state.keys.selectedReason); + state.testGetter(state.keys.submittedReason); + }); + describe('valueCallback', () => { + describe('returned react callback', () => { + test('calls passed method with event value and prereqs', () => { + const cb = jest.fn(); + const prereqs = ['test', 'values']; + const returned = hooks.valueCallback(cb, prereqs).useCallback; + expect(returned.prereqs).toEqual(prereqs); + returned.cb({ target: { value: testValue } }); + expect(cb).toHaveBeenCalledWith(testValue); + }); + test('calls passed method with event value and no prereqs if not passed', () => { + const cb = jest.fn(); + const returned = hooks.valueCallback(cb).useCallback; + expect(returned.prereqs).toEqual([]); + returned.cb({ target: { value: testValue } }); + expect(cb).toHaveBeenCalledWith(testValue); + }); + }); + }); + describe('unenrollReasons', () => { + const mockValueCB = (cb) => ({ callback: cb }); + beforeEach(() => { + hook = hooks.unenrollReasons; + state.mock(); + hooks.valueCallback = jest.fn(mockValueCB); + out = hook(); + }); + afterEach(() => { + state.restore(); + hooks.valueCallback.mockClear(); + }); + describe('clear method', () => { + it('resets selected and submitted reasons, custom option and isSkipped', () => { + const { cb, prereqs } = out.clear.useCallback; + expect(prereqs).toEqual([]); + cb(); + expect(state.setState.selectedReason).toHaveBeenCalledWith(null); + expect(state.setState.submittedReason).toHaveBeenCalledWith(null); + expect(state.setState.customOption).toHaveBeenCalledWith(''); + expect(state.setState.isSkipped).toHaveBeenCalledWith(false); + }); + }); + test('value returns submitted reason', () => { + state.mockVal(state.keys.submittedReason, testValue); + expect(hook().value).toEqual(testValue); + }); + test('customOption.value returns custom option', () => { + state.mockVal(state.keys.customOption, testValue); + expect(hook().customOption.value).toEqual(testValue); + }); + test('customOption.onChange returns valueCallback for setCustomOption', () => { + expect(out.customOption.onChange).toEqual(mockValueCB(state.setState.customOption)); + }); + test('selected returns selectedReason', () => { + state.mockVal(state.keys.selectedReason, testValue); + expect(hook().selected).toEqual(testValue); + }); + test('selectedOption returns valueCallback for setSelectedReason', () => { + expect(out.selectOption).toEqual(mockValueCB(state.setState.selectedReason)); + }); + test('isSkipped returns state value', () => { + state.mockVal(state.keys.isSkipped, testValue); + expect(hook().isSkipped).toEqual(testValue); + }); + test('skip returns callback based on isSkipped that sets isSkipped to true', () => { + const { cb, prereqs } = out.skip.useCallback; + expect(prereqs).toEqual([state.stateVals.isSkipped]); + cb(); + expect(state.setState.isSkipped).toHaveBeenCalledWith(true); + }); + describe('isSubmitted', () => { + it('returns false if submittedReason is null and not isSkipped', () => { + expect(out.isSubmitted).toEqual(false); + }); + it('returns true if submittedReason is not null', () => { + state.mockVal(state.keys.submittedReason, testValue); + expect(hook().isSubmitted).toEqual(true); + }); + it('returns true if isSkipped', () => { + state.mockVal(state.keys.isSkipped, true); + expect(hook().isSubmitted).toEqual(true); + }); + }); + describe('submit', () => { + it('sets customOption as submittedReason if selectedReason is custom', () => { + state.mockVal(state.keys.selectedReason, 'custom'); + state.mockVal(state.keys.customOption, testValue); + hook().submit.useCallback.cb(); + expect(state.setState.submittedReason).toHaveBeenCalledWith(testValue); + }); + it('sets selectedReason as submittedReason if selectedReason is not custom', () => { + state.mockVal(state.keys.selectedReason, testValue); + state.mockVal(state.keys.customOption, 'customValue'); + hook().submit.useCallback.cb(); + expect(state.setState.submittedReason).toHaveBeenCalledWith(testValue); + }); + it('depends on customOption and selectedReason', () => { + const customValue = 'custom-value'; + state.mockVal(state.keys.selectedReason, testValue); + state.mockVal(state.keys.customOption, customValue); + const { prereqs } = hook().submit.useCallback; + expect(prereqs).toContain(testValue); + expect(prereqs).toContain(customValue); + }); + }); + }); + describe('modalHooks', () => { + const closeModal = jest.fn(); + const dispatch = jest.fn(); + let mockReason; + beforeEach(() => { + hook = hooks.modalHooks; + mockReason = { + isSubmitted: false, + clear: jest.fn(), + }; + state.mock(); + state.mockVal(state.keys.confirmed, testValue); + hooks.unenrollReasons = jest.fn(() => mockReason); + out = hook({ closeModal, dispatch }); + }); + afterEach(() => { + state.restore(); + hooks.unenrollReasons.mockReset(); + }); + test('isConfirmed is forwarded from state', () => { + expect(out.isConfirmed).toEqual(testValue); + }); + test('confirm is no-prereqs callback that sets isConfirmed to true', () => { + const { cb, prereqs } = out.confirm.useCallback; + expect(prereqs).toEqual([]); + cb(); + expect(state.setState.confirmed).toHaveBeenCalledWith(true); + }); + test('reason returns unenrollReasons output', () => { + expect(out.reason).toEqual(mockReason); + }); + describe('close', () => { + test('callback based on reason and isConfirmed', () => { + expect(out.close.useCallback.prereqs).toEqual([mockReason, testValue]); + }); + it('calls closeModal, sets isConfirmed to false, and calls reason.clear', () => { + out.close.useCallback.cb(); + expect(closeModal).toHaveBeenCalled(); + expect(state.setState.confirmed).toHaveBeenCalledWith(false); + expect(mockReason.clear).toHaveBeenCalled(); + }); + }); + describe('closeAndRefresh', () => { + test('callback based on reason and isConfirmed', () => { + expect(out.closeAndRefresh.useCallback.prereqs).toEqual([mockReason, testValue]); + }); + it('calls closeModal, sets isConfirmed to false, and calls reason.clear', () => { + out.closeAndRefresh.useCallback.cb(); + expect(closeModal).toHaveBeenCalled(); + expect(state.setState.confirmed).toHaveBeenCalledWith(false); + expect(mockReason.clear).toHaveBeenCalled(); + }); + it('dispatches refreshList thunkAction', () => { + out.closeAndRefresh.useCallback.cb(); + expect(dispatch).toHaveBeenCalledWith(thunkActions.app.refreshList()); + }); + }); + describe('modalState', () => { + it('returns modalStates.finished if confirmed and submitted', () => { + state.mockVal(state.keys.confirmed, true); + hooks.unenrollReasons = jest.fn(() => ({ ...mockReason, isSubmitted: true })); + out = hook({ closeModal, dispatch }); + expect(out.modalState).toEqual(hooks.modalStates.finished); + }); + it('returns modalStates.reason if confirmed and not submitted', () => { + state.mockVal(state.keys.confirmed, true); + out = hook({ closeModal, dispatch }); + expect(out.modalState).toEqual(hooks.modalStates.reason); + }); + it('returns modalStates.confirm if not confirmed', () => { + state.mockVal(state.keys.confirmed, false); + out = hook({ closeModal, dispatch }); + expect(out.modalState).toEqual(hooks.modalStates.confirm); + }); + }); + }); +}); diff --git a/src/containers/UnenrollConfirmModal/index.jsx b/src/containers/UnenrollConfirmModal/index.jsx index 9c33bee..cb4df20 100644 --- a/src/containers/UnenrollConfirmModal/index.jsx +++ b/src/containers/UnenrollConfirmModal/index.jsx @@ -12,7 +12,7 @@ import ConfirmPane from './components/ConfirmPane'; import ReasonPane from './components/ReasonPane'; import FinishedPane from './components/FinishedPane'; -import hooks, { modalStates } from './hooks'; +import { modalHooks, modalStates } from './hooks'; export const UnenrollConfirmModal = ({ closeModal, @@ -25,7 +25,7 @@ export const UnenrollConfirmModal = ({ closeAndRefresh, close, modalState, - } = hooks({ dispatch, closeModal }); + } = modalHooks({ dispatch, closeModal }); return ( 'ConfirmPane'); +jest.mock('./components/ReasonPane', () => 'ReasonPane'); +jest.mock('./components/FinishedPane', () => 'FinishedPane'); + +jest.mock('./hooks', () => ({ + __esModule: true, + modalStates: jest.requireActual('./hooks').modalStates, + modalHooks: jest.fn(), +})); + +describe('UnenrollConfirmModal component', () => { + const dispatch = useDispatch(); + const hookProps = { + confirm: jest.fn().mockName('hooks.confirm'), + reason: { + isSkipped: false, + reasonProps: 'other', + }, + close: jest.fn().mockName('hooks.close'), + closeAndRefresh: jest.fn().mockName('hooks.closeAndRefresh'), + modalState: hooks.modalStates.confirm, + }; + const closeModal = jest.fn().mockName('props.closeModal'); + const show = true; + test('hooks called with dispatch and closeModal props', () => { + hooks.modalHooks.mockReturnValueOnce(hookProps); + shallow(); + expect(hooks.modalHooks).toHaveBeenCalledWith({ dispatch, closeModal }); + }); + test('snapshot: modalStates.confirm', () => { + hooks.modalHooks.mockReturnValueOnce(hookProps); + expect(shallow()).toMatchSnapshot(); + }); + test('snapshot: modalStates.finished, reason given', () => { + hooks.modalHooks.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.finished }); + expect(shallow()).toMatchSnapshot(); + }); + test('snapshot: modalStates.finished, reason skipped', () => { + hooks.modalHooks.mockReturnValueOnce({ + ...hookProps, + modalState: hooks.modalStates.finished, + isSkipped: true, + }); + expect(shallow()).toMatchSnapshot(); + }); + test('snapshot: modalStates.reason', () => { + hooks.modalHooks.mockReturnValueOnce({ ...hookProps, modalState: hooks.modalStates.reason }); + expect(shallow()).toMatchSnapshot(); + }); +}); diff --git a/src/setupTest.js b/src/setupTest.js index 7177b9a..7c18c9b 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -77,6 +77,7 @@ jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedCompon Hyperlink: 'Hyperlink', Icon: 'Icon', IconButton: 'IconButton', + ModalDialog: 'ModalDialog', MultiSelectDropdownFilter: 'MultiSelectDropdownFilter', OverlayTrigger: 'OverlayTrigger', Popover: {