diff --git a/package-lock.json b/package-lock.json index fc1eecf9cf..3d09753c9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "@fortawesome/free-brands-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/frontend-plugin-framework": "^1.4.0", "@openedx/paragon": "^22.1.1", "@optimizely/react-sdk": "^2.9.1", "@redux-devtools/extension": "3.3.0", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^8.0.1", "algoliasearch": "^4.14.3", "algoliasearch-helper": "^3.14.0", "classnames": "2.5.1", @@ -3477,6 +3477,39 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@openedx/frontend-plugin-framework": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.4.0.tgz", + "integrity": "sha512-cwwjvGjYnzQdyhw2PuMYfaZoZGclXTzYfR3NjZ8bSLkj0f7lRB64nMy6WUmBjTB6hjaD4vxMs0zahrU4IM7EWQ==", + "license": "AGPL-3.0", + "dependencies": { + "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", + "classnames": "^2.3.2", + "core-js": "3.37.1", + "react-redux": "7.2.9", + "redux": "4.2.1", + "regenerator-runtime": "0.14.1" + }, + "peerDependencies": { + "@edx/frontend-platform": "^7.0.0 || ^8.0.0", + "@openedx/paragon": "^21.0.0 || ^22.0.0", + "prop-types": "^15.8.0", + "react": "^17.0.0", + "react-dom": "^17.0.0", + "react-error-boundary": "^4.0.11" + } + }, + "node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/@openedx/paragon": { "version": "22.13.0", "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.13.0.tgz", @@ -4125,34 +4158,6 @@ "react-dom": "<18.0.0" } }, - "node_modules/@testing-library/react-hooks": { - "version": "8.0.1", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5", - "react-error-boundary": "^3.1.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0", - "react": "^16.9.0 || ^17.0.0", - "react-dom": "^16.9.0 || ^17.0.0", - "react-test-renderer": "^16.9.0 || ^17.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "react-test-renderer": { - "optional": true - } - } - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "license": "MIT", @@ -13515,15 +13520,14 @@ } }, "node_modules/react-error-boundary": { - "version": "3.1.4", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz", + "integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.12.5" }, - "engines": { - "node": ">=10", - "npm": ">=6" - }, "peerDependencies": { "react": ">=16.13.1" } @@ -13855,7 +13859,7 @@ }, "node_modules/react-shallow-renderer": { "version": "16.15.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -13906,7 +13910,7 @@ }, "node_modules/react-test-renderer": { "version": "17.0.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -13920,7 +13924,7 @@ }, "node_modules/react-test-renderer/node_modules/react-is": { "version": "17.0.2", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/react-transition-group": { diff --git a/package.json b/package.json index 515c9fffac..6e8edc2d92 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "@fortawesome/free-brands-svg-icons": "6.7.2", "@fortawesome/free-solid-svg-icons": "6.7.2", "@fortawesome/react-fontawesome": "0.2.2", + "@openedx/frontend-plugin-framework": "^1.4.0", "@openedx/paragon": "^22.1.1", "@optimizely/react-sdk": "^2.9.1", "@redux-devtools/extension": "3.3.0", "@testing-library/react": "^12.1.5", - "@testing-library/react-hooks": "^8.0.1", "algoliasearch": "^4.14.3", "algoliasearch-helper": "^3.14.0", "classnames": "2.5.1", diff --git a/src/login/LoginPage.jsx b/src/login/LoginPage.jsx index 6281572675..db0e98f3ea 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -1,26 +1,17 @@ -import React, { useEffect, useMemo, useState } from 'react'; -import { connect } from 'react-redux'; +import React, { + useCallback, useEffect, useMemo, useState, +} from 'react'; +import { useDispatch, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { injectIntl, useIntl } from '@edx/frontend-platform/i18n'; -import { - Form, StatefulButton, -} from '@openedx/paragon'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import { Form, StatefulButton } from '@openedx/paragon'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import Skeleton from 'react-loading-skeleton'; import { Link } from 'react-router-dom'; -import AccountActivationMessage from './AccountActivationMessage'; -import { - backupLoginFormBegin, - dismissPasswordResetBanner, - loginRequest, -} from './data/actions'; -import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants'; -import LoginFailureMessage from './LoginFailure'; -import messages from './messages'; import { FormGroup, InstitutionLogistration, @@ -28,13 +19,12 @@ import { RedirectLogistration, ThirdPartyAuthAlert, } from '../common-components'; +import AccountActivationMessage from './AccountActivationMessage'; import { getThirdPartyAuthContext } from '../common-components/data/actions'; import { thirdPartyAuthContextSelector } from '../common-components/data/selectors'; import EnterpriseSSO from '../common-components/EnterpriseSSO'; import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; -import { - DEFAULT_STATE, PENDING_STATE, RESET_PAGE, -} from '../data/constants'; +import { PENDING_STATE, RESET_PAGE } from '../data/constants'; import { getActivationStatus, getAllPossibleQueryParams, @@ -43,37 +33,57 @@ import { updatePathWithQueryParams, } from '../data/utils'; import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; +import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from './data/actions'; +import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants'; +import LoginFailureMessage from './LoginFailure'; +import messages from './messages'; -const LoginPage = (props) => { +const LoginPage = ({ + institutionLogin, + handleInstitutionLogin, +}) => { + const dispatch = useDispatch(); + const backupFormState = useCallback((data) => dispatch(backupLoginFormBegin(data)), [dispatch]); + const getTPADataFromBackend = useCallback(() => dispatch(getThirdPartyAuthContext()), [dispatch]); const { backedUpFormData, loginErrorCode, loginErrorContext, loginResult, shouldBackupState, - thirdPartyAuthContext: { - providers, - currentProvider, - secondaryProviders, - finishAuthUrl, - platformName, - errorMessage: thirdPartyErrorMessage, - }, - thirdPartyAuthApiStatus, - institutionLogin, showResetPasswordSuccessBanner, submitState, - // Actions - backupFormState, - handleInstitutionLogin, - getTPADataFromBackend, - } = props; + thirdPartyAuthContext, + thirdPartyAuthApiStatus, + } = useSelector((state) => ({ + backedUpFormData: state.login.loginFormData, + loginErrorCode: state.login.loginErrorCode, + loginErrorContext: state.login.loginErrorContext, + loginResult: state.login.loginResult, + shouldBackupState: state.login.shouldBackupState, + showResetPasswordSuccessBanner: state.login.showResetPasswordSuccessBanner, + submitState: state.login.submitState, + thirdPartyAuthContext: thirdPartyAuthContextSelector(state), + thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus, + })); + const { + providers, + currentProvider, + secondaryProviders, + finishAuthUrl, + platformName, + errorMessage: thirdPartyErrorMessage, + } = thirdPartyAuthContext; const { formatMessage } = useIntl(); const activationMsgType = getActivationStatus(); const queryParams = useMemo(() => getAllPossibleQueryParams(), []); const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields }); - const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} }); + const [errorCode, setErrorCode] = useState({ + type: '', + count: 0, + context: {}, + }); const [errors, setErrors] = useState({ ...backedUpFormData.errors }); const tpaHint = getTpaHint(); @@ -87,7 +97,7 @@ const LoginPage = (props) => { payload.tpa_hint = tpaHint; } getTPADataFromBackend(payload); - }, [getTPADataFromBackend, queryParams, tpaHint]); + }, [queryParams, tpaHint, getTPADataFromBackend]); /** * Backup the login form in redux when login page is toggled. */ @@ -98,7 +108,7 @@ const LoginPage = (props) => { errors: { ...errors }, }); } - }, [shouldBackupState, formFields, errors, backupFormState]); + }, [backupFormState, shouldBackupState, formFields, errors]); useEffect(() => { if (loginErrorCode) { @@ -123,7 +133,10 @@ const LoginPage = (props) => { }, [thirdPartyErrorMessage]); const validateFormFields = (payload) => { - const { emailOrUsername, password } = payload; + const { + emailOrUsername, + password, + } = payload; const fieldErrors = { ...errors }; if (emailOrUsername === '') { @@ -141,14 +154,18 @@ const LoginPage = (props) => { const handleSubmit = (event) => { event.preventDefault(); if (showResetPasswordSuccessBanner) { - props.dismissPasswordResetBanner(); + dispatch(dismissPasswordResetBanner()); } const formData = { ...formFields }; const validationErrors = validateFormFields(formData); if (validationErrors.emailOrUsername || validationErrors.password) { setErrors({ ...validationErrors }); - setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} })); + setErrorCode(prevState => ({ + type: INVALID_FORM, + count: prevState.count + 1, + context: {}, + })); return; } @@ -158,23 +175,35 @@ const LoginPage = (props) => { password: formData.password, ...queryParams, }; - props.loginRequest(payload); + dispatch(loginRequest(payload)); }; const handleOnChange = (event) => { - const { name, value } = event.target; - setFormFields(prevState => ({ ...prevState, [name]: value })); + const { + name, + value, + } = event.target; + setFormFields(prevState => ({ + ...prevState, + [name]: value, + })); }; const handleOnFocus = (event) => { const { name } = event.target; - setErrors(prevErrors => ({ ...prevErrors, [name]: '' })); + setErrors(prevErrors => ({ + ...prevErrors, + [name]: '', + })); }; const trackForgotPasswordLinkClick = () => { sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' }); }; - const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders); + const { + provider, + skipHintedLogin, + } = getTpaProvider(tpaHint, providers, secondaryProviders); if (tpaHint) { if (thirdPartyAuthApiStatus === PENDING_STATE) { @@ -281,88 +310,9 @@ const LoginPage = (props) => { ); }; -const mapStateToProps = state => { - const loginPageState = state.login; - return { - backedUpFormData: loginPageState.loginFormData, - loginErrorCode: loginPageState.loginErrorCode, - loginErrorContext: loginPageState.loginErrorContext, - loginResult: loginPageState.loginResult, - shouldBackupState: loginPageState.shouldBackupState, - showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner, - submitState: loginPageState.submitState, - thirdPartyAuthContext: thirdPartyAuthContextSelector(state), - thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus, - }; -}; - LoginPage.propTypes = { - backedUpFormData: PropTypes.shape({ - formFields: PropTypes.shape({}), - errors: PropTypes.shape({}), - }), - loginErrorCode: PropTypes.string, - loginErrorContext: PropTypes.shape({ - email: PropTypes.string, - redirectUrl: PropTypes.string, - context: PropTypes.shape({}), - }), - loginResult: PropTypes.shape({ - redirectUrl: PropTypes.string, - success: PropTypes.bool, - }), - shouldBackupState: PropTypes.bool, - showResetPasswordSuccessBanner: PropTypes.bool, - submitState: PropTypes.string, - thirdPartyAuthApiStatus: PropTypes.string, institutionLogin: PropTypes.bool.isRequired, - thirdPartyAuthContext: PropTypes.shape({ - currentProvider: PropTypes.string, - errorMessage: PropTypes.string, - platformName: PropTypes.string, - providers: PropTypes.arrayOf(PropTypes.shape({})), - secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})), - finishAuthUrl: PropTypes.string, - }), - // Actions - backupFormState: PropTypes.func.isRequired, - dismissPasswordResetBanner: PropTypes.func.isRequired, - loginRequest: PropTypes.func.isRequired, - getTPADataFromBackend: PropTypes.func.isRequired, handleInstitutionLogin: PropTypes.func.isRequired, }; -LoginPage.defaultProps = { - backedUpFormData: { - formFields: { - emailOrUsername: '', password: '', - }, - errors: { - emailOrUsername: '', password: '', - }, - }, - loginErrorCode: null, - loginErrorContext: {}, - loginResult: {}, - shouldBackupState: false, - showResetPasswordSuccessBanner: false, - submitState: DEFAULT_STATE, - thirdPartyAuthApiStatus: PENDING_STATE, - thirdPartyAuthContext: { - currentProvider: null, - errorMessage: null, - finishAuthUrl: null, - providers: [], - secondaryProviders: [], - }, -}; - -export default connect( - mapStateToProps, - { - backupFormState: backupLoginFormBegin, - dismissPasswordResetBanner, - loginRequest, - getTPADataFromBackend: getThirdPartyAuthContext, - }, -)(injectIntl(LoginPage)); +export default LoginPage; diff --git a/src/login/tests/LoginPage.test.jsx b/src/login/tests/LoginPage.test.jsx index 9c337bf25e..a337d4b851 100644 --- a/src/login/tests/LoginPage.test.jsx +++ b/src/login/tests/LoginPage.test.jsx @@ -43,6 +43,14 @@ describe('LoginPage', () => { const initialState = { login: { loginResult: { success: false, redirectUrl: '' }, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, commonComponents: { thirdPartyAuthApiStatus: null, diff --git a/src/logistration/Logistration.jsx b/src/logistration/Logistration.jsx index 9451aa2745..d93366ea11 100644 --- a/src/logistration/Logistration.jsx +++ b/src/logistration/Logistration.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { connect } from 'react-redux'; +import { useDispatch, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; @@ -24,16 +24,20 @@ import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { getTpaHint, getTpaProvider, updatePathWithQueryParams, } from '../data/utils'; -import { LoginPage } from '../login'; import { backupLoginForm } from '../login/data/actions'; +import LoginPageSlot from '../plugin-slots/LoginPage'; import { RegistrationPage } from '../register'; import { backupRegistrationForm } from '../register/data/actions'; -const Logistration = (props) => { - const { selectedPage, tpaProviders } = props; +const Logistration = ({ + selectedPage, +}) => { const tpaHint = getTpaHint(); + const tpaProviders = useSelector(tpaProvidersSelector); + const dispatch = useDispatch(); const { - providers, secondaryProviders, + providers, + secondaryProviders, } = tpaProviders; const { formatMessage } = useIntl(); const [institutionLogin, setInstitutionLogin] = useState(false); @@ -45,7 +49,8 @@ const Logistration = (props) => { useEffect(() => { const authService = getAuthService(); if (authService) { - authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL); + authService.getCsrfTokenService() + .getCsrfToken(getConfig().LMS_BASE_URL); } }); @@ -71,11 +76,11 @@ const Logistration = (props) => { return; } sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' }); - props.clearThirdPartyAuthContextErrorMessage(); + dispatch(clearThirdPartyAuthContextErrorMessage()); if (tabKey === LOGIN_PAGE) { - props.backupRegistrationForm(); + dispatch(backupRegistrationForm()); } else if (tabKey === REGISTER_PAGE) { - props.backupLoginForm(); + dispatch(backupLoginForm()); } setKey(tabKey); }; @@ -111,7 +116,7 @@ const Logistration = (props) => { {!institutionLogin && (

{formatMessage(messages['logistration.sign.in'])}

)} - + ) @@ -124,12 +129,16 @@ const Logistration = (props) => { ) : (!isValidTpaHint() && !hideRegistrationLink && ( - handleOnSelect(tabKey, selectedPage)}> + handleOnSelect(tabKey, selectedPage)} + > ))} - { key && ( + {key && ( )}
@@ -139,7 +148,12 @@ const Logistration = (props) => { )} {selectedPage === LOGIN_PAGE - ? + ? ( + + ) : ( { Logistration.propTypes = { selectedPage: PropTypes.string, - backupLoginForm: PropTypes.func.isRequired, - backupRegistrationForm: PropTypes.func.isRequired, - clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired, - tpaProviders: PropTypes.shape({ - providers: PropTypes.arrayOf(PropTypes.shape({})), - secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})), - }), -}; - -Logistration.defaultProps = { - tpaProviders: { - providers: [], - secondaryProviders: [], - }, }; Logistration.defaultProps = { selectedPage: REGISTER_PAGE, }; -const mapStateToProps = state => ({ - tpaProviders: tpaProvidersSelector(state), -}); - -export default connect( - mapStateToProps, - { - backupLoginForm, - backupRegistrationForm, - clearThirdPartyAuthContextErrorMessage, - }, -)(Logistration); +export default Logistration; diff --git a/src/logistration/Logistration.test.jsx b/src/logistration/Logistration.test.jsx index 87bf3e705e..80d5b44127 100644 --- a/src/logistration/Logistration.test.jsx +++ b/src/logistration/Logistration.test.jsx @@ -3,16 +3,14 @@ import { Provider } from 'react-redux'; import { getConfig, mergeConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; +import { configure, IntlProvider } from '@edx/frontend-platform/i18n'; import { fireEvent, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import Logistration from './Logistration'; import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions'; -import { - COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE, -} from '../data/constants'; +import { COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE } from '../data/constants'; import { backupLoginForm } from '../login/data/actions'; import { backupRegistrationForm } from '../register/data/actions'; @@ -23,11 +21,20 @@ jest.mock('@edx/frontend-platform/analytics', () => ({ jest.mock('@edx/frontend-platform/auth'); const mockStore = configureStore(); -const IntlLogistration = injectIntl(Logistration); +let store = {}; +const getComponent = (props) => ( + + + + + + + +); + +const renderComponent = (props = {}) => render(getComponent(props)); describe('Logistration', () => { - let store = {}; - const secondaryProviders = { id: 'saml-test', name: 'Test University', @@ -35,14 +42,6 @@ describe('Logistration', () => { registerUrl: '/dummy_auth', }; - const reduxWrapper = children => ( - - - {children} - - - ); - const initialState = { register: { registrationFormData: { @@ -50,16 +49,26 @@ describe('Logistration', () => { marketingEmailsOptIn: true, }, formFields: { - name: '', email: '', username: '', password: '', + name: '', + email: '', + username: '', + password: '', }, emailSuggestion: { - suggestion: '', type: '', + suggestion: '', + type: '', }, errors: { - name: '', email: '', username: '', password: '', + name: '', + email: '', + username: '', + password: '', }, }, - registrationResult: { success: false, redirectUrl: '' }, + registrationResult: { + success: false, + redirectUrl: '', + }, registrationError: {}, usernameSuggestions: [], validationApiRateLimited: false, @@ -71,7 +80,18 @@ describe('Logistration', () => { }, }, login: { - loginResult: { success: false, redirectUrl: '' }, + loginResult: { + success: false, + redirectUrl: '', + }, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, }; @@ -90,16 +110,22 @@ describe('Logistration', () => { ENVIRONMENT: 'production', LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum', }, - messages: { 'es-419': {}, de: {}, 'en-us': {} }, + messages: { + 'es-419': {}, + de: {}, + 'en-us': {}, + }, }); }); it('should do nothing when user clicks on the same tab (login/register) again', () => { - const { container } = render(reduxWrapper()); + const { container } = renderComponent(); // While staying on the registration form, clicking the register tab again fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); - expect(sendTrackEvent).not.toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' }); + expect(sendTrackEvent) + .not + .toHaveBeenCalledWith('edx.bi.register_form.toggled', { category: 'user-engagement' }); }); it('should render registration page', () => { @@ -107,16 +133,18 @@ describe('Logistration', () => { ALLOW_PUBLIC_ACCOUNT_CREATION: true, }); - const { container } = render(reduxWrapper()); + const { container } = renderComponent(); - expect(container.querySelector('RegistrationPage')).toBeDefined(); + expect(container.querySelector('RegistrationPage')) + .toBeDefined(); }); it('should render login page', () => { const props = { selectedPage: LOGIN_PAGE }; - const { container } = render(reduxWrapper()); + const { container } = renderComponent(props); - expect(container.querySelector('LoginPage')).toBeDefined(); + expect(container.querySelector('LoginPage')) + .toBeDefined(); }); it('should render login/register headings when show registration links is disabled', () => { @@ -125,18 +153,20 @@ describe('Logistration', () => { }); let props = { selectedPage: LOGIN_PAGE }; - const { rerender } = render(reduxWrapper()); + const { rerender } = renderComponent(props); // verifying sign in heading - expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in'); + expect(screen.getByRole('heading', { level: 3 }).textContent) + .toEqual('Sign in'); // register page is still accessible when SHOW_REGISTRATION_LINKS is false // but it needs to be accessed directly props = { selectedPage: REGISTER_PAGE }; - rerender(reduxWrapper()); + rerender(getComponent(props)); // verifying register heading - expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register'); + expect(screen.getByRole('heading', { level: 3 }).textContent) + .toEqual('Register'); }); it('should render only login page when public account creation is disabled', () => { @@ -160,14 +190,16 @@ describe('Logistration', () => { }); const props = { selectedPage: LOGIN_PAGE }; - const { container } = render(reduxWrapper()); + const { container } = renderComponent(props); // verifying sign in heading for institution login false - expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in'); + expect(screen.getByRole('heading', { level: 3 }).textContent) + .toEqual('Sign in'); // verifying tabs heading for institution login true fireEvent.click(screen.getByRole('link')); - expect(container.querySelector('#controlled-tab')).toBeDefined(); + expect(container.querySelector('#controlled-tab')) + .toBeDefined(); }); it('should display institution login option when secondary providers are present', () => { @@ -190,12 +222,14 @@ describe('Logistration', () => { }); const props = { selectedPage: LOGIN_PAGE }; - render(reduxWrapper()); - expect(screen.getByText('Institution/campus credentials')).toBeDefined(); + renderComponent(props); + expect(screen.getByText('Institution/campus credentials')) + .toBeDefined(); // on clicking "Institution/campus credentials" button, it should display institution login page fireEvent.click(screen.getByText('Institution/campus credentials')); - expect(screen.getByText('Test University')).toBeDefined(); + expect(screen.getByText('Test University')) + .toBeDefined(); mergeConfig({ DISABLE_ENTERPRISE_LOGIN: '', @@ -221,11 +255,13 @@ describe('Logistration', () => { }); const props = { selectedPage: LOGIN_PAGE }; - render(reduxWrapper()); + renderComponent(props); fireEvent.click(screen.getByText('Institution/campus credentials')); - expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); - expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login'); + expect(sendTrackEvent) + .toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' }); + expect(sendPageEvent) + .toHaveBeenCalledWith('login_and_registration', 'institution_login'); mergeConfig({ DISABLE_ENTERPRISE_LOGIN: '', @@ -251,11 +287,15 @@ describe('Logistration', () => { }); delete window.location; - window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL }; + window.location = { + hostname: getConfig().SITE_NAME, + href: getConfig().BASE_URL, + }; - render(reduxWrapper()); + renderComponent(); fireEvent.click(screen.getByText('Institution/campus credentials')); - expect(screen.getByText('Test University')).toBeDefined(); + expect(screen.getByText('Test University')) + .toBeDefined(); mergeConfig({ DISABLE_ENTERPRISE_LOGIN: '', @@ -264,23 +304,26 @@ describe('Logistration', () => { it('should fire action to backup registration form on tab click', () => { store.dispatch = jest.fn(store.dispatch); - const { container } = render(reduxWrapper()); + const { container } = renderComponent(); fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); - expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm()); + expect(store.dispatch) + .toHaveBeenCalledWith(backupRegistrationForm()); }); it('should fire action to backup login form on tab click', () => { store.dispatch = jest.fn(store.dispatch); const props = { selectedPage: LOGIN_PAGE }; - const { container } = render(reduxWrapper()); + const { container } = renderComponent(props); fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]')); - expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm()); + expect(store.dispatch) + .toHaveBeenCalledWith(backupLoginForm()); }); it('should clear tpa context errorMessage tab click', () => { store.dispatch = jest.fn(store.dispatch); - const { container } = render(reduxWrapper()); + const { container } = renderComponent(); fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]')); - expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage()); + expect(store.dispatch) + .toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage()); }); }); diff --git a/src/plugin-slots/LoginPage/index.jsx b/src/plugin-slots/LoginPage/index.jsx new file mode 100644 index 0000000000..ac77c37455 --- /dev/null +++ b/src/plugin-slots/LoginPage/index.jsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import PropTypes from 'prop-types'; + +import LoginPage from '../../login/LoginPage'; + +const LoginPageSlot = ({ + institutionLogin, + handleInstitutionLogin, +}) => ( + + + +); + +LoginPageSlot.propTypes = { + institutionLogin: PropTypes.bool.isRequired, + handleInstitutionLogin: PropTypes.func.isRequired, +}; + +export default LoginPageSlot; diff --git a/src/recommendations/data/tests/hooks.test.jsx b/src/recommendations/data/tests/hooks.test.jsx index 9e5d6cf22a..43c989889d 100644 --- a/src/recommendations/data/tests/hooks.test.jsx +++ b/src/recommendations/data/tests/hooks.test.jsx @@ -1,10 +1,12 @@ -import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; + +import { render } from '@testing-library/react'; import algoliasearchHelper from 'algoliasearch-helper'; import mockedRecommendedProducts from './mockedData'; -import CreateAlgoliaSearchHelperMock from './test_utils/test_utils'; import isOneTrustFunctionalCookieEnabled from '../../../data/oneTrust'; import useAlgoliaRecommendations from '../hooks/useAlgoliaRecommendations'; +import CreateAlgoliaSearchHelperMock from './test_utils/test_utils'; jest.mock('algoliasearch-helper'); @@ -17,6 +19,23 @@ jest.mock('../../../data/algolia', () => ({ jest.mock('../algoliaResultsParser', () => jest.fn((course) => course)); +const renderHook = (hookCallback) => { + const result = { + current: null, + }; + + const Component = () => { + const val = hookCallback(); + React.useEffect(() => { + result.current = val; + }); + return null; + }; + + render(); + return { result }; +}; + describe('useAlgoliaRecommendations Tests', () => { const MockSearchHelperWithData = new CreateAlgoliaSearchHelperMock(mockedRecommendedProducts); const MockSearchHelperWithoutData = new CreateAlgoliaSearchHelperMock(); @@ -28,8 +47,10 @@ describe('useAlgoliaRecommendations Tests', () => { () => useAlgoliaRecommendations('PK', 'Introductory'), ); - expect(result.current.recommendations).toEqual(mockedRecommendedProducts); - expect(result.current.isLoading).toBe(false); + expect(result.current.recommendations) + .toEqual(mockedRecommendedProducts); + expect(result.current.isLoading) + .toBe(false); }); it('should not fetch recommendations if functional cookies are not set', async () => { @@ -39,8 +60,10 @@ describe('useAlgoliaRecommendations Tests', () => { () => useAlgoliaRecommendations('PK', 'Introductory'), ); - expect(result.current.recommendations).toEqual([]); - expect(result.current.isLoading).toBe(false); + expect(result.current.recommendations) + .toEqual([]); + expect(result.current.isLoading) + .toBe(false); }); it('should return empty list if no recommendations returned from Algolia', async () => { @@ -50,7 +73,9 @@ describe('useAlgoliaRecommendations Tests', () => { () => useAlgoliaRecommendations('PK', 'Introductory'), ); - expect(result.current.recommendations).toEqual([]); - expect(result.current.isLoading).toBe(false); + expect(result.current.recommendations) + .toEqual([]); + expect(result.current.isLoading) + .toBe(false); }); });