feat: breadcrumb rolloutout flag + analytics (#647)

As an addendum to https://openedx.atlassian.net/browse/TNL-7107, we want to hide rollout behind a frontend feature flag added in https://github.com/edx/edx-internal/pull/5489. We also want to report these events to the events api with name `edx.ui.lms.jump_nav.selected`. Doummentation to add this event is listed at the following PR: https://github.com/edx/edx-documentation/pull/1982
This commit is contained in:
connorhaugh
2021-09-21 15:39:52 -04:00
committed by GitHub
parent 8c0e98ad4f
commit 2932d98976
8 changed files with 181 additions and 10 deletions

1
.env
View File

@@ -37,3 +37,4 @@ TWITTER_HASHTAG=''
TWITTER_URL=''
USER_INFO_COOKIE_NAME=''
SESSION_COOKIE_DOMAIN=''
ENABLE_JUMPNAV='true'

View File

@@ -37,3 +37,4 @@ TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
SESSION_COOKIE_DOMAIN='localhost'
ENABLE_JUMPNAV='true'

View File

@@ -36,3 +36,4 @@ TERMS_OF_SERVICE_URL='https://www.edx.org/edx-terms-service'
TWITTER_HASHTAG='myedxjourney'
TWITTER_URL='https://twitter.com/edXOnline'
USER_INFO_COOKIE_NAME='edx-user-info'
ENABLE_JUMPNAV='true'

View File

@@ -109,3 +109,9 @@ TWITTER_URL
unless this is set. Optional.
Example: https://twitter.com/edXOnline
ENABLE_JUMPNAV
Enables the new Jump Navigation feature in the course breadcrumbs, defaulted to the string 'true'.
Disable to have simple hyperlinks for breadcrumbs. Setting it to any other value but 'true' ('false','I love flags', 'etc' would disable the Jumpnav).
This feature flag is slated to be removed as jumpnav becomes default. Follow the progress of this ticket here:
https://openedx.atlassian.net/browse/TNL-8678

View File

@@ -91,6 +91,31 @@ describe('Course', () => {
expect(notificationTrigger).not.toHaveClass('trigger-active');
});
it('renders course breadcrumbs as expected', async () => {
const courseMetadata = Factory.build('courseMetadata');
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
'block',
{ type: 'vertical' },
{ courseId: courseMetadata.id },
));
const testStore = await initializeTestStore({ courseMetadata, unitBlocks }, false);
const { courseware, models } = testStore.getState();
const { courseId, sequenceId } = courseware;
const testData = {
...mockData,
courseId,
sequenceId,
unitId: Object.values(models.units)[1].id, // Corner cases are already covered in `Sequence` tests.
};
render(<Course {...testData} />, { store: testStore });
loadUnit();
await waitFor(() => expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument());
// expect the section and sequence "titles" to be loaded in as breadcrumb labels.
expect(screen.getByText('cdabcdabcdabcdabcdabcdabcdabcd13')).toBeInTheDocument();
expect(screen.getByText('cdabcdabcdabcdabcdabcdabcdabcd12')).toBeInTheDocument();
});
it('passes handlers to the sequence', async () => {
const nextSequenceHandler = jest.fn();
const previousSequenceHandler = jest.fn();

View File

@@ -7,6 +7,10 @@ import { faHome } from '@fortawesome/free-solid-svg-icons';
import { useSelector } from 'react-redux';
import { Hyperlink, MenuItem, SelectMenu } from '@edx/paragon';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import {
sendTrackingLogEvent,
sendTrackEvent,
} from '@edx/frontend-platform/analytics';
import { useModel, useModels } from '../../generic/model-store';
/** [MM-P2P] Experiment */
import { MMP2PFlyoverTrigger } from '../../experiments/mm-p2p';
@@ -16,14 +20,31 @@ function CourseBreadcrumb({
}) {
const defaultContent = content.filter(destination => destination.default)[0];
const { administrator } = getAuthenticatedUser();
function logEvent(target) {
const eventName = 'edx.ui.lms.jump_nav.selected';
const payload = {
target_name: target.label,
id: target.id,
current_id: defaultContent.id,
widget_placement: 'breadcrumb',
};
sendTrackEvent(eventName, payload);
sendTrackingLogEvent(eventName, payload);
}
return (
<>
{withSeparator && (
<li className="mx-2 text-primary-500" role="presentation" aria-hidden>/</li>
<li className="mx-2 text-primary-500 text-truncate text-nowrap" role="presentation" aria-hidden>/</li>
)}
<li>
{process.env.NODE_ENV !== 'test' || content.length < 2 || !administrator
<li style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
}}
>
{ getConfig().ENABLE_JUMPNAV !== 'true' || content.length < 2 || !administrator
? (
<a className="text-primary-500" href={defaultContent.url}>{defaultContent.label}
</a>
@@ -34,7 +55,8 @@ function CourseBreadcrumb({
<MenuItem
as={Hyperlink}
defaultSelected={item.default}
href={item.url}
destination={item.url}
onClick={logEvent(item)}
>
{item.label}
</MenuItem>
@@ -46,7 +68,6 @@ function CourseBreadcrumb({
</>
);
}
CourseBreadcrumb.propTypes = {
content: PropTypes.arrayOf(
PropTypes.shape({
@@ -72,7 +93,7 @@ export default function CourseBreadcrumbs({
}) {
const course = useModel('coursewareMeta', courseId);
const courseStatus = useSelector(state => state.courseware.courseStatus);
const sections = Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, section]));
const sections = course ? Object.fromEntries(useModels('sections', course.sectionIds).map(section => [section.id, section])) : null;
const possibleSequences = sections && sectionId ? sections[sectionId].sequenceIds : [];
const sequences = Object.fromEntries(useModels('sequences', possibleSequences).map(sequence => [sequence.id, sequence]));
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
@@ -97,13 +118,12 @@ export default function CourseBreadcrumbs({
}
return temp;
}, [courseStatus, sections, sequences]);
return (
<nav aria-label="breadcrumb" className="my-1 d-inline-block col-sm-10">
<ol className="list-unstyled d-flex align-items-center m-0">
<li>
<a
href={`${getConfig().LMS_BASE_URL}/courses/${course.id}/course/`}
href={`${getConfig().LMS_BASE_URL}/courses/${courseId}/course/`}
className="flex-shrink-0 text-primary"
>
<FontAwesomeIcon icon={faHome} className="mr-2" />
@@ -121,7 +141,7 @@ export default function CourseBreadcrumbs({
/>
))}
{/** [MM-P2P] Experiment */}
{mmp2p.state.isEnabled && (
{mmp2p.state && mmp2p.state.isEnabled && (
<MMP2PFlyoverTrigger options={mmp2p} />
)}
</ol>
@@ -144,7 +164,6 @@ CourseBreadcrumbs.propTypes = {
CourseBreadcrumbs.defaultProps = {
sectionId: null,
sequenceId: null,
/** [MM-P2P] Experiment */
mmp2p: {},
};

View File

@@ -0,0 +1,117 @@
import React from 'react';
import { screen, render, fireEvent } from '@testing-library/react';
import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { getConfig } from '@edx/frontend-platform';
import { useModel, useModels } from '../../generic/model-store';
import CourseBreadcrumbs from './CourseBreadcrumbs';
jest.mock('@edx/frontend-platform');
jest.mock('react-redux');
jest.mock('@edx/frontend-platform/analytics');
// Remove When Fully rolled out>>>
jest.mock('../../generic/model-store');
jest.mock('@edx/frontend-platform/auth');
getConfig.mockImplementation(() => ({ ENABLE_JUMPNAV: 'true' }));
getAuthenticatedUser.mockImplementation(() => ({ administrator: true }));
// ^^^^Remove When Fully rolled out
useSelector.mockImplementation(() => 'loaded');
useModels.mockImplementation((name) => {
if (name === 'sections') {
return [
{
courseId: 'course-v1:edX+DemoX+Demo_Course',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'],
title: 'Introduction',
},
{
courseId: 'course-v1:edX+DemoX+Demo_Course',
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
sequenceIds: ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions'],
title: 'Example Week 1: Getting Started',
},
];
}
return [
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
title: 'Lesson 1 - Getting Started',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@867dddb6f55d410caaa9c1eb9c6743ec',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4f6c1b4e316a419ab5b6bf30e6c708e9',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@3dc16db8d14842e38324e95d4030b8a0',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@4a1bba2a403f40bca5ec245e945b0d76',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@256f17a44983429fb1a60802203ee4e0',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e3601c0abee6427d8c17e6d6f8fdddd1',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@134df56c516a4a0dbb24dd5facef746e',
],
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions',
sectionId: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
title: 'Homework - Question Styles',
unitIds: [
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2152d4a4aadc4cb0af5256394a3d1fc7',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@47dbd5f836544e61877a483c0b75606c',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@54bb9b142c6c4c22afc62bcb628f0e68',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0c92347a5c00',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_1fef54c2b23b',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@2889db1677a549abb15eb4d886f95d1c',
'block-v1:edX+DemoX+Demo_Course+type@vertical+block@e8a5cc2aed424838853defab7be45e42',
],
},
];
});
useModel.mockImplementation(() => ({
sectionIds: ['block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations'],
}));
describe('CourseBreadcrumbs', () => {
jest.spyOn(React, 'useMemo').mockImplementation(() => [
[
{
default: false,
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@d8a6192ade314473a78242dfeedfbf5b',
label: 'Introduction',
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction',
},
{
default: true,
id: 'block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations',
label: 'Example Week 1: Getting Started',
url: 'http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course/block-v1:edX+DemoX+Demo_Course+type@sequential+block@19a30717eff543078a5d94ae9d6c18a5',
},
],
[
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@simulations', label: "Lesson 2 - Let's Get Interactive!", default: true, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@d0d804e8863c4a95a659c04d8a2b2bc0',
},
{
id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@175e76c4951144a29d46211361266e0e', label: 'Homework - Essays', default: false, url: 'http://localhost:2000/course/course-v1:edX+DemoX+D…e@vertical+block@fb79dcbad35b466a8c6364f8ffee9050',
},
],
]);
render(
<CourseBreadcrumbs
courseId="course-v1:edX+DemoX+Demo_Course"
sectionId="block-v1:edX+DemoX+Demo_Course+type@chapter+block@interactive_demonstrations"
sequenceId="block-v1:edX+DemoX+Demo_Course+type@sequential+block@basic_questions"
/>,
);
it('renders course breadcrumbs as expected, handles clicks', async () => {
expect(screen.getByRole('navigation', { name: 'breadcrumb' })).toBeInTheDocument();
expect(screen.queryAllByRole('button')).toHaveLength(2);
const sectionButton = screen.getByText('Example Week 1: Getting Started');
expect(screen.queryAllByRole('link')).toHaveLength(1);
fireEvent.click(sectionButton);
expect(screen.queryAllByRole('link')).toHaveLength(2);
const menuItem = screen.queryAllByRole('link')[0];
fireEvent.click(menuItem);
});
});

View File

@@ -92,6 +92,7 @@ initialize({
CONTACT_URL: process.env.CONTACT_URL || null,
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,
INSIGHTS_BASE_URL: process.env.INSIGHTS_BASE_URL || null,
SEARCH_CATALOG_URL: process.env.SEARCH_CATALOG_URL || null,
SOCIAL_UTM_MILESTONE_CAMPAIGN: process.env.SOCIAL_UTM_MILESTONE_CAMPAIGN || null,