feat: upgrade react router to v6 (#936)

This commit is contained in:
Syed Ali Abbas Zaidi
2023-08-15 17:23:34 +05:00
committed by GitHub
parent 9730a4f55d
commit efdefc300e
20 changed files with 299 additions and 334 deletions

108
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 = () => (
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Switch>
<Route exact path="/">
<Redirect to={updatePathWithQueryParams(REGISTER_PAGE)} />
</Route>
<EmbeddedRegistrationRoute
exact
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
component={RegistrationPage}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<UnAuthOnlyRoute exact path={LOGIN_PAGE} render={() => <Logistration selectedPage={LOGIN_PAGE} />} />
<UnAuthOnlyRoute exact path={REGISTER_PAGE} component={Logistration} />
<UnAuthOnlyRoute exact path={RESET_PAGE} component={ForgotPasswordPage} />
<Route exact path={PASSWORD_RESET_CONFIRM} component={ResetPasswordPage} />
<Route exact path={AUTHN_PROGRESSIVE_PROFILING} component={ProgressiveProfiling} />
<Route exact path={RECOMMENDATIONS} component={RecommendationsPage} />
<Route path={PAGE_NOT_FOUND} component={NotFoundPage} />
<Route path="*">
<Redirect to={PAGE_NOT_FOUND} />
</Route>
</Switch>
<Route
path={LOGIN_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
</AppProvider>
);

View File

@@ -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 <Route {...props} />;
return children;
}
return <Redirect to={PAGE_NOT_FOUND} />;
return <Navigate to={PAGE_NOT_FOUND} replace />;
};
EmbeddedRegistrationRoute.propTypes = {
path: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
export default EmbeddedRegistrationRoute;

View File

@@ -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 (
<Redirect to={{
pathname: AUTHN_PROGRESSIVE_PROFILING,
state: {
<Navigate
to={AUTHN_PROGRESSIVE_PROFILING}
state={{
registrationResult,
optionalFields,
},
}}
}}
replace
/>
);
}
@@ -64,14 +62,14 @@ const RedirectLogistration = (props) => {
if (redirectToRecommendationsPage) {
const registrationResult = { redirectUrl: finalRedirectUrl, success };
return (
<Redirect to={{
pathname: RECOMMENDATIONS,
state: {
<Navigate
to={RECOMMENDATIONS}
state={{
registrationResult,
educationLevel,
userId,
},
}}
}}
replace
/>
);
}

View File

@@ -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 <Route {...props} />;
return children;
}
return null;
};
UnAuthOnlyRoute.propTypes = {
path: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
export default UnAuthOnlyRoute;

View File

@@ -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 = () => (
<Router>
<div>
<Switch>
<EmbeddedRegistrationRoute path={REGISTER_EMBEDDED_PAGE} render={() => (<span>Embedded Register Page</span>)} />
</Switch>
<Routes>
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><span>Embedded Register Page</span></EmbeddedRegistrationRoute>}
/>
</Routes>
</div>
</Router>
);

View File

@@ -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 = () => (
<Router>
<div>
<Switch>
<UnAuthOnlyRoute path={REGISTER_PAGE} render={() => (<span>Register Page</span>)} />
</Switch>
<Routes>
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><span>Register Page</span></UnAuthOnlyRoute>} />
</Routes>
</div>
</Router>
);

View File

@@ -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) => {
</title>
</Helmet>
<div>
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}>
<Tabs activeKey="" id="controlled-tab" onSelect={(key) => navigate(updatePathWithQueryParams(key))}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
</Tabs>
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
<Form id="forget-password-form" name="forget-password-form" className="mw-xs">
<ForgotPasswordAlert email={bannerEmail} emailError={formErrors} status={status} />

View File

@@ -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(
<Router history={history}>
<IntlForgotPasswordPage {...props} />
</Router>,
));
const forgotPasswordPage = mount(reduxWrapper(<IntlForgotPasswordPage {...props} />));
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);
});
});

View File

