diff --git a/packages/peregrine/lib/talons/SignIn/__tests__/useSignIn.spec.js b/packages/peregrine/lib/talons/SignIn/__tests__/useSignIn.spec.js new file mode 100644 index 0000000000..c760f77344 --- /dev/null +++ b/packages/peregrine/lib/talons/SignIn/__tests__/useSignIn.spec.js @@ -0,0 +1,174 @@ +import React from 'react'; +import { MockedProvider } from '@apollo/client/testing'; +import { renderHook, act } from '@testing-library/react-hooks'; + +import { useCartContext } from '../../../context/cart'; +import { useUserContext } from '../../../context/user'; +import defaultOperations from '../signIn.gql'; +import { useSignIn } from '../useSignIn'; + +jest.mock('../../../Apollo/clearCartDataFromCache'); +jest.mock('../../../Apollo/clearCustomerDataFromCache'); +jest.mock('../../../hooks/useAwaitQuery'); +jest.mock('../../../store/actions/cart', () => ({ + retrieveCartId: jest.fn().mockReturnValue('new-cart-id') +})); + +jest.mock('../../../context/cart', () => ({ + useCartContext: jest.fn().mockReturnValue([ + { cartId: 'old-cart-id' }, + { + createCart: jest.fn(), + removeCart: jest.fn(), + getCartDetails: jest.fn() + } + ]) +})); + +jest.mock('../../../context/user', () => ({ + useUserContext: jest.fn().mockReturnValue([ + { + isGettingDetails: false, + getDetailsError: 'getDetails error from redux' + }, + { getUserDetails: jest.fn(), setToken: jest.fn() } + ]) +})); + +const signInVariables = { + email: 'fry@planetexpress.com', + password: 'slurm is the best' +}; +const authToken = 'auth-token-123'; + +const signInMock = { + request: { + query: defaultOperations.signInMutation, + variables: signInVariables + }, + result: { + data: { generateCustomerToken: { token: authToken } } + } +}; + +const mergeCartsMock = { + request: { + query: defaultOperations.mergeCartsMutation, + variables: { + destinationCartId: 'new-cart-id', + sourceCartId: 'old-cart-id' + } + }, + result: { + data: null + } +}; + +const initialProps = { + getCartDetailsQuery: 'getCartDetailsQuery', + setDefaultUsername: jest.fn(), + showCreateAccount: jest.fn(), + showForgotPassword: jest.fn() +}; + +const renderHookWithProviders = ({ + renderHookOptions = { initialProps }, + mocks = [signInMock, mergeCartsMock] +} = {}) => { + const wrapper = ({ children }) => ( + + {children} + + ); + + return renderHook(useSignIn, { wrapper, ...renderHookOptions }); +}; + +test('returns correct shape', () => { + const { result } = renderHookWithProviders(); + + expect(result.current).toMatchInlineSnapshot(` + Object { + "errors": Map { + "getUserDetailsQuery" => "getDetails error from redux", + "signInMutation" => undefined, + }, + "handleCreateAccount": [Function], + "handleForgotPassword": [Function], + "handleSubmit": [Function], + "isBusy": false, + "setFormApi": [Function], + } + `); +}); + +test('handleSubmit triggers waterfall of operations and actions', async () => { + const [, { getCartDetails }] = useCartContext(); + const [, { getUserDetails, setToken }] = useUserContext(); + + const { result } = renderHookWithProviders(); + + await act(() => result.current.handleSubmit(signInVariables)); + + expect(result.current.isBusy).toBe(true); + expect(setToken).toHaveBeenCalledWith(authToken); + expect(getCartDetails).toHaveBeenCalled(); + expect(getUserDetails).toHaveBeenCalled(); +}); + +test('handleSubmit exception is logged and resets state', async () => { + const errorMessage = 'Oh no! Something went wrong :('; + const [, { getUserDetails, setToken }] = useUserContext(); + setToken.mockRejectedValue(errorMessage); + jest.spyOn(console, 'error'); + + const { result } = renderHookWithProviders(); + + await act(() => result.current.handleSubmit(signInVariables)); + + expect(result.current.isBusy).toBe(false); + expect(getUserDetails).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalledWith(errorMessage); +}); + +test('handleForgotPassword triggers callbacks', () => { + const mockUsername = 'fry@planetexpress.com'; + const mockApi = { + getValue: jest.fn().mockReturnValue(mockUsername) + }; + + const { result } = renderHookWithProviders(); + act(() => result.current.setFormApi(mockApi)); + act(() => result.current.handleForgotPassword()); + + expect(initialProps.setDefaultUsername).toHaveBeenCalledWith(mockUsername); + expect(initialProps.showForgotPassword).toHaveBeenCalled(); +}); + +test('handleCreateAccount triggers callbacks', () => { + const mockUsername = 'fry@planetexpress.com'; + const mockApi = { + getValue: jest.fn().mockReturnValue(mockUsername) + }; + + const { result } = renderHookWithProviders(); + act(() => result.current.setFormApi(mockApi)); + act(() => result.current.handleCreateAccount()); + + expect(initialProps.setDefaultUsername).toHaveBeenCalledWith(mockUsername); + expect(initialProps.showCreateAccount).toHaveBeenCalled(); +}); + +test('mutation error is returned by talon', async () => { + const signInErrorMock = { + request: signInMock.request, + error: new Error('Uh oh! There was an error signing in :(') + }; + + const { result } = renderHookWithProviders({ mocks: [signInErrorMock] }); + await act(() => result.current.handleSubmit(signInVariables)); + + expect(result.current.errors.get('signInMutation')).toMatchInlineSnapshot( + `[Error: Uh oh! There was an error signing in :(]` + ); +}); diff --git a/packages/peregrine/package.json b/packages/peregrine/package.json index a5c2eeb499..0c3b762cb6 100644 --- a/packages/peregrine/package.json +++ b/packages/peregrine/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@magento/eslint-config": "~1.5.0", + "@testing-library/react-hooks": "~5.0.3", "intl": "~1.2.5", "intl-locales-supported": "~1.8.12", "react": "~17.0.1", diff --git a/yarn.lock b/yarn.lock index f6eacfa928..10d058c737 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2975,6 +2975,18 @@ dependencies: defer-to-connect "^1.0.1" +"@testing-library/react-hooks@~5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@testing-library/react-hooks/-/react-hooks-5.0.3.tgz#dd0d2048817b013b266d35ca45e3ea48a19fd87e" + integrity sha512-UrnnRc5II7LMH14xsYNm/WRch/67cBafmrSQcyFh0v+UUmSf1uzfB7zn5jQXSettGwOSxJwdQUN7PgkT0w22Lg== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + filter-console "^0.1.1" + react-error-boundary "^3.1.0" + "@types/anymatch@*": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a" @@ -3262,6 +3274,13 @@ "@types/react" "*" "@types/reactcss" "*" +"@types/react-dom@>=16.9.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.0.tgz#b3b691eb956c4b3401777ee67b900cb28415d95a" + integrity sha512-lUqY7OlkF/RbNtD5nIq7ot8NquXrdFrjSOR6+w9a9RFQevGi1oZO1dcJbXMeONAPKtZ2UrZOEJ5UOCVsxbLk/g== + dependencies: + "@types/react" "*" + "@types/react-syntax-highlighter@11.0.4": version "11.0.4" resolved "https://registry.yarnpkg.com/@types/react-syntax-highlighter/-/react-syntax-highlighter-11.0.4.tgz#d86d17697db62f98046874f62fdb3e53a0bbc4cd" @@ -3269,6 +3288,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@>=16.9.0": + version "17.0.0" + resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-17.0.0.tgz#9be47b375eeb906fced37049e67284a438d56620" + integrity sha512-nvw+F81OmyzpyIE1S0xWpLonLUZCMewslPuA8BtjSKc5XEbn8zEQBXS7KuOLHTNnSOEM2Pum50gHOoZ62tqTRg== + dependencies: + "@types/react" "*" + "@types/react@*": version "16.9.17" resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.17.tgz#58f0cc0e9ec2425d1441dd7b623421a867aa253e" @@ -3277,6 +3303,14 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/react@>=16.9.0": + version "17.0.1" + resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.1.tgz#eb1f1407dea8da3bc741879c1192aa703ab9975b" + integrity sha512-w8t9f53B2ei4jeOqf/gxtc2Sswnc3LBK5s0DyJcg5xd10tMHXts2N31cKjWfH9IC/JvEPa/YF1U4YeP1t4R6HQ== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -6416,6 +6450,11 @@ csstype@^2.2.0, csstype@^2.5.7: resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.8.tgz#0fb6fc2417ffd2816a418c9336da74d7f07db431" integrity sha512-msVS9qTuMT5zwAGCVm4mxfrZ18BNc6Csd0oJAtiFMZ1FAx1CCvy2+5MDmYoix63LM/6NDbNtodCiGYGmFgO0dA== +csstype@^3.0.2: + version "3.0.6" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.6.tgz#865d0b5833d7d8d40f4e5b8a6d76aea3de4725ef" + integrity sha512-+ZAmfyWMT7TiIlzdqJgjMb7S4f1beorDbWbsocyK4RaiqA5RTX3K14bnBWmmA9QEM0gRdsjyyrEmcyga8Zsxmw== + csv-parse@~4.4.6: version "4.4.7" resolved "https://registry.yarnpkg.com/csv-parse/-/csv-parse-4.4.7.tgz#069de0875b92780ca74a018c9880ab41cb3517a1" @@ -8198,6 +8237,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-console@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/filter-console/-/filter-console-0.1.1.tgz#6242be28982bba7415bcc6db74a79f4a294fa67c" + integrity sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg== + finalhandler@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" @@ -14322,6 +14366,13 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" +react-error-boundary@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/react-error-boundary/-/react-error-boundary-3.1.0.tgz#9487443df2f9ba1db90d8ab52351814907ea4af3" + integrity sha512-lmPrdi5SLRJR+AeJkqdkGlW/CRkAUvZnETahK58J4xb5wpbfDngasEGu+w0T1iXEhVrYBJZeW+c4V1hILCnMWQ== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.7: version "6.0.8" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.8.tgz#474ed11d04fc6bda3af643447d85e9127ed6b5de"