From e19d19d4b7cb0ce3a7fc0de259eca9e492abb59a Mon Sep 17 00:00:00 2001 From: "kshitij.sobti" Date: Thu, 7 Nov 2024 15:58:13 +0530 Subject: [PATCH] feat: Add plugin slot for login page This change adds a plugin slot for the login page allowing it to be customised. Since there was a dependency conflict between frontend-plugin-framework and the react-hooks testing package, the react-hooks testing package has been removed and a replaced with a simple mechanism for testing hooks. Since this touched the Login Page those have also been refactored to move away from redux connect. --- package-lock.json | 78 ++++----- package.json | 2 +- src/login/LoginPage.jsx | 51 +++--- src/login/tests/LoginPage.test.jsx | 8 + src/logistration/Logistration.jsx | 79 ++++----- src/logistration/Logistration.test.jsx | 150 +++++++++++------- src/plugin-slots/LoginPage/index.jsx | 25 +++ src/recommendations/data/tests/hooks.test.jsx | 42 +++-- 8 files changed, 260 insertions(+), 175 deletions(-) create mode 100644 src/plugin-slots/LoginPage/index.jsx diff --git a/package-lock.json b/package-lock.json index d4c8256e51..2c8ce716b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,11 @@ "@fortawesome/free-brands-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@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", @@ -3424,6 +3424,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.9.0", "resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-22.9.0.tgz", @@ -4072,34 +4105,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", @@ -13429,15 +13434,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" } @@ -13743,7 +13747,7 @@ }, "node_modules/react-shallow-renderer": { "version": "16.15.0", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -13794,7 +13798,7 @@ }, "node_modules/react-test-renderer": { "version": "17.0.2", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "object-assign": "^4.1.1", @@ -13808,7 +13812,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 3dfd3565b6..fee69e76c5 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "@fortawesome/free-brands-svg-icons": "6.6.0", "@fortawesome/free-solid-svg-icons": "6.6.0", "@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..cf1ea1ba6a 100644 --- a/src/login/LoginPage.jsx +++ b/src/login/LoginPage.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { connect } from 'react-redux'; +import { connect, useSelector } from 'react-redux'; import { getConfig } from '@edx/frontend-platform'; import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics'; @@ -33,7 +33,7 @@ import { thirdPartyAuthContextSelector } from '../common-components/data/selecto import EnterpriseSSO from '../common-components/EnterpriseSSO'; import ThirdPartyAuth from '../common-components/ThirdPartyAuth'; import { - DEFAULT_STATE, PENDING_STATE, RESET_PAGE, + PENDING_STATE, RESET_PAGE, } from '../data/constants'; import { getActivationStatus, @@ -46,11 +46,6 @@ import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess'; const LoginPage = (props) => { const { - backedUpFormData, - loginErrorCode, - loginErrorContext, - loginResult, - shouldBackupState, thirdPartyAuthContext: { providers, currentProvider, @@ -59,15 +54,32 @@ const LoginPage = (props) => { platformName, errorMessage: thirdPartyErrorMessage, }, - thirdPartyAuthApiStatus, institutionLogin, - showResetPasswordSuccessBanner, - submitState, // Actions backupFormState, handleInstitutionLogin, getTPADataFromBackend, } = props; + const { + backedUpFormData, + loginErrorCode, + loginErrorContext, + loginResult, + shouldBackupState, + showResetPasswordSuccessBanner, + submitState, + 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 { formatMessage } = useIntl(); const activationMsgType = getActivationStatus(); const queryParams = useMemo(() => getAllPossibleQueryParams(), []); @@ -282,17 +294,8 @@ 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, }; }; @@ -301,7 +304,6 @@ LoginPage.propTypes = { formFields: PropTypes.shape({}), errors: PropTypes.shape({}), }), - loginErrorCode: PropTypes.string, loginErrorContext: PropTypes.shape({ email: PropTypes.string, redirectUrl: PropTypes.string, @@ -311,10 +313,6 @@ LoginPage.propTypes = { 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, @@ -341,13 +339,8 @@ LoginPage.defaultProps = { emailOrUsername: '', password: '', }, }, - loginErrorCode: null, loginErrorContext: {}, loginResult: {}, - shouldBackupState: false, - showResetPasswordSuccessBanner: false, - submitState: DEFAULT_STATE, - thirdPartyAuthApiStatus: PENDING_STATE, thirdPartyAuthContext: { currentProvider: null, errorMessage: null, 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..9d04a8749d 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,18 +76,18 @@ 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); }; const tabTitle = (
- + {selectedPage === LOGIN_PAGE ? formatMessage(messages['logistration.sign.in']) @@ -104,14 +109,14 @@ const Logistration = (props) => { <> {institutionLogin && ( - + )}
{!institutionLogin && (

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

)} - +
) @@ -120,17 +125,21 @@ const Logistration = (props) => { {institutionLogin ? ( - + ) : (!isValidTpaHint() && !hideRegistrationLink && ( - handleOnSelect(tabKey, selectedPage)}> - - + handleOnSelect(tabKey, selectedPage)} + > + + ))} - { key && ( - + {key && ( + )}
{!institutionLogin && !isValidTpaHint() && hideRegistrationLink && ( @@ -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..0ce5396c90 100644 --- a/src/logistration/Logistration.test.jsx +++ b/src/logistration/Logistration.test.jsx @@ -1,21 +1,18 @@ -import React from 'react'; -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 React from 'react'; +import { Provider } from 'react-redux'; 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'; +import Logistration from './Logistration'; + jest.mock('@edx/frontend-platform/analytics', () => ({ sendPageEvent: jest.fn(), sendTrackEvent: jest.fn(), @@ -23,11 +20,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 +41,6 @@ describe('Logistration', () => { registerUrl: '/dummy_auth', }; - const reduxWrapper = children => ( - - - {children} - - - ); - const initialState = { register: { registrationFormData: { @@ -50,16 +48,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 +79,18 @@ describe('Logistration', () => { }, }, login: { - loginResult: { success: false, redirectUrl: '' }, + loginResult: { + success: false, + redirectUrl: '', + }, + loginFormData: { + formFields: { + emailOrUsername: '', password: '', + }, + errors: { + emailOrUsername: '', password: '', + }, + }, }, }; @@ -90,16 +109,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 +132,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 +152,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 +189,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 +221,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 +254,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 +286,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 +303,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..71c828a663 --- /dev/null +++ b/src/plugin-slots/LoginPage/index.jsx @@ -0,0 +1,25 @@ +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import PropTypes from 'prop-types'; +import React from 'react'; +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..325faf161a 100644 --- a/src/recommendations/data/tests/hooks.test.jsx +++ b/src/recommendations/data/tests/hooks.test.jsx @@ -1,10 +1,11 @@ -import { renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { render } from '@testing-library/react'; import algoliasearchHelper from 'algoliasearch-helper'; +import isOneTrustFunctionalCookieEnabled from '../../../data/oneTrust'; +import useAlgoliaRecommendations from '../hooks/useAlgoliaRecommendations'; import mockedRecommendedProducts from './mockedData'; import CreateAlgoliaSearchHelperMock from './test_utils/test_utils'; -import isOneTrustFunctionalCookieEnabled from '../../../data/oneTrust'; -import useAlgoliaRecommendations from '../hooks/useAlgoliaRecommendations'; jest.mock('algoliasearch-helper'); @@ -17,6 +18,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 +46,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 +59,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 +72,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); }); });