@@ -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 <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
}
return (
<ModalDialog
title="Password security"

View File

@@ -3,15 +3,19 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { 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 { RESET_PAGE } from '../../data/constants';
import ChangePasswordPrompt from '../ChangePasswordPrompt';
const IntlChangePasswordPrompt = injectIntl(ChangePasswordPrompt);
const history = createMemoryHistory();
const mockedNavigator = jest.fn();
jest.mock('react-router-dom', () => ({
...(jest.requireActual('react-router-dom')),
useNavigate: () => mockedNavigator,
}));
describe('ChangePasswordPromptTests', () => {
let props = {};
@@ -55,9 +59,7 @@ describe('ChangePasswordPromptTests', () => {
const changePasswordPrompt = mount(
<IntlProvider locale="en">
<MemoryRouter>
<Router history={history}>
<IntlChangePasswordPrompt {...props} />
</Router>
<IntlChangePasswordPrompt {...props} />
</MemoryRouter>
</IntlProvider>,
);
@@ -67,6 +69,6 @@ describe('ChangePasswordPromptTests', () => {
});
changePasswordPrompt.update();
expect(history.location.pathname).toEqual(RESET_PAGE);
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
});
});

View File

@@ -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
? (
<>
<Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} />
{institutionLogin && (
<Tabs defaultActiveKey="" id="controlled-tab" onSelect={handleInstitutionLogin}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
@@ -116,9 +121,6 @@ const Logistration = (props) => {
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
))}
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
{selectedPage === LOGIN_PAGE
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />

View File

@@ -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,

View File

@@ -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 <div />;
};
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(
<Router history={history}>
<IntlProgressiveProfilingPage {...props} />
</Router>,
));
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage />));
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: {

View File

@@ -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,
});

View File

@@ -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', () => {
</IntlProvider>
);
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(<IntlRecommendationsPage {...defaultProps} />));
mockUseLocation();
mount(reduxWrapper(<IntlRecommendationsPage />));
expect(window.location.href).toEqual(redirectUrl);
});
it('should redirect user if they click "Skip for now" button', () => {
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...defaultProps} />));
mockUseLocation();
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
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(<IntlRecommendationsPage {...defaultProps} />));
mockUseLocation();
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage />));
expect(recommendationsPage.find('.nav-link .active a').text()).toEqual('Most Popular');
});
});

View File

