zamir/van 1295/add tests for recommender (#755)
* feat: VAN-1295 add tests for recommendations * feat: add test for loading state VAN-1295
This commit is contained in:
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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 => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</MemoryRouter>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const getProgressiveProfilingPage = async () => {
|
||||
const progressiveProfilingPage = mount(reduxWrapper(<IntlProgressiveProfilingPage {...props} />));
|
||||
const progressiveProfilingPage = mount(reduxWrapper(
|
||||
<Router history={history}>
|
||||
<IntlProgressiveProfilingPage {...props} />
|
||||
</Router>,
|
||||
));
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ const RecommendationsList = (props) => {
|
||||
const { title, recommendations } = props;
|
||||
|
||||
return (
|
||||
<Container size="lg" className="recommendations-container">
|
||||
<Container id="course-recommendations" size="lg" className="recommendations-container">
|
||||
<h2 className="text-sm-center mb-4 text-left recommendations-heading">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
@@ -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) => {
|
||||
<Image className="logo" alt={getConfig().SITE_NAME} src={getConfig().LOGO_URL} />
|
||||
</Hyperlink>
|
||||
</div>
|
||||
{(!isLoading && recommendations.length > 2) ? (
|
||||
{(!isLoading && recommendations.length === RECOMMENDATIONS_COUNT) ? (
|
||||
<div className="d-flex flex-column align-items-center justify-content-center flex-grow-1 p-1">
|
||||
<RecommendationsList
|
||||
title={intl.formatMessage(messages['recommendation.page.heading'])}
|
||||
|
||||
11
src/recommendations/data/constants.js
Normal file
11
src/recommendations/data/constants.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export const RECOMMENDATIONS_COUNT = 4;
|
||||
|
||||
// Education difficulty level mapping
|
||||
export const EDUCATION_LEVEL_MAPPING = {
|
||||
p: 'Advanced',
|
||||
m: 'Advanced',
|
||||
b: 'Intermediate',
|
||||
a: 'Intermediate',
|
||||
hs: 'Introductory',
|
||||
jhs: 'Introductory',
|
||||
};
|
||||
111
src/recommendations/tests/RecommendationsPage.test.jsx
Normal file
111
src/recommendations/tests/RecommendationsPage.test.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { injectIntl, IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { mount } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import configureStore from 'redux-mock-store';
|
||||
|
||||
import { DEFAULT_REDIRECT_URL } from '../../data/constants';
|
||||
import * as getPersonalizedRecommendations from '../data/service';
|
||||
import RecommendationsPage from '../RecommendationsPage';
|
||||
import mockedResponse from './constants';
|
||||
|
||||
const IntlRecommendationsPage = injectIntl(RecommendationsPage);
|
||||
const mockStore = configureStore();
|
||||
|
||||
jest.mock('../data/service');
|
||||
|
||||
describe('RecommendationsPageTests', () => {
|
||||
let defaultProps = {};
|
||||
let store = {};
|
||||
|
||||
const registrationResult = {
|
||||
redirectUrl: getConfig().LMS_BASE_URL.concat('/course-about-page-url'),
|
||||
success: true,
|
||||
};
|
||||
const reduxWrapper = children => (
|
||||
<IntlProvider locale="en">
|
||||
<Provider store={store}>{children}</Provider>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const getRecommendationsPage = async (props = defaultProps) => {
|
||||
const recommendationsPage = mount(reduxWrapper(<IntlRecommendationsPage {...props} />));
|
||||
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(<IntlRecommendationsPage {...defaultProps} />));
|
||||
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');
|
||||
});
|
||||
});
|
||||
79
src/recommendations/tests/constants.js
Normal file
79
src/recommendations/tests/constants.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user