diff --git a/package-lock.json b/package-lock.json index ddfc4f1e..b6301811 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "AGPL-3.0", "dependencies": { "@edx/brand": "npm:@edx/brand-openedx@1.2.0", - "@edx/frontend-platform": "^4.6.0", + "@edx/frontend-platform": "^5.0.0", "@edx/paragon": "20.46.0", "@fortawesome/fontawesome-svg-core": "6.4.2", "@fortawesome/free-brands-svg-icons": "6.4.2", @@ -31,8 +31,8 @@ "react-loading-skeleton": "3.3.1", "react-redux": "7.2.9", "react-responsive": "8.2.0", - "react-router": "5.3.4", - "react-router-dom": "5.3.4", + "react-router": "6.14.2", + "react-router-dom": "6.14.2", "react-zendesk": "^0.1.13", "redux": "4.2.0", "redux-logger": "3.0.6", @@ -3330,9 +3330,9 @@ } }, "node_modules/@edx/frontend-platform": { - "version": "4.6.1", - "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-4.6.1.tgz", - "integrity": "sha512-Fi/k7iZlFYs8qCsAAVz6Dseyzb9bJGh3r6iKUCiAq4emUl9UA/LfFHe4fDZcA5trVIkohhdLqrDu1U3UksY/5w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-5.0.0.tgz", + "integrity": "sha512-DD9/B4rnC3BKPiWlbEFF1JIYFbWC6vUBKTyN8sf4khi4DNhhWhsobk+iNeCWNzF9UgCPRbniIqesdV1F9NXNZw==", "dependencies": { "@cospired/i18n-iso-languages": "4.1.0", "@formatjs/intl-pluralrules": "4.3.3", @@ -3365,7 +3365,7 @@ "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0", "react-redux": "^7.1.1", - "react-router-dom": "^5.0.1", + "react-router-dom": "^6.0.0", "redux": "^4.0.4" } }, @@ -5078,6 +5078,14 @@ "resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz", "integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==" }, + "node_modules/@remix-run/router": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.7.2.tgz", + "integrity": "sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==", + "engines": { + "node": ">=14" + } + }, "node_modules/@restart/context": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@restart/context/-/context-2.1.4.tgz", @@ -16591,19 +16599,6 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, - "node_modules/path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/path-to-regexp/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==" - }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -18298,72 +18293,35 @@ } }, "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.14.2.tgz", + "integrity": "sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "@remix-run/router": "1.7.2" + }, + "engines": { + "node": ">=14" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8" } }, "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.14.2.tgz", + "integrity": "sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==", "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" + "@remix-run/router": "1.7.2", + "react-router": "6.14.2" + }, + "engines": { + "node": ">=14" }, "peerDependencies": { - "react": ">=15" + "react": ">=16.8", + "react-dom": ">=16.8" } }, - "node_modules/react-router-dom/node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, - "node_modules/react-router/node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, - "node_modules/react-router/node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" - }, "node_modules/react-shallow-renderer": { "version": "16.15.0", "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz", diff --git a/package.json b/package.json index 2558a15a..30b17621 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ }, "dependencies": { "@edx/brand": "npm:@edx/brand-openedx@1.2.0", - "@edx/frontend-platform": "^4.6.0", + "@edx/frontend-platform": "^5.0.0", "@edx/paragon": "20.46.0", "@fortawesome/fontawesome-svg-core": "6.4.2", "@fortawesome/free-brands-svg-icons": "6.4.2", @@ -54,8 +54,8 @@ "react-loading-skeleton": "3.3.1", "react-redux": "7.2.9", "react-responsive": "8.2.0", - "react-router": "5.3.4", - "react-router-dom": "5.3.4", + "react-router": "6.14.2", + "react-router-dom": "6.14.2", "react-zendesk": "^0.1.13", "redux": "4.2.0", "redux-logger": "3.0.6", diff --git a/src/MainApp.jsx b/src/MainApp.jsx index 75f1aac0..26c2cf59 100755 --- a/src/MainApp.jsx +++ b/src/MainApp.jsx @@ -3,7 +3,7 @@ import React from 'react'; import { getConfig } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; import { Helmet } from 'react-helmet'; -import { Redirect, Route, Switch } from 'react-router-dom'; +import { Navigate, Route, Routes } from 'react-router-dom'; import { EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk, @@ -37,26 +37,26 @@ const MainApp = () => ( {getConfig().ZENDESK_KEY && } - - - - - + } /> + } /> - } /> - - - - - - - - - - + + } + /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + ); diff --git a/src/common-components/EmbeddedRegistrationRoute.jsx b/src/common-components/EmbeddedRegistrationRoute.jsx index 81d17641..3d26078c 100644 --- a/src/common-components/EmbeddedRegistrationRoute.jsx +++ b/src/common-components/EmbeddedRegistrationRoute.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Redirect, Route } from 'react-router-dom'; +import { Navigate } from 'react-router-dom'; import { PAGE_NOT_FOUND } from '../data/constants'; import { isHostAvailableInQueryParams } from '../data/utils'; @@ -10,19 +10,19 @@ import { isHostAvailableInQueryParams } from '../data/utils'; * This wrapper redirects the requester to embedded register page only if host * query param is present. */ -const EmbeddedRegistrationRoute = (props) => { +const EmbeddedRegistrationRoute = ({ children }) => { const registrationEmbedded = isHostAvailableInQueryParams(); // Show registration page for embedded experience even if the user is authenticated if (registrationEmbedded) { - return ; + return children; } - return ; + return ; }; EmbeddedRegistrationRoute.propTypes = { - path: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, }; export default EmbeddedRegistrationRoute; diff --git a/src/common-components/RedirectLogistration.jsx b/src/common-components/RedirectLogistration.jsx index 213cf272..7b116f8b 100644 --- a/src/common-components/RedirectLogistration.jsx +++ b/src/common-components/RedirectLogistration.jsx @@ -1,8 +1,6 @@ -import React from 'react'; - import { getConfig } from '@edx/frontend-platform'; import PropTypes from 'prop-types'; -import { Redirect } from 'react-router-dom'; +import { Navigate } from 'react-router-dom'; import { AUTHN_PROGRESSIVE_PROFILING, RECOMMENDATIONS, REDIRECT, @@ -49,13 +47,13 @@ const RedirectLogistration = (props) => { } const registrationResult = { redirectUrl: finalRedirectUrl, success }; return ( - ); } @@ -64,14 +62,14 @@ const RedirectLogistration = (props) => { if (redirectToRecommendationsPage) { const registrationResult = { redirectUrl: finalRedirectUrl, success }; return ( - ); } diff --git a/src/common-components/UnAuthOnlyRoute.jsx b/src/common-components/UnAuthOnlyRoute.jsx index 71a52717..7be62aae 100644 --- a/src/common-components/UnAuthOnlyRoute.jsx +++ b/src/common-components/UnAuthOnlyRoute.jsx @@ -1,9 +1,8 @@ -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; import { getConfig } from '@edx/frontend-platform'; import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth'; import PropTypes from 'prop-types'; -import { Route } from 'react-router-dom'; import { DEFAULT_REDIRECT_URL, @@ -13,7 +12,7 @@ import { * This wrapper redirects the requester to our default redirect url if they are * already authenticated. */ -const UnAuthOnlyRoute = (props) => { +const UnAuthOnlyRoute = ({ children }) => { const [authUser, setAuthUser] = useState({}); const [isReady, setIsReady] = useState(false); @@ -30,14 +29,14 @@ const UnAuthOnlyRoute = (props) => { return null; } - return ; + return children; } return null; }; UnAuthOnlyRoute.propTypes = { - path: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, }; export default UnAuthOnlyRoute; diff --git a/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx b/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx index 0b6638bf..38ccff6a 100644 --- a/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx +++ b/src/common-components/tests/EmbeddedRegistrationRoute.test.jsx @@ -9,7 +9,9 @@ import { act } from 'react-dom/test-utils'; import { REGISTER_EMBEDDED_PAGE } from '../../data/constants'; import EmbeddedRegistrationRoute from '../EmbeddedRegistrationRoute'; -import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom'; +import { + MemoryRouter, Route, BrowserRouter as Router, Routes, +} from 'react-router-dom'; const RRD = require('react-router-dom'); // Just render plain div with its children @@ -20,9 +22,12 @@ module.exports = RRD; const TestApp = () => (
- - (Embedded Register Page)} /> - + + Embedded Register Page} + /> +
); diff --git a/src/common-components/tests/UnAuthOnlyRoute.test.jsx b/src/common-components/tests/UnAuthOnlyRoute.test.jsx index a055de4a..9625386e 100644 --- a/src/common-components/tests/UnAuthOnlyRoute.test.jsx +++ b/src/common-components/tests/UnAuthOnlyRoute.test.jsx @@ -9,7 +9,9 @@ import { act } from 'react-dom/test-utils'; import { UnAuthOnlyRoute } from '..'; import { REGISTER_PAGE } from '../../data/constants'; -import { MemoryRouter, BrowserRouter as Router, Switch } from 'react-router-dom'; +import { + MemoryRouter, Route, BrowserRouter as Router, Routes, +} from 'react-router-dom'; jest.mock('@edx/frontend-platform/auth', () => ({ getAuthenticatedUser: jest.fn(), @@ -25,9 +27,9 @@ module.exports = RRD; const TestApp = () => (
- - (Register Page)} /> - + + Register Page} /> +
); diff --git a/src/forgot-password/ForgotPasswordPage.jsx b/src/forgot-password/ForgotPasswordPage.jsx index 2f2b12e4..49770cde 100644 --- a/src/forgot-password/ForgotPasswordPage.jsx +++ b/src/forgot-password/ForgotPasswordPage.jsx @@ -15,7 +15,7 @@ import { import { ChevronLeft } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { Redirect } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { forgotPassword, setForgotPasswordFormData } from './data/actions'; import { forgotPasswordResultSelector } from './data/selectors'; @@ -38,7 +38,7 @@ const ForgotPasswordPage = (props) => { const [bannerEmail, setBannerEmail] = useState(''); const [formErrors, setFormErrors] = useState(''); const [validationError, setValidationError] = useState(emailValidationError); - const [key, setKey] = useState(''); + const navigate = useNavigate(); useEffect(() => { sendPageEvent('login_and_registration', 'reset'); @@ -102,12 +102,9 @@ const ForgotPasswordPage = (props) => {
- setKey(k)}> + navigate(updatePathWithQueryParams(key))}> - { key && ( - - )}
diff --git a/src/forgot-password/tests/ForgotPasswordPage.test.jsx b/src/forgot-password/tests/ForgotPasswordPage.test.jsx index c0648219..2d655242 100644 --- a/src/forgot-password/tests/ForgotPasswordPage.test.jsx +++ b/src/forgot-password/tests/ForgotPasswordPage.test.jsx @@ -4,9 +4,8 @@ import { Provider } from 'react-redux'; import { mergeConfig } from '@edx/frontend-platform'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; -import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; -import { MemoryRouter, Router } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants'; @@ -14,15 +13,20 @@ import { PASSWORD_RESET } from '../../reset-password/data/constants'; import { setForgotPasswordFormData } from '../data/actions'; import ForgotPasswordPage from '../ForgotPasswordPage'; +const mockedNavigator = jest.fn(); + jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), sendTrackEvent: jest.fn(), })); jest.mock('@edx/frontend-platform/auth'); +jest.mock('react-router-dom', () => ({ + ...(jest.requireActual('react-router-dom')), + useNavigate: () => mockedNavigator, +})); const IntlForgotPasswordPage = injectIntl(ForgotPasswordPage); const mockStore = configureStore(); -const history = createMemoryHistory(); const initialState = { forgotPassword: { @@ -225,15 +229,11 @@ describe('ForgotPasswordPage', () => { }); it('should redirect onto login page', async () => { - const forgotPasswordPage = mount(reduxWrapper( - - - , - )); + const forgotPasswordPage = mount(reduxWrapper()); await act(async () => { await forgotPasswordPage.find('nav').find('a').first().simulate('click'); }); forgotPasswordPage.update(); - expect(history.location.pathname).toEqual(LOGIN_PAGE); + expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE); }); }); diff --git a/src/login/ChangePasswordPrompt.jsx b/src/login/ChangePasswordPrompt.jsx index 6a989320..7012eab6 100644 --- a/src/login/ChangePasswordPrompt.jsx +++ b/src/login/ChangePasswordPrompt.jsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { getConfig } from '@edx/frontend-platform'; import { useIntl } from '@edx/frontend-platform/i18n'; @@ -7,7 +7,7 @@ import { } from '@edx/paragon'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import { Link, Redirect } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import messages from './messages'; import { DEFAULT_REDIRECT_URL, RESET_PAGE } from '../data/constants'; @@ -29,10 +29,14 @@ const ChangePasswordPrompt = ({ variant, redirectUrl }) => { // eslint-disable-next-line no-unused-vars const [isOpen, open, close] = useToggle(true, handlers); const { formatMessage } = useIntl(); + const navigate = useNavigate(); + + useEffect(() => { + if (redirectToResetPasswordPage) { + navigate(updatePathWithQueryParams(RESET_PAGE)); + } + }, [redirectToResetPasswordPage, navigate]); - if (redirectToResetPasswordPage) { - return ; - } return ( ({ + ...(jest.requireActual('react-router-dom')), + useNavigate: () => mockedNavigator, +})); describe('ChangePasswordPromptTests', () => { let props = {}; @@ -55,9 +59,7 @@ describe('ChangePasswordPromptTests', () => { const changePasswordPrompt = mount( - - - + , ); @@ -67,6 +69,6 @@ describe('ChangePasswordPromptTests', () => { }); changePasswordPrompt.update(); - expect(history.location.pathname).toEqual(RESET_PAGE); + expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE); }); }); diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx index 777087ce..1a74df96 100644 --- a/src/logistration/Logistration.jsx +++ b/src/logistration/Logistration.jsx @@ -12,7 +12,7 @@ import { } from '@edx/paragon'; import { ChevronLeft } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; -import { Redirect } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import BaseContainer from '../base-container'; import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions'; @@ -36,7 +36,7 @@ const Logistration = (props) => { } = tpaProviders; const { formatMessage } = useIntl(); const [institutionLogin, setInstitutionLogin] = useState(false); - const [key, setKey] = useState(''); + const navigate = useNavigate(); const disablePublicAccountCreation = getConfig().ALLOW_PUBLIC_ACCOUNT_CREATION === false; useEffect(() => { @@ -46,6 +46,12 @@ const Logistration = (props) => { } }); + useEffect(() => { + if (disablePublicAccountCreation) { + navigate(updatePathWithQueryParams(LOGIN_PAGE)); + } + }, [navigate, disablePublicAccountCreation]); + const handleInstitutionLogin = (e) => { sendTrackEvent('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); if (typeof e === 'string') { @@ -63,7 +69,7 @@ const Logistration = (props) => { if (tabKey === LOGIN_PAGE) { props.backupRegistrationForm(); } - setKey(tabKey); + navigate(updatePathWithQueryParams(tabKey)); }; const tabTitle = ( @@ -88,7 +94,6 @@ const Logistration = (props) => { {disablePublicAccountCreation ? ( <> - {institutionLogin && ( @@ -116,9 +121,6 @@ const Logistration = (props) => { ))} - { key && ( - - )}
{selectedPage === LOGIN_PAGE ? diff --git a/src/progressive-profiling/ProgressiveProfiling.jsx b/src/progressive-profiling/ProgressiveProfiling.jsx index ed9171a6..c64f0742 100644 --- a/src/progressive-profiling/ProgressiveProfiling.jsx +++ b/src/progressive-profiling/ProgressiveProfiling.jsx @@ -21,6 +21,7 @@ import { import { Error } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; +import { useLocation } from 'react-router-dom'; import { saveUserProfile } from './data/actions'; import { welcomePageContextSelector } from './data/selectors'; @@ -47,12 +48,12 @@ const ProgressiveProfiling = (props) => { const { formatMessage } = useIntl(); const { getFieldDataFromBackend, - location, submitState, showError, welcomePageContext, welcomePageContextApiStatus, } = props; + const location = useLocation(); const registrationEmbedded = isHostAvailableInQueryParams(); const queryParams = getAllPossibleQueryParams(); @@ -170,7 +171,7 @@ const ProgressiveProfiling = (props) => { const handleSkip = (e) => { e.preventDefault(); - window.history.replaceState(props.location.state, null, ''); + window.history.replaceState(location.state, null, ''); setShowModal(true); sendTrackEvent( 'edx.bi.welcome.page.skip.link.clicked', @@ -282,17 +283,6 @@ const ProgressiveProfiling = (props) => { }; ProgressiveProfiling.propTypes = { - location: PropTypes.shape({ - state: PropTypes.shape({ - registrationResult: PropTypes.shape({ - redirectUrl: PropTypes.string, - }), - optionalFields: PropTypes.shape({ - extended_profile: PropTypes.arrayOf(PropTypes.string), - fields: PropTypes.shape({}), - }), - }), - }), showError: PropTypes.bool, shouldRedirect: PropTypes.bool, submitState: PropTypes.string, @@ -308,7 +298,6 @@ ProgressiveProfiling.propTypes = { }; ProgressiveProfiling.defaultProps = { - location: { state: {} }, shouldRedirect: false, showError: false, submitState: DEFAULT_STATE, diff --git a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx index bee4775d..1014da35 100644 --- a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +++ b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx @@ -6,9 +6,8 @@ import { identifyAuthenticatedUser, sendTrackEvent } from '@edx/frontend-platfor import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; -import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; -import { MemoryRouter, Router } from 'react-router-dom'; +import { MemoryRouter, mockNavigate, useLocation } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { @@ -44,8 +43,22 @@ jest.mock('../../recommendations/optimizelyExperiment.js', () => ({ trackRecommendationViewedOptimizely: jest.fn(), RECOMMENDATIONS_EXP_VARIATION: 'welcome_page_recommendations_enabled', })); +jest.mock('react-router-dom', () => { + const mockNavigation = jest.fn(); -const history = createMemoryHistory(); + // eslint-disable-next-line react/prop-types + const Navigate = ({ to }) => { + mockNavigation(to); + return
; + }; + + return { + ...jest.requireActual('react-router-dom'), + Navigate, + mockNavigate: mockNavigation, + useLocation: jest.fn(), + }; +}); describe('ProgressiveProfilingTests', () => { mergeConfig({ @@ -63,7 +76,6 @@ describe('ProgressiveProfilingTests', () => { }; const extendedProfile = ['company']; const optionalFields = { fields, extended_profile: extendedProfile }; - let props = {}; let store = {}; const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); const initialState = { @@ -86,11 +98,7 @@ describe('ProgressiveProfilingTests', () => { ); const getProgressiveProfilingPage = async () => { - const progressiveProfilingPage = mount(reduxWrapper( - - - , - )); + const progressiveProfilingPage = mount(reduxWrapper()); await act(async () => { await Promise.resolve(progressiveProfilingPage); await new Promise(resolve => { setImmediate(resolve); }); @@ -110,14 +118,12 @@ describe('ProgressiveProfilingTests', () => { }, messages: { 'es-419': {}, de: {}, 'en-us': {} }, }); - props = { - location: { - state: { - registrationResult, - optionalFields, - }, + useLocation.mockReturnValue({ + state: { + registrationResult, + optionalFields, }, - }; + }); }); it('should not display button "Learn more about how we use this information."', async () => { @@ -225,7 +231,7 @@ describe('ProgressiveProfilingTests', () => { const progressiveProfilingPage = await getProgressiveProfilingPage(); expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Next'); - expect(history.location.pathname).toEqual(RECOMMENDATIONS); + expect(mockNavigate).toHaveBeenCalledWith(RECOMMENDATIONS); }); it('should fire segments recommendations viewed and variation group events', async () => { @@ -258,17 +264,15 @@ describe('ProgressiveProfilingTests', () => { it('should not redirect to recommendations page if user is on its way to enroll in a course', async () => { const redirectUrl = `${getConfig().LMS_BASE_URL}${DEFAULT_REDIRECT_URL}?enrollment_action=1`; - props = { - location: { - state: { - registrationResult: { - redirectUrl, - success: true, - }, - optionalFields, + useLocation.mockReturnValue({ + state: { + registrationResult: { + redirectUrl, + success: true, }, + optionalFields, }, - }; + }); store = mockStore({ ...initialState, @@ -293,7 +297,9 @@ describe('ProgressiveProfilingTests', () => { const host = 'http://example.com'; beforeEach(() => { - props = {}; + useLocation.mockReturnValue({ + state: {}, + }); store = mockStore({ ...initialState, commonComponents: { @@ -372,7 +378,6 @@ describe('ProgressiveProfilingTests', () => { href: getConfig().BASE_URL, search: `?variant=${EMBEDDED}&host=${host}&next=${redirectUrl}`, }; - props = {}; store = mockStore({ ...initialState, commonComponents: { diff --git a/src/recommendations/RecommendationsPage.jsx b/src/recommendations/RecommendationsPage.jsx index 65b92587..84079d93 100644 --- a/src/recommendations/RecommendationsPage.jsx +++ b/src/recommendations/RecommendationsPage.jsx @@ -8,6 +8,7 @@ import { } from '@edx/paragon'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; +import { useLocation } from 'react-router-dom'; import { POPULAR, TRENDING } from './data/constants'; import useProducts from './data/hooks/useProducts'; @@ -16,8 +17,9 @@ import RecommendationsList from './RecommendationsList'; import { trackRecommendationsViewed } from './track'; import { DEFAULT_REDIRECT_URL } from '../data/constants'; -const RecommendationsPage = ({ location, countryCode }) => { +const RecommendationsPage = ({ countryCode }) => { const { formatMessage } = useIntl(); + const location = useLocation(); const registrationResponse = location.state?.registrationResult; const userId = location.state?.userId; @@ -113,21 +115,9 @@ const RecommendationsPage = ({ location, countryCode }) => { }; RecommendationsPage.propTypes = { - location: PropTypes.shape({ - state: PropTypes.shape({ - registrationResult: PropTypes.shape({ - redirectUrl: PropTypes.string, - }), - userId: PropTypes.number, - }), - }), countryCode: PropTypes.string.isRequired, }; -RecommendationsPage.defaultProps = { - location: { state: {} }, -}; - const mapStateToProps = state => ({ countryCode: state.register.backendCountryCode, }); diff --git a/src/recommendations/tests/RecommendationsPage.test.jsx b/src/recommendations/tests/RecommendationsPage.test.jsx index 966809c7..7548587a 100644 --- a/src/recommendations/tests/RecommendationsPage.test.jsx +++ b/src/recommendations/tests/RecommendationsPage.test.jsx @@ -4,6 +4,7 @@ import { Provider } from 'react-redux'; import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; +import { useLocation } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { DEFAULT_REDIRECT_URL } from '../../data/constants'; @@ -19,6 +20,10 @@ jest.mock('../data/service', () => ({ __esModule: true, default: jest.fn(), })); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useLocation: jest.fn(), +})); describe('RecommendationsPageTests', () => { mergeConfig({ @@ -27,7 +32,6 @@ describe('RecommendationsPageTests', () => { TRENDING_PRODUCTS: '[]', }); - let defaultProps = {}; let store = {}; const dashboardUrl = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); @@ -43,20 +47,24 @@ describe('RecommendationsPageTests', () => { ); + const mockUseLocation = () => ( + useLocation.mockReturnValue({ + state: { + registrationResult, + userId: 111, + }, + }) + ); + beforeEach(() => { store = mockStore({ register: { backendCountryCode: 'PK', }, }); - defaultProps = { - location: { - state: { - registrationResult, - userId: 111, - }, - }, - }; + useLocation.mockReturnValue({ + state: {}, + }); }); it('should redirect to dashboard if user is not coming from registration workflow', () => { @@ -65,19 +73,22 @@ describe('RecommendationsPageTests', () => { }); it('should redirect if either popular or trending recommendations are not configured', () => { - mount(reduxWrapper()); + mockUseLocation(); + mount(reduxWrapper()); expect(window.location.href).toEqual(redirectUrl); }); it('should redirect user if they click "Skip for now" button', () => { - const recommendationsPage = mount(reduxWrapper()); + mockUseLocation(); + const recommendationsPage = mount(reduxWrapper()); recommendationsPage.find('.pgn__stateful-btn-state-default').first().simulate('click'); expect(window.location.href).toEqual(redirectUrl); }); it('displays popular products as default recommendations', () => { - const recommendationsPage = mount(reduxWrapper()); + mockUseLocation(); + const recommendationsPage = mount(reduxWrapper()); expect(recommendationsPage.find('.nav-link .active a').text()).toEqual('Most Popular'); }); }); diff --git a/src/register/tests/RegistrationPage.test.jsx b/src/register/tests/RegistrationPage.test.jsx index e6a9ff5e..435f73eb 100644 --- a/src/register/tests/RegistrationPage.test.jsx +++ b/src/register/tests/RegistrationPage.test.jsx @@ -7,8 +7,7 @@ import { configure, getLocale, injectIntl, IntlProvider, } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; -import { createMemoryHistory } from 'history'; -import { Router } from 'react-router-dom'; +import { mockNavigate, BrowserRouter as Router } from 'react-router-dom'; import renderer from 'react-test-renderer'; import configureStore from 'redux-mock-store'; @@ -40,7 +39,22 @@ jest.mock('@edx/frontend-platform/i18n', () => ({ const IntlRegistrationPage = injectIntl(RegistrationPage); const IntlRegistrationFailure = injectIntl(RegistrationFailureMessage); const mockStore = configureStore(); -const history = createMemoryHistory(); + +jest.mock('react-router-dom', () => { + const mockNavigation = jest.fn(); + + // eslint-disable-next-line react/prop-types + const Navigate = ({ to }) => { + mockNavigation(to); + return
; + }; + + return { + ...jest.requireActual('react-router-dom'), + Navigate, + mockNavigate: mockNavigation, + }; +}); describe('RegistrationPage', () => { mergeConfig({ @@ -72,6 +86,12 @@ describe('RegistrationPage', () => { ); + const routerWrapper = children => ( + + {children} + + ); + const thirdPartyAuthContext = { currentProvider: null, finishAuthUrl: null, @@ -174,7 +194,7 @@ describe('RegistrationPage', () => { }; store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); populateRequiredFields(registrationPage, payload); registrationPage.find('button.btn-brand').simulate('click'); expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); @@ -204,7 +224,7 @@ describe('RegistrationPage', () => { }, }); store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); populateRequiredFields(registrationPage, formPayload, true); registrationPage.find('button.btn-brand').simulate('click'); @@ -230,7 +250,7 @@ describe('RegistrationPage', () => { }; store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); populateRequiredFields(registrationPage, payload); registrationPage.find('button.btn-brand').simulate('click'); expect(store.dispatch).toHaveBeenCalledWith(registerNewUser({ ...payload, country: 'PK' })); @@ -243,7 +263,7 @@ describe('RegistrationPage', () => { it('should not dispatch registerNewUser on empty form Submission', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('button.btn-brand').simulate('click'); expect(store.dispatch).not.toHaveBeenCalledWith(registerNewUser({})); }); @@ -263,7 +283,7 @@ describe('RegistrationPage', () => { }); it('should show error messages for required fields on empty form submission', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('button.btn-brand').simulate('click'); expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name); @@ -277,7 +297,7 @@ describe('RegistrationPage', () => { }); it('should update errors for frontend validations', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#name').simulate('blur', { target: { value: 'http://test.com', name: 'name' } }); expect( @@ -294,7 +314,7 @@ describe('RegistrationPage', () => { registrationPage.find('div[feedback-for="username"]').text(), ).toContain( 'Usernames can only contain letters (A-Z, a-z), numerals (0-9),' - + ' underscores (_), and hyphens (-). Usernames cannot contain spaces', + + ' underscores (_), and hyphens (-). Usernames cannot contain spaces', ); registrationPage.find('input#email').simulate('blur', { target: { value: 'ab', name: 'email' } }); @@ -304,7 +324,7 @@ describe('RegistrationPage', () => { }); it('should validate fields on blur event', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#username').simulate('blur', { target: { value: '', name: 'username' } }); expect(registrationPage.find('div[feedback-for="username"]').text()).toEqual(emptyFieldValidation.username); @@ -324,7 +344,7 @@ describe('RegistrationPage', () => { it('should call validation api on blur event, if frontend validations have passed', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); // Enter a valid name so that frontend validations are passed registrationPage.find('input#name').simulate('change', { target: { value: 'John Doe', name: 'name' } }); @@ -342,7 +362,7 @@ describe('RegistrationPage', () => { }); it('should run validations for focused field on form submission', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input[name="country"]').simulate('focus'); registrationPage.find('button.btn-brand').simulate('click'); @@ -351,7 +371,7 @@ describe('RegistrationPage', () => { it('should give email suggestions for common service provider domain typos', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#email').simulate('change', { target: { value: 'john@yopmail.com', name: 'email' } }); registrationPage.find('input#email').simulate('blur'); @@ -360,7 +380,7 @@ describe('RegistrationPage', () => { }); it('should click on email suggestions for common service provider domain typos', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#email').simulate('change', { target: { value: 'john@yopmail.com', name: 'email' } }); registrationPage.find('input#email').simulate('blur'); @@ -370,7 +390,7 @@ describe('RegistrationPage', () => { it('should give error for common top level domain mistakes', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage .find('input#email') @@ -392,7 +412,7 @@ describe('RegistrationPage', () => { }, }, }); - const registrationPage = mount(reduxWrapper()).find('RegistrationPage'); + const registrationPage = mount(routerWrapper(reduxWrapper())).find('RegistrationPage'); expect(registrationPage.prop('backendValidations')).toEqual({ email: `This email is already associated with an existing or previous ${ getConfig().SITE_NAME } account`, username: 'It looks like this username is already taken', @@ -400,20 +420,20 @@ describe('RegistrationPage', () => { }); it('should remove space from the start of username', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#username').simulate('change', { target: { value: ' test-user', name: 'username' } }); expect(registrationPage.find('input#username').prop('value')).toEqual('test-user'); }); it('should remove extra character if username is more than 30 character long', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#username').simulate('change', { target: { value: 'why_this_is_not_valid_username_', name: 'username' } }); expect(registrationPage.find('input#username').prop('value')).toEqual(''); }); it('should give error with suggestion for common top level domain mistakes', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#email').simulate('change', { target: { value: 'ahtesham@hotmail', name: 'email' } }); registrationPage.find('input#email').simulate('blur'); @@ -423,7 +443,7 @@ describe('RegistrationPage', () => { it('should call backend validation api for password validation', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#password').simulate('change', { target: { value: 'aziz194@', name: 'password' } }); registrationPage.find('input#password').simulate('blur'); @@ -434,7 +454,7 @@ describe('RegistrationPage', () => { // ******** test field focus in functionality ******** it('should clear field related error messages on input field Focus', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('button.btn-brand').simulate('click'); expect(registrationPage.find('div[feedback-for="name"]').text()).toEqual(emptyFieldValidation.name); @@ -461,7 +481,7 @@ describe('RegistrationPage', () => { it('should clear username suggestions when username field is focused in', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#username').simulate('focus'); expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions()); @@ -484,7 +504,7 @@ describe('RegistrationPage', () => { const expectedMessage = `${'You\'ve successfully signed into Apple! We just need a little more information before ' + 'you start learning with '}${ getConfig().SITE_NAME }.`; - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('#tpa-alert').find('p').text()).toEqual(expectedMessage); }); @@ -545,7 +565,7 @@ describe('RegistrationPage', () => { // ******** test form buttons and fields ******** it('should match default button state', () => { - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('button[type="submit"] span').first().text()).toEqual('Create an account for free'); }); @@ -558,7 +578,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); const button = registrationPage.find('button[type="submit"] span').first(); expect(button.find('.sr-only').text()).toEqual('pending'); @@ -569,7 +589,7 @@ describe('RegistrationPage', () => { MARKETING_EMAILS_OPT_IN: 'true', }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('div.form-field--checkbox').length).toEqual(1); mergeConfig({ @@ -589,7 +609,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find(`button#${ssoProvider.id}`).length).toEqual(1); }); @@ -611,7 +631,7 @@ describe('RegistrationPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL }; - const root = mount(reduxWrapper()); + const root = mount(routerWrapper(reduxWrapper())); expect(root.text().includes('Institution/campus credentials')).toBe(true); mergeConfig({ @@ -639,7 +659,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('input#password').length).toEqual(0); }); @@ -654,7 +674,7 @@ describe('RegistrationPage', () => { }, }); - renderer.create(reduxWrapper()); + renderer.create(routerWrapper(reduxWrapper())); expect(document.cookie).toMatch(`${getConfig().REGISTER_CONVERSION_COOKIE_NAME}=true`); }); @@ -674,7 +694,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3); }); @@ -691,7 +711,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } }); expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3); @@ -710,7 +730,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } }); registrationPage.find('.username-suggestions--chip').first().simulate('click'); expect(registrationPage.find('input#username').props().value).toEqual('test_1'); @@ -730,7 +750,7 @@ describe('RegistrationPage', () => { }); store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#name').simulate('change', { target: { value: 'test name', name: 'name' } }); registrationPage.find('button.username-suggestions__close__button').at(0).simulate('click'); expect(store.dispatch).toHaveBeenCalledWith(clearUsernameSuggestions()); @@ -750,7 +770,7 @@ describe('RegistrationPage', () => { }); delete window.location; window.location = { href: getConfig().BASE_URL }; - renderer.create(reduxWrapper()); + renderer.create(routerWrapper(reduxWrapper())); expect(window.location.href).toBe(dashboardURL); }); @@ -773,7 +793,7 @@ describe('RegistrationPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL }; - const loginPage = mount(reduxWrapper()); + const loginPage = mount(routerWrapper(reduxWrapper())); loginPage.find('button#oa2-apple-id').simulate('click'); expect(window.location.href).toBe(getConfig().LMS_BASE_URL + registerUrl); @@ -801,7 +821,7 @@ describe('RegistrationPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL }; - renderer.create(reduxWrapper()); + renderer.create(routerWrapper(reduxWrapper())); expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl); }); @@ -822,7 +842,7 @@ describe('RegistrationPage', () => { }); delete window.location; window.location = { href: getConfig().BASE_URL }; - renderer.create(reduxWrapper()); + renderer.create(routerWrapper(reduxWrapper())); expect(window.location.href).toBe(dashboardUrl); }); @@ -851,12 +871,12 @@ describe('RegistrationPage', () => { }); const progressiveProfilingPage = mount(reduxWrapper( - + , )); progressiveProfilingPage.update(); - expect(history.location.pathname).toEqual(AUTHN_PROGRESSIVE_PROFILING); + expect(mockNavigate).toHaveBeenCalledWith(AUTHN_PROGRESSIVE_PROFILING); }); // ******** test hinted third party auth ******** @@ -877,7 +897,7 @@ describe('RegistrationPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find(`button#${ssoProvider.id}`).find('span').text()).toEqual(ssoProvider.name); expect(registrationPage.find(`button#${ssoProvider.id}`).hasClass(`btn-tpa btn-${ssoProvider.id}`)).toEqual(true); }); @@ -935,7 +955,7 @@ describe('RegistrationPage', () => { window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` }; ssoProvider.iconImage = null; - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find(`button#${ssoProvider.id}`).find('div').find('span').hasClass('pgn__icon')).toEqual(true); }); @@ -956,7 +976,7 @@ describe('RegistrationPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL.concat(REGISTER_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` }; - mount(reduxWrapper()); + mount(routerWrapper(reduxWrapper())); expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.registerUrl); }); @@ -977,7 +997,7 @@ describe('RegistrationPage', () => { delete window.location; window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' }; - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find(`button#${ssoProvider.id}`).find('span#provider-name').text()).toEqual(expectedMessage); }); @@ -993,12 +1013,12 @@ describe('RegistrationPage', () => { }); store.dispatch = jest.fn(store.dispatch); - mount(reduxWrapper()); + mount(routerWrapper(reduxWrapper())); expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData })); }); it('should send page event when register page is rendered', () => { - mount(reduxWrapper()); + mount(routerWrapper(reduxWrapper())); expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register'); }); @@ -1023,7 +1043,7 @@ describe('RegistrationPage', () => { }); store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()).find('RegistrationPage'); + const registrationPage = mount(routerWrapper(reduxWrapper())).find('RegistrationPage'); expect(registrationPage.find('input#email').props().value).toEqual('test@example.com'); expect(registrationPage.find('input#username').props().value).toEqual('test'); expect(store.dispatch).toHaveBeenCalledWith(setUserPipelineDataLoaded(true)); @@ -1038,7 +1058,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('input[name="country"]').props().value).toEqual('Pakistan'); }); @@ -1053,7 +1073,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()).find('RegistrationPage'); + const registrationPage = mount(routerWrapper(reduxWrapper())).find('RegistrationPage'); expect(registrationPage.find('div#validation-errors').first().text()).toContain( 'An error has occurred. Try refreshing the page, or check your internet connection.', ); @@ -1079,7 +1099,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()).find('RegistrationPage'); + const registrationPage = mount(routerWrapper(reduxWrapper())).find('RegistrationPage'); expect(registrationPage.find('input#name').props().value).toEqual('John Doe'); expect(registrationPage.find('input#username').props().value).toEqual('john_doe'); @@ -1091,7 +1111,7 @@ describe('RegistrationPage', () => { it('should set country in component state when form is translated used i18n', () => { getLocale.mockImplementation(() => ('ar-ae')); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input[name="country"]').simulate('click'); registrationPage.find('button.dropdown-item').at(0).simulate('click', { target: { value: 'أفغانستان ', name: 'countryItem' } }); expect(registrationPage.find('div[feedback-for="country"]').exists()).toBeFalsy(); @@ -1111,7 +1131,9 @@ describe('RegistrationPage', () => { store.dispatch = jest.fn(store.dispatch); const clearBackendError = jest.fn(); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper( + , + ))); registrationPage.find('input#email').simulate('change', { target: { value: 'a@gmail.com', name: 'email' } }); expect(registrationPage.find('div[feedback-for="email"]').exists()).toBeFalsy(); }); @@ -1137,7 +1159,7 @@ describe('RegistrationPage', () => { }, }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('#profession').exists()).toBeTruthy(); expect(registrationPage.find('#tos').exists()).toBeTruthy(); }); @@ -1168,7 +1190,7 @@ describe('RegistrationPage', () => { }; store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); populateRequiredFields(registrationPage, payload); registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } }); @@ -1197,7 +1219,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('button.btn-brand').simulate('click'); expect(registrationPage.find('#profession-error').last().text()).toEqual(professionError); @@ -1217,7 +1239,7 @@ describe('RegistrationPage', () => { }, }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#email').simulate('change', { target: { value: 'test1@gmail.com', name: 'email' } }); registrationPage.find('input#confirm_email').simulate('blur', { target: { value: 'test2@gmail.com', name: 'confirm_email' } }); expect(registrationPage.find('div#confirm_email-error').text()).toEqual('The email addresses do not match.'); @@ -1237,7 +1259,7 @@ describe('RegistrationPage', () => { }, }); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input#profession').simulate('focus', { target: { value: '', name: 'profession' } }); registrationPage.find('button.btn-brand').simulate('click'); @@ -1279,7 +1301,7 @@ describe('RegistrationPage', () => { }); store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('input#tos').props().value).toEqual(true); expect(registrationPage.find('input#honor-code').props().value).toEqual(true); @@ -1313,7 +1335,7 @@ describe('RegistrationPage', () => { }); store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('#tpa-spinner').exists()).toBeTruthy(); expect(registrationPage.find('#registration-form').exists()).toBeFalsy(); }); @@ -1343,7 +1365,7 @@ describe('RegistrationPage', () => { }); store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('#tpa-spinner').exists()).toBeFalsy(); expect(registrationPage.find('#registration-form').exists()).toBeTruthy(); }); @@ -1373,7 +1395,7 @@ describe('RegistrationPage', () => { store.dispatch = jest.fn(store.dispatch); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); expect(registrationPage.find('div.alert-heading').length).toEqual(1); expect(registrationPage.find('div.alert').first().text()).toContain('An error occured'); }); @@ -1381,7 +1403,7 @@ describe('RegistrationPage', () => { it('should not run country field validation when onBlur is fired by drop-down arrow icon click', () => { getLocale.mockImplementation(() => ('en-us')); - const registrationPage = mount(reduxWrapper()); + const registrationPage = mount(routerWrapper(reduxWrapper())); registrationPage.find('input[name="country"]').simulate('blur', { target: { value: '', name: 'country' }, relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' }, diff --git a/src/reset-password/ResetPasswordPage.jsx b/src/reset-password/ResetPasswordPage.jsx index 39afac4e..38da95a5 100644 --- a/src/reset-password/ResetPasswordPage.jsx +++ b/src/reset-password/ResetPasswordPage.jsx @@ -14,7 +14,7 @@ import { import { ChevronLeft } from '@edx/paragon/icons'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; -import { Redirect } from 'react-router-dom'; +import { useNavigate, useParams } from 'react-router-dom'; import { resetPassword, validateToken } from './data/actions'; import { @@ -39,7 +39,8 @@ const ResetPasswordPage = (props) => { const [confirmPassword, setConfirmPassword] = useState(''); const [formErrors, setFormErrors] = useState({}); const [errorCode, setErrorCode] = useState(null); - const [key, setKey] = useState(''); + const { token } = useParams(); + const navigate = useNavigate(); useEffect(() => { if (props.status !== TOKEN_STATE.PENDING && props.status !== PASSWORD_RESET_ERROR) { @@ -145,15 +146,14 @@ const ResetPasswordPage = (props) => { ); if (props.status === TOKEN_STATE.PENDING) { - const { token } = props.match.params; if (token) { props.validateToken(token); return ; } } else if (props.status === PASSWORD_RESET_ERROR) { - return ; + navigate(updatePathWithQueryParams(RESET_PAGE)); } else if (props.status === 'success') { - return ; + navigate(updatePathWithQueryParams(LOGIN_PAGE)); } else { return ( @@ -163,12 +163,9 @@ const ResetPasswordPage = (props) => { {formatMessage(messages['reset.password.page.title'], { siteName: getConfig().SITE_NAME })} - setKey(k)}> + navigate(updatePathWithQueryParams(key))}> - { key && ( - - )}
@@ -220,7 +217,6 @@ const ResetPasswordPage = (props) => { ResetPasswordPage.defaultProps = { status: null, token: null, - match: null, errorMsg: null, }; @@ -228,11 +224,6 @@ ResetPasswordPage.propTypes = { resetPassword: PropTypes.func.isRequired, validateToken: PropTypes.func.isRequired, token: PropTypes.string, - match: PropTypes.shape({ - params: PropTypes.shape({ - token: PropTypes.string, - }), - }), status: PropTypes.string, errorMsg: PropTypes.string, }; diff --git a/src/reset-password/tests/ResetPasswordPage.test.jsx b/src/reset-password/tests/ResetPasswordPage.test.jsx index facdb5a1..e565a5b4 100644 --- a/src/reset-password/tests/ResetPasswordPage.test.jsx +++ b/src/reset-password/tests/ResetPasswordPage.test.jsx @@ -3,23 +3,29 @@ import { Provider } from 'react-redux'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import { mount } from 'enzyme'; -import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; -import { MemoryRouter, Router } from 'react-router-dom'; +import { MemoryRouter } from 'react-router-dom'; import configureStore from 'redux-mock-store'; -import { LOGIN_PAGE } from '../../data/constants'; +import { LOGIN_PAGE, RESET_PAGE } from '../../data/constants'; import { resetPassword, validateToken } from '../data/actions'; import { PASSWORD_RESET, PASSWORD_RESET_ERROR, SUCCESS, TOKEN_STATE, } from '../data/constants'; import ResetPasswordPage from '../ResetPasswordPage'; +const mockedNavigator = jest.fn(); +const token = '1c-bmjdkc-5e60e084cf8113048ca7'; + jest.mock('@edx/frontend-platform/auth'); +jest.mock('react-router-dom', () => ({ + ...(jest.requireActual('react-router-dom')), + useNavigate: () => mockedNavigator, + useParams: jest.fn().mockReturnValue({ token }), +})); const IntlResetPasswordPage = injectIntl(ResetPasswordPage); const mockStore = configureStore(); -const history = createMemoryHistory(); describe('ResetPasswordPage', () => { let props = {}; @@ -188,36 +194,24 @@ describe('ResetPasswordPage', () => { props = { status: TOKEN_STATE.PENDING, - match: { - params: { token: '1c-bmjdkc-5e60e084cf8113048ca7' }, - }, }; mount(reduxWrapper()); - expect(store.dispatch).toHaveBeenCalledWith(validateToken(props.match.params.token)); + expect(store.dispatch).toHaveBeenCalledWith(validateToken(token)); }); it('should redirect the user to Reset password email screen ', async () => { props = { status: PASSWORD_RESET_ERROR, }; - mount(reduxWrapper( - - - , - - )); - expect(history.location.pathname).toEqual('/reset'); + mount(reduxWrapper()); + expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE); }); it('should redirect the user to root url of the application ', async () => { props = { status: SUCCESS, }; - mount(reduxWrapper( - - - , - )); - expect(history.location.pathname).toEqual('/login'); + mount(reduxWrapper()); + expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE); }); it('show spinner during token validation', () => { @@ -228,15 +222,11 @@ describe('ResetPasswordPage', () => { // ******** redirection tests ******** it('by clicking on sign in tab should redirect onto login page', async () => { - const resetPasswordPage = mount(reduxWrapper( - - - , - )); + const resetPasswordPage = mount(reduxWrapper()); await act(async () => { await resetPasswordPage.find('nav').find('a').first().simulate('click'); }); resetPasswordPage.update(); - expect(history.location.pathname).toEqual(LOGIN_PAGE); + expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE); }); });