diff --git a/package-lock.json b/package-lock.json index 20b78564a0..deba109501 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13886,6 +13886,11 @@ } } }, + "js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", diff --git a/package.json b/package.json index 7099a250ca..8d75655f4d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "@reduxjs/toolkit": "1.3.6", "classnames": "2.2.6", "core-js": "3.6.5", + "js-cookie": "2.2.1", "lodash.camelcase": "^4.3.0", "prop-types": "15.7.2", "react": "16.13.1", diff --git a/src/courseware/course/Course.jsx b/src/courseware/course/Course.jsx index 2220d63e48..6c2b5f21b0 100644 --- a/src/courseware/course/Course.jsx +++ b/src/courseware/course/Course.jsx @@ -1,7 +1,8 @@ -import React from 'react'; +import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { useDispatch } from 'react-redux'; +import Cookies from 'js-cookie'; import { getConfig } from '@edx/frontend-platform'; import { AlertList } from '../../generic/user-messages'; @@ -13,8 +14,11 @@ import Sequence from './sequence'; import { CelebrationModal, shouldCelebrateOnSectionLoad } from './celebration'; import ContentTools from './content-tools'; import CourseBreadcrumbs from './CourseBreadcrumbs'; +import SidebarNotificationButton from './SidebarNotificationButton'; + import CourseSock from '../../generic/course-sock'; import { useModel } from '../../generic/model-store'; +import useWindowSize, { responsiveBreakpoints } from '../../generic/tabs/useWindowSize'; /** [MM-P2P] Experiment */ import { initCoursewareMMP2P, MMP2PBlockModal } from '../../experiments/mm-p2p'; @@ -57,6 +61,19 @@ function Course({ courseId, sequenceId, unitId, celebrateFirstSection, dispatch, celebrations, ); + // REV-2130 TODO: temporary cookie code that should be removed. + // In order to see the Value Prop sidebar in prod, a cookie should be set in + // the browser console and refresh: document.cookie = 'value_prop_cookie=true'; + const isValuePropCookieSet = Cookies.get('value_prop_cookie') === 'true'; + + const shouldDisplaySidebarButton = useWindowSize().width >= responsiveBreakpoints.small.minWidth; + + const [sidebarVisible, setSidebar] = useState(false); + const isSidebarVisible = () => sidebarVisible && setSidebar; + const toggleSidebar = () => { + if (sidebarVisible) { setSidebar(false); } else { setSidebar(true); } + }; + /** [MM-P2P] Experiment */ const MMP2P = initCoursewareMMP2P(courseId, sequenceId, unitId); @@ -76,13 +93,23 @@ function Course({ }} /> )} - +
+ + + { isValuePropCookieSet && shouldDisplaySidebarButton ? ( + + ) : null} +
+ diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx index 389ce8ddd5..4edd9a1c90 100644 --- a/src/courseware/course/Course.test.jsx +++ b/src/courseware/course/Course.test.jsx @@ -1,5 +1,6 @@ import React from 'react'; import { Factory } from 'rosie'; +import Cookies from 'js-cookie'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { loadUnit, render, screen, waitFor, getByRole, initializeTestStore, fireEvent, @@ -19,6 +20,7 @@ describe('Course', () => { nextSequenceHandler: () => {}, previousSequenceHandler: () => {}, unitNavigationHandler: () => {}, + toggleSidebar: () => {}, }; beforeAll(async () => { @@ -85,6 +87,31 @@ describe('Course', () => { expect(screen.getByRole('button', { name: 'Learn About Verified Certificates' })).toBeInTheDocument(); }); + it('displays sidebar notification button', async () => { + const toggleSidebar = jest.fn(); + const isSidebarVisible = jest.fn(); + + // REV-2130 TODO: remove cookie related code once temporary value prop cookie code is removed. + const cookieName = 'value_prop_cookie'; + Cookies.set = jest.fn(); + Cookies.get = jest.fn().mockImplementation(() => cookieName); + const getSpy = jest.spyOn(Cookies, 'get').mockReturnValueOnce('true'); + + const courseMetadata = Factory.build('courseMetadata'); + const testStore = await initializeTestStore({ courseMetadata, excludeFetchSequence: true }, false); + const testData = { + ...mockData, + toggleSidebar, + isSidebarVisible, + }; + render(, { store: testStore }); + + const sidebarOpenButton = screen.getByRole('button', { name: /Show sidebar notification/i }); + + expect(getSpy).toBeCalledWith(cookieName); + expect(sidebarOpenButton).toBeInTheDocument(); + }); + it('displays offer and expiration alert', async () => { const courseMetadata = Factory.build('courseMetadata', { access_expiration: { diff --git a/src/courseware/course/CourseBreadcrumbs.jsx b/src/courseware/course/CourseBreadcrumbs.jsx index a90dd5e4b3..d8a73c8d70 100644 --- a/src/courseware/course/CourseBreadcrumbs.jsx +++ b/src/courseware/course/CourseBreadcrumbs.jsx @@ -60,7 +60,7 @@ export default function CourseBreadcrumbs({ }, [courseStatus, sequenceStatus]); return ( -