From 8e527efd076edcaca4a8b9520850e80eb8dc57b8 Mon Sep 17 00:00:00 2001 From: Zainab Amir Date: Thu, 23 Feb 2023 13:59:21 +0500 Subject: [PATCH] zamir/van 1295/add tests for recommender (#755) * feat: VAN-1295 add tests for recommendations * feat: add test for loading state VAN-1295 --- src/data/constants.js | 10 -- .../tests/ProgressiveProfiling.test.jsx | 72 +++++++++++- src/recommendations/RecommendationsList.jsx | 2 +- src/recommendations/RecommendationsPage.jsx | 31 ++--- src/recommendations/data/constants.js | 11 ++ .../tests/RecommendationsPage.test.jsx | 111 ++++++++++++++++++ src/recommendations/tests/constants.js | 79 +++++++++++++ 7 files changed, 288 insertions(+), 28 deletions(-) create mode 100644 src/recommendations/data/constants.js create mode 100644 src/recommendations/tests/RecommendationsPage.test.jsx create mode 100644 src/recommendations/tests/constants.js diff --git a/src/data/constants.js b/src/data/constants.js index fc48e04e..eb1a1962 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -36,13 +36,3 @@ export const INVALID_NAME_REGEX = /[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{ // Query string parameters that can be passed to LMS to manage // things like auto-enrollment upon login and registration. export const AUTH_PARAMS = ['course_id', 'enrollment_action', 'course_mode', 'email_opt_in', 'purchase_workflow', 'next', 'save_for_later', 'register_for_free']; - -// Education difficulty level mapping -export const EDUCATION_LEVEL_MAPPING = { - p: 'Advanced', - m: 'Advanced', - b: 'Intermediate', - a: 'Intermediate', - hs: 'Introductory', - jhs: 'Introductory', -}; diff --git a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx index c1e749e7..b24c430e 100644 --- a/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx +++ b/src/progressive-profiling/tests/ProgressiveProfiling.test.jsx @@ -7,11 +7,13 @@ import * as auth from '@edx/frontend-platform/auth'; import { configure, injectIntl, IntlProvider } from '@edx/frontend-platform/i18n'; import * as logging from '@edx/frontend-platform/logging'; import { mount } from 'enzyme'; +import { createMemoryHistory } from 'history'; import { act } from 'react-dom/test-utils'; +import { MemoryRouter, Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import { - COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE, + COMPLETE_STATE, DEFAULT_REDIRECT_URL, FAILURE_STATE, RECOMMENDATIONS, } from '../../data/constants'; import { saveUserProfile } from '../data/actions'; import ProgressiveProfiling from '../ProgressiveProfiling'; @@ -31,6 +33,8 @@ auth.configure = jest.fn(); auth.ensureAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true)); auth.hydrateAuthenticatedUser = jest.fn().mockImplementation(() => Promise.resolve(true)); +const history = createMemoryHistory(); + describe('ProgressiveProfilingTests', () => { mergeConfig({ AUTHN_PROGRESSIVE_PROFILING_SUPPORT_LINK: 'http://localhost:1999/welcome', @@ -58,12 +62,18 @@ describe('ProgressiveProfilingTests', () => { const reduxWrapper = children => ( - {children} + + {children} + ); const getProgressiveProfilingPage = async () => { - const progressiveProfilingPage = mount(reduxWrapper()); + const progressiveProfilingPage = mount(reduxWrapper( + + + , + )); await act(async () => { await Promise.resolve(progressiveProfilingPage); await new Promise(resolve => setImmediate(resolve)); @@ -156,4 +166,60 @@ describe('ProgressiveProfilingTests', () => { await getProgressiveProfilingPage(); expect(window.location.href).toBe(DASHBOARD_URL); }); + + describe('Recommendations test', () => { + mergeConfig({ + ENABLE_PERSONALIZED_RECOMMENDATIONS: true, + }); + + it('should redirect to recommendations page if recommendations are enabled', async () => { + store = mockStore({ + welcomePage: { + ...initialState.welcomePage, + success: true, + }, + }); + + auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' })); + const progressiveProfilingPage = await getProgressiveProfilingPage(); + + expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Next'); + expect(history.location.pathname).toEqual(RECOMMENDATIONS); + }); + + it('should not redirect to recommendations page if user is on its way to enroll in a course', async () => { + delete window.location; + window.location = { + href: getConfig().BASE_URL, + assign: jest.fn().mockImplementation((value) => { window.location.href = value; }), + }; + + const redirectUrl = `${getConfig().LMS_BASE_URL}${DEFAULT_REDIRECT_URL}?enrollment_action=1`; + props = { + getFieldData: jest.fn(), + location: { + state: { + registrationResult: { + redirectUrl, + success: true, + }, + optionalFields, + }, + }, + }; + + store = mockStore({ + welcomePage: { + ...initialState.welcomePage, + success: true, + }, + }); + + auth.getAuthenticatedUser = jest.fn(() => ({ userId: 3, username: 'abc123' })); + const progressiveProfilingPage = await getProgressiveProfilingPage(); + + expect(progressiveProfilingPage.find('button.btn-brand').text()).toEqual('Submit'); + expect(window.location.href).toEqual(redirectUrl); + }); + }); }); diff --git a/src/recommendations/RecommendationsList.jsx b/src/recommendations/RecommendationsList.jsx index 53570554..3da2ed00 100644 --- a/src/recommendations/RecommendationsList.jsx +++ b/src/recommendations/RecommendationsList.jsx @@ -10,7 +10,7 @@ const RecommendationsList = (props) => { const { title, recommendations } = props; return ( - +

{title}

diff --git a/src/recommendations/RecommendationsPage.jsx b/src/recommendations/RecommendationsPage.jsx index 955710f2..02125b0d 100644 --- a/src/recommendations/RecommendationsPage.jsx +++ b/src/recommendations/RecommendationsPage.jsx @@ -7,31 +7,34 @@ import { } from '@edx/paragon'; import PropTypes from 'prop-types'; -import { DEFAULT_REDIRECT_URL, EDUCATION_LEVEL_MAPPING } from '../data/constants'; +import { DEFAULT_REDIRECT_URL } from '../data/constants'; +import { EDUCATION_LEVEL_MAPPING, RECOMMENDATIONS_COUNT } from './data/constants'; import getPersonalizedRecommendations from './data/service'; import messages from './messages'; import RecommendationsList from './RecommendationsList'; const RecommendationsPage = (props) => { const { intl, location } = props; + const registrationResponse = location.state?.registrationResult; + const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); const [isLoading, setIsLoading] = useState(true); const [recommendations, setRecommendations] = useState([]); const educationLevel = EDUCATION_LEVEL_MAPPING[location.state?.educationLevel]; - const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); - const registrationResponse = location.state?.registrationResult; useEffect(() => { - getPersonalizedRecommendations(educationLevel).then((response) => { - if (response.length > 2) { - setRecommendations(response); - } - setIsLoading(false); - }) - .catch(() => { + if (registrationResponse) { + getPersonalizedRecommendations(educationLevel).then((response) => { + if (response.length) { + setRecommendations(response.slice(0, RECOMMENDATIONS_COUNT)); + } setIsLoading(false); - }); - }, [DASHBOARD_URL, educationLevel]); + }) + .catch(() => { + setIsLoading(false); + }); + } + }, [registrationResponse, DASHBOARD_URL, educationLevel]); if (!registrationResponse) { global.location.assign(DASHBOARD_URL); @@ -47,7 +50,7 @@ const RecommendationsPage = (props) => { } }; - if (!isLoading && recommendations.length < 3) { + if (!isLoading && recommendations.length < RECOMMENDATIONS_COUNT) { handleRedirection(); } @@ -64,7 +67,7 @@ const RecommendationsPage = (props) => { {getConfig().SITE_NAME} - {(!isLoading && recommendations.length > 2) ? ( + {(!isLoading && recommendations.length === RECOMMENDATIONS_COUNT) ? (
{ + let defaultProps = {}; + let store = {}; + + const registrationResult = { + redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'), + success: true, + }; + const reduxWrapper = children => ( + + {children} + + ); + + const getRecommendationsPage = async (props = defaultProps) => { + const recommendationsPage = mount(reduxWrapper()); + await act(async () => { + await Promise.resolve(recommendationsPage); + recommendationsPage.update(); + }); + + return recommendationsPage; + }; + + beforeEach(() => { + store = mockStore({}); + defaultProps = { + location: { + state: { + registrationResult, + }, + }, + }; + }); + + it('redirects to dashboard if user tries to access the page directly', async () => { + const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL); + delete window.location; + window.location = { + href: getConfig().BASE_URL, + assign: jest.fn().mockImplementation((value) => { window.location.href = value; }), + }; + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([])); + await getRecommendationsPage({}); + + expect(getPersonalizedRecommendations.default).toHaveBeenCalledTimes(0); + expect(window.location.href).toEqual(DASHBOARD_URL); + }); + + it('should show loading state to user', async () => { + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse)); + await act(async () => { + const recommendationsPage = mount(reduxWrapper()); + expect(recommendationsPage.find('.centered-align-spinner').exists()).toBeTruthy(); + }); + }); + + it('should call getPersonalizedRecommendations', async () => { + delete window.location; + window.location = { assign: jest.fn() }; + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([])); + await getRecommendationsPage(); + + expect(getPersonalizedRecommendations.default).toHaveBeenCalledTimes(1); + }); + + it('should display recommendations returned by Algolia', async () => { + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse)); + const recommendationsPage = await getRecommendationsPage(); + + expect(recommendationsPage.find('#course-recommendations').exists()).toBeTruthy(); + }); + + it('should redirect if recommended courses count is less than RECOMMENDATIONS_COUNT', async () => { + delete window.location; + window.location = { assign: jest.fn() }; + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve([mockedResponse[0]])); + const recommendationsPage = await getRecommendationsPage(); + + expect(recommendationsPage.find('#course-recommendations').exists()).toBeFalsy(); + expect(window.location.href).toEqual(registrationResult.redirectUrl); + }); + + it('should display all owners for a course', async () => { + getPersonalizedRecommendations.default = jest.fn().mockImplementation(() => Promise.resolve(mockedResponse)); + const recommendationsPage = await getRecommendationsPage(); + + expect( + recommendationsPage.find('.pgn__card-header-subtitle-md').getElements()[0].props.children, + ).toEqual('firstOwnerX, secondOwnerX'); + }); +}); diff --git a/src/recommendations/tests/constants.js b/src/recommendations/tests/constants.js new file mode 100644 index 00000000..0ab9c16d --- /dev/null +++ b/src/recommendations/tests/constants.js @@ -0,0 +1,79 @@ +const mockedResponse = [ + { + title: 'How to Learn Online 1', + marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-1', + cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-1.png', + activeRunKey: 'course-v1:test+testX+2018', + owners: [ + { + key: 'firstOwnerX', + logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png', + name: 'first owner', + }, + { + key: 'secondOwnerX', + logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-1.png', + name: 'second owner', + }, + ], + objectId: 'course-how-to-learn-online-key-1', + }, + { + title: 'How to Learn Online 2', + marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-2', + cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-2.png', + activeRunKey: 'course-v1:test+testX+2019', + owners: [ + { + key: 'testX', + logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-2.png', + name: 'test', + }, + ], + objectId: 'course-how-to-learn-online-key-2', + }, + { + title: 'How to Learn Online 3', + marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-3', + cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-3.png', + activeRunKey: 'course-v1:test+testX+2020', + owners: [ + { + key: 'testX', + logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-3.png', + name: 'test', + }, + ], + objectId: 'course-how-to-learn-online-key-3', + }, + { + title: 'How to Learn Online 4', + marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-4', + cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-4.png', + activeRunKey: 'course-v1:test+testX+2021', + owners: [ + { + key: 'testX', + logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-4.png', + name: 'test', + }, + ], + objectId: 'course-how-to-learn-online-key-4', + }, + { + title: 'How to Learn Online 5', + marketingUrl: 'https://test-recommendations.com/course/how-to-learn-online-5', + cardImageUrl: 'https://test-recommendations.com/image/how-to-learn-online-5.png', + activeRunKey: 'course-v1:test+testX+2022', + owners: [ + { + key: 'testX', + logoImageUrl: 'https://test-recommendations.com/logos/how-to-learn-online-5.png', + name: 'test', + }, + ], + objectId: 'course-how-to-learn-online-key-5', + }, +]; + +export default mockedResponse;