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:
Zainab Amir
2023-02-23 13:59:21 +05:00
committed by GitHub
parent c31c03f5a9
commit 8e527efd07
7 changed files with 288 additions and 28 deletions

View File

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

View File

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

View File

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

View File

@@ -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'])}

View 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',
};

View 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');
});
});

View 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;