Skip to content

Commit

Permalink
Merge pull request #642 from openedx/btahir/ENT-6372
Browse files Browse the repository at this point in the history
feat: course length in course search card footer and optimizely A/B test
  • Loading branch information
bilaltahir21 authored Dec 20, 2022
2 parents c38f8e8 + 93b37d3 commit 7283041
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 2 deletions.
25 changes: 24 additions & 1 deletion src/components/search/SearchCourseCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ import { Card } from '@edx/paragon';
import { sendEnterpriseTrackEvent } from '@edx/frontend-enterprise-utils';

import { getPrimaryPartnerLogo, isDefinedAndNotNull } from '../../utils/common';
import { GENERAL_LENGTH_COURSE, SHORT_LENGTH_COURSE } from './data/constants';
import { useCourseAboutPageVisitClickHandler } from './data/hooks';
import { isExperimentVariant } from '../../utils/optimizely';
import { isShortCourse } from './utils';

const SearchCourseCard = ({ hit, isLoading, ...rest }) => {
const { enterpriseConfig: { slug, uuid } } = useContext(AppContext);
Expand Down Expand Up @@ -45,12 +49,26 @@ const SearchCourseCard = ({ hit, isLoading, ...rest }) => {
[course],
);

const config = getConfig();
const isExperimentVariationA = isExperimentVariant(
config.EXPERIMENT_3_ID,
config.EXPERIMENT_3_VARIANT_1_ID,
);

const isShortLengthCourse = isShortCourse(course);

const primaryPartnerLogo = getPrimaryPartnerLogo(partnerDetails);

const handleCourseAboutPageVisitClick = useCourseAboutPageVisitClickHandler({
courseKey: course.key,
enterpriseId: uuid,
});

const handleCardClick = () => {
if (!linkToCourse) {
return;
}
handleCourseAboutPageVisitClick();
sendEnterpriseTrackEvent(
uuid,
'edx.ui.enterprise.learner_portal.search.card.clicked',
Expand Down Expand Up @@ -92,7 +110,12 @@ const SearchCourseCard = ({ hit, isLoading, ...rest }) => {
)}
/>
<Card.Section />
<Card.Footer textElement={<span className="text-muted">Course</span>} />
<Card.Footer textElement={(
<span className="text-muted">
{(isExperimentVariationA && isShortLengthCourse) ? SHORT_LENGTH_COURSE : GENERAL_LENGTH_COURSE}
</span>
)}
/>
</Card>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/components/search/data/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const GENERAL_LENGTH_COURSE = 'Course';
export const SHORT_LENGTH_COURSE = 'Short Course';
20 changes: 19 additions & 1 deletion src/components/search/data/hooks.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {
useContext, useMemo, useEffect,
useContext, useMemo, useEffect, useCallback,
} from 'react';
import {
SearchContext, getCatalogString, SHOW_ALL_NAME, setRefinementAction,
} from '@edx/frontend-enterprise-catalog-search';
import { features } from '../../../config';
import { LICENSE_STATUS } from '../../enterprise-user-subsidy/data/constants';
import { pushEvent, EVENTS } from '../../../utils/optimizely';

export const useSearchCatalogs = ({
subscriptionPlan,
Expand Down Expand Up @@ -76,3 +77,20 @@ export const useDefaultSearchFilters = ({

return { filters };
};

/**
* Returns a function to be used as a click handler emitting an optimizely event on course about page visit click event.
*
* @returns Click handler function for course about page visit click events.
*/
export const useCourseAboutPageVisitClickHandler = ({ courseKey, enterpriseId }) => {
const handleClick = useCallback(
() => {
// Send the Optimizely event to track the course about page visit
pushEvent(EVENTS.COURSE_ABOUT_PAGE_CLICK, { courseKey, enterpriseId });
},
[courseKey, enterpriseId],
);

return handleClick;
};
46 changes: 46 additions & 0 deletions src/components/search/tests/SearchCourseCard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { AppContext } from '@edx/frontend-platform/react';
import '@testing-library/jest-dom/extend-expect';
import { renderHook } from '@testing-library/react-hooks';

import SearchCourseCard from '../SearchCourseCard';
import * as optimizelyUtils from '../../../utils/optimizely';
import * as courseSearchUtils from '../utils';

import { renderWithRouter } from '../../../utils/tests';
import { TEST_ENTERPRISE_SLUG, TEST_IMAGE_URL } from './constants';
import { useCourseAboutPageVisitClickHandler } from '../data/hooks';

jest.mock('react-truncate', () => ({
__esModule: true,
Expand Down Expand Up @@ -83,4 +87,46 @@ describe('<SearchCourseCard />', () => {
userEvent.click(cardEl);
expect(history.entries).toHaveLength(1);
});

test('render course_length field in place of course text', () => {
jest.spyOn(optimizelyUtils, 'isExperimentVariant').mockImplementation(() => true);
jest.spyOn(courseSearchUtils, 'isShortCourse').mockImplementation(() => true);

const { container } = renderWithRouter(<SearchCourseCardWithAppContext {...defaultProps} />);

// assert that the card footer shows text "Short Course"
expect(container.querySelector('.pgn__card-footer-text')).toHaveTextContent('Short Course');
});

test('do not render course_length field in place of course text', () => {
jest.spyOn(optimizelyUtils, 'isExperimentVariant').mockImplementation(() => true);
jest.spyOn(courseSearchUtils, 'isShortCourse').mockImplementation(() => false);

const { container } = renderWithRouter(<SearchCourseCardWithAppContext {...defaultProps} />);

// assert that the card footer shows text "Course"
expect(container.querySelector('.pgn__card-footer-text')).toHaveTextContent('Course');
});

test('optimizely event is being triggered in onClick when search card is clicked', () => {
const basicProps = {
courseKey: 'course-key',
enterpriseId: 'enterprise-id',
};
const pushEventSpy = jest.spyOn(optimizelyUtils, 'pushEvent').mockImplementation(() => (true));

const { result } = renderHook(() => useCourseAboutPageVisitClickHandler(basicProps));
result.current({ preventDefault: jest.fn() });

const { container } = renderWithRouter(<SearchCourseCardWithAppContext {...defaultProps} />);

// select card with class pgn__card and click on it
const card = container.querySelector('.pgn__card');
card.click();

expect(pushEventSpy).toHaveBeenCalledWith('enterprise_learner_portal_course_about_page_click', {
courseKey: 'course-key',
enterpriseId: 'enterprise-id',
});
});
});
3 changes: 3 additions & 0 deletions src/components/search/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isShortCourse(course) {
return course.course_length === 'short';
}
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ initialize({
GETSMARTER_PRIVACY_POLICY_URL: process.env.GETSMARTER_PRIVACY_POLICY_URL || null,
EXPERIMENT_2_ID: process.env.EXPERIMENT_2_ID || null,
EXPERIMENT_2_VARIANT_1_ID: process.env.EXPERIMENT_2_VARIANT_1_ID || null,
EXPERIMENT_3_ID: process.env.EXPERIMENT_3_ID || null,
EXPERIMENT_3_VARIANT_1_ID: process.env.EXPERIMENT_3_VARIANT_1_ID || null,
});
},
},
Expand Down
1 change: 1 addition & 0 deletions src/utils/optimizely.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export const EVENTS = {
ENROLLMENT_CLICK: 'enterprise_learner_portal_enrollment_click',
FIRST_ENROLLMENT_CLICK: 'enterprise_learner_portal_first_enrollment_click',
LICENSE_SUBSIDY_ENROLLMENT_CLICK: 'enterprise_learner_portal_license_subsidy_enrollment_click',
COURSE_ABOUT_PAGE_CLICK: 'enterprise_learner_portal_course_about_page_click',
};

export const getActiveExperiments = () => {
Expand Down

0 comments on commit 7283041

Please sign in to comment.