@@ -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 <div />;
};
return {
...jest.requireActual('react-router-dom'),
Navigate,
mockNavigate: mockNavigation,
};
});
describe('RegistrationPage', () => {
mergeConfig({
@@ -72,6 +86,12 @@ describe('RegistrationPage', () => {
</IntlProvider>
);
const routerWrapper = children => (
<Router>
{children}
</Router>
);
const thirdPartyAuthContext = {
currentProvider: null,
finishAuthUrl: null,
@@ -174,7 +194,7 @@ describe('RegistrationPage', () => {
};
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
// 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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
registrationPage
.find('input#email')
@@ -392,7 +412,7 @@ describe('RegistrationPage', () => {
},
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(registrationPage.find('div.form-field--checkbox').length).toEqual(1);
mergeConfig({
@@ -589,7 +609,7 @@ describe('RegistrationPage', () => {
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const root = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(root.text().includes('Institution/campus credentials')).toBe(true);
mergeConfig({
@@ -639,7 +659,7 @@ describe('RegistrationPage', () => {
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(registrationPage.find('input#password').length).toEqual(0);
});
@@ -654,7 +674,7 @@ describe('RegistrationPage', () => {
},
});
renderer.create(reduxWrapper(<IntlRegistrationPage {...props} />));
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(document.cookie).toMatch(`${getConfig().REGISTER_CONVERSION_COOKIE_NAME}=true`);
});
@@ -674,7 +694,7 @@ describe('RegistrationPage', () => {
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(registrationPage.find('button.username-suggestions--chip').length).toEqual(3);
});
@@ -691,7 +711,7 @@ describe('RegistrationPage', () => {
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const loginPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
renderer.create(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(window.location.href).toBe(dashboardUrl);
});
@@ -851,12 +871,12 @@ describe('RegistrationPage', () => {
});
const progressiveProfilingPage = mount(reduxWrapper(
<Router history={history}>
<Router>
<IntlRegistrationPage {...props} />
</Router>,
));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationFormBegin({ ...registrationFormData }));
});
it('should send page event when register page is rendered', () => {
mount(reduxWrapper(<IntlRegistrationPage {...props} />));
mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'register');
});
@@ -1023,7 +1043,7 @@ describe('RegistrationPage', () => {
});
store.dispatch = jest.fn(store.dispatch);
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
expect(registrationPage.find('input[name="country"]').props().value).toEqual('Pakistan');
});
@@ -1053,7 +1073,7 @@ describe('RegistrationPage', () => {
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).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(<IntlRegistrationPage {...props} />)).find('RegistrationPage');
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />))).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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} {...clearBackendError} />));
const registrationPage = mount(routerWrapper(reduxWrapper(
<IntlRegistrationPage {...props} {...clearBackendError} />,
)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
populateRequiredFields(registrationPage, payload);
registrationPage.find('input#profession').simulate('change', { target: { value: 'Engineer', name: 'profession' } });
@@ -1197,7 +1219,7 @@ describe('RegistrationPage', () => {
},
});
const registrationPage = mount(reduxWrapper(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
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(<IntlRegistrationPage {...props} />));
const registrationPage = mount(routerWrapper(reduxWrapper(<IntlRegistrationPage {...props} />)));
registrationPage.find('input[name="country"]').simulate('blur', {
target: { value: '', name: 'country' },
relatedTarget: { type: 'button', className: 'btn-icon pgn__form-autosuggest__icon-button' },

View File

@@ -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 <Spinner animation="border" variant="primary" className="spinner--position-centered" />;
}
} else if (props.status === PASSWORD_RESET_ERROR) {
return <Redirect to={updatePathWithQueryParams(RESET_PAGE)} />;
navigate(updatePathWithQueryParams(RESET_PAGE));
} else if (props.status === 'success') {
return <Redirect to={updatePathWithQueryParams(LOGIN_PAGE)} />;
navigate(updatePathWithQueryParams(LOGIN_PAGE));
} else {
return (
<BaseContainer>
@@ -163,12 +163,9 @@ const ResetPasswordPage = (props) => {
{formatMessage(messages['reset.password.page.title'], { siteName: getConfig().SITE_NAME })}
</title>
</Helmet>
<Tabs activeKey="" id="controlled-tab" onSelect={(k) => setKey(k)}>
<Tabs activeKey="" id="controlled-tab" onSelect={(key) => navigate(updatePathWithQueryParams(key))}>
<Tab title={tabTitle} eventKey={LOGIN_PAGE} />
</Tabs>
{ key && (
<Redirect to={updatePathWithQueryParams(key)} />
)}
<div id="main-content" className="main-content">
<div className="mw-xs">
<ResetPasswordFailure errorCode={errorCode} errorMsg={props.errorMsg} />
@@ -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,
};

View File

@@ -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(<IntlResetPasswordPage {...props} />));
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(
<Router history={history}>
<IntlResetPasswordPage {...props} />
</Router>,
));
expect(history.location.pathname).toEqual('/reset');
mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
expect(mockedNavigator).toHaveBeenCalledWith(RESET_PAGE);
});
it('should redirect the user to root url of the application ', async () => {
props = {
status: SUCCESS,
};
mount(reduxWrapper(
<Router history={history}>
<IntlResetPasswordPage {...props} />
</Router>,
));
expect(history.location.pathname).toEqual('/login');
mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
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(
<Router history={history}>
<IntlResetPasswordPage {...props} />
</Router>,
));
const resetPasswordPage = mount(reduxWrapper(<IntlResetPasswordPage {...props} />));
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);
});
});