diff --git a/src/App.jsx b/src/App.jsx index 101f34e..672ecae 100755 --- a/src/App.jsx +++ b/src/App.jsx @@ -2,9 +2,10 @@ import React from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import { useDispatch } from 'react-redux'; +import { AppContext } from '@edx/frontend-platform/react'; import Footer from '@edx/frontend-component-footer'; -import { actions } from 'data/redux'; +import { thunkActions } from 'data/redux'; import fakeData from 'data/services/lms/fakeData/courses'; import LearnerDashboardHeader from 'containers/LearnerDashboardHeader'; import Dashboard from 'containers/Dashboard'; @@ -14,16 +15,22 @@ import './App.scss'; export const App = () => { const dispatch = useDispatch(); // TODO: made development-only + const { authenticatedUser } = React.useContext(AppContext); React.useEffect(() => { - window.loadMockData = () => { - dispatch(actions.app.loadGlobalData(fakeData.globalData)); - dispatch(actions.app.loadCourses({ - courses: [ - ...fakeData.courseRunData, - ...fakeData.entitlementData, - ], - })); - }; + if (authenticatedUser?.administrator || process.env.NODE_ENV === 'development') { + window.loadEmptyData = () => { + dispatch(thunkActions.app.loadData({ ...fakeData.globalData, courses: [] })); + }; + window.loadMockData = () => { + dispatch(thunkActions.app.loadData({ + ...fakeData.globalData, + courses: [ + ...fakeData.courseRunData, + ...fakeData.entitlementData, + ], + })); + }; + } }); return ( diff --git a/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx index 6cfcc24..302ef60 100644 --- a/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/BeginCourseButton.test.jsx @@ -1,36 +1,55 @@ import { shallow } from 'enzyme'; +import { htmlProps } from 'testKeys'; import { hooks } from 'data/redux'; import BeginCourseButton from './BeginCourseButton'; jest.mock('data/redux', () => ({ hooks: { - useCardCourseRunData: jest.fn(), - useCardEnrollmentData: jest.fn(), + useCardCourseRunData: jest.fn(() => ({ homeUrl: 'home-url' })), + useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })), + useMasqueradeData: jest.fn(() => ({ isMasquerading: false })), }, })); +let wrapper; +const { homeUrl } = hooks.useCardCourseRunData(); + describe('BeginCourseButton', () => { const props = { cardId: 'cardId', }; - hooks.useCardCourseRunData.mockReturnValue({ - homeUrl: 'homeUrl', + beforeEach(() => { + jest.clearAllMocks(); }); describe('snapshot', () => { test('renders default button when learner has access to the course', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess: true, - }); - const wrapper = shallow(); + wrapper = shallow(); expect(wrapper).toMatchSnapshot(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(false); + expect(wrapper.prop(htmlProps.href)).toEqual(homeUrl); }); - test('renders disabled button when learner does not have access to the course', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess: false, + }); + describe('behavior', () => { + it('initializes course run data with cardId', () => { + wrapper = shallow(); + expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); + }); + it('initializes enrollment data with cardId', () => { + wrapper = shallow(); + expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId); + }); + describe('disabled states', () => { + test('learner does not have access', () => { + hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('masquerading', () => { + hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); }); }); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx index 7c09529..36a1267 100644 --- a/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/ResumeButton.test.jsx @@ -1,56 +1,68 @@ import { shallow } from 'enzyme'; +import { htmlProps } from 'testKeys'; import { hooks } from 'data/redux'; import ResumeButton from './ResumeButton'; jest.mock('data/redux', () => ({ hooks: { - useCardCourseRunData: jest.fn(), - useCardEnrollmentData: jest.fn(), + useCardCourseRunData: jest.fn(() => ({ resumeUrl: 'resumeUrl' })), + useCardEnrollmentData: jest.fn(() => ({ + hasAccess: true, + isAudit: true, + isAuditAccessExpired: false, + })), + useMasqueradeData: jest.fn(() => ({ isMasquerading: false })), }, })); +const { resumeUrl } = hooks.useCardCourseRunData(); + describe('ResumeButton', () => { const props = { cardId: 'cardId', }; - hooks.useCardCourseRunData.mockReturnValue({ - resumeUrl: 'resumeUrl', - }); describe('snapshot', () => { test('renders default button when learner has access to the course', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess: true, - }); - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - test('renders disabled button when learner does not have access to the course', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess: false, - }); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(false); + expect(wrapper.prop(htmlProps.href)).toEqual(resumeUrl); }); }); describe('behavior', () => { - it('renders disabled button when audit access expired', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess: true, - isAudit: true, - isAuditAccessExpired: true, - }); - const wrapper = shallow(); - expect(wrapper.prop('disabled')).toEqual(true); + it('initializes course run data based on cardId', () => { + shallow(); + expect(hooks.useCardCourseRunData).toHaveBeenCalledWith(props.cardId); }); - it('renders enabled button when audit access not expired', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess: true, - isAudit: true, - isAuditAccessExpired: false, + it('initializes course enrollment data based on cardId', () => { + shallow(); + expect(hooks.useCardEnrollmentData).toHaveBeenCalledWith(props.cardId); + }); + describe('disabled states', () => { + test('masquerading', () => { + hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + const wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('learner does not have access', () => { + hooks.useCardEnrollmentData.mockReturnValueOnce({ + hasAccess: false, + isAudit: true, + isAuditAccessExpired: false, + }); + const wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('audit access expired', () => { + hooks.useCardEnrollmentData.mockReturnValueOnce({ + hasAccess: true, + isAudit: true, + isAuditAccessExpired: true, + }); + const wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); - const wrapper = shallow(); - expect(wrapper.prop('disabled')).toEqual(false); }); }); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx b/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx index 88767a6..54a9382 100644 --- a/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.jsx @@ -9,7 +9,6 @@ import { hooks } from 'data/redux'; import messages from './messages'; export const SelectSessionButton = ({ cardId }) => { - const { resumeUrl } = hooks.useCardCourseRunData(cardId); const { hasAccess } = hooks.useCardEnrollmentData(cardId); const { canChange, hasSessions } = hooks.useCardEntitlementData(cardId); const { isMasquerading } = hooks.useMasqueradeData(); @@ -20,8 +19,6 @@ export const SelectSessionButton = ({ cardId }) => { diff --git a/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.test.jsx index d0d8244..d3d853c 100644 --- a/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/SelectSessionButton.test.jsx @@ -1,61 +1,66 @@ import { shallow } from 'enzyme'; import { hooks } from 'data/redux'; +import { htmlProps } from 'testKeys'; import SelectSessionButton from './SelectSessionButton'; jest.mock('data/redux', () => ({ hooks: { - useCardCourseRunData: jest.fn(), - useCardEnrollmentData: jest.fn(), - useCardEntitlementData: jest.fn(), + useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })), + useCardEntitlementData: jest.fn(() => ({ canChange: true, hasSessions: true })), + useMasqueradeData: jest.fn(() => ({ isMasquerading: false })), useUpdateSelectSessionModalCallback: () => jest.fn().mockName('mockOpenSessionModal'), }, })); +let wrapper; + describe('SelectSessionButton', () => { - const props = { - cardId: 'cardId', - }; - hooks.useCardCourseRunData.mockReturnValue({ - resumeUrl: 'resumeUrl', - }); - const createWrapper = ({ hasAccess, canChange, hasSessions }) => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ - hasAccess, - }); - hooks.useCardEntitlementData.mockReturnValueOnce({ - canChange, - hasSessions, - }); - return shallow(); - }; + const props = { cardId: 'cardId' }; describe('snapshot', () => { test('renders default button', () => { - const wrapper = createWrapper({ hasAccess: true, canChange: true, hasSessions: true }); + wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); it('renders disabled button when user does not have access to the course', () => { - const wrapper = createWrapper({ hasAccess: false, canChange: true, hasSessions: true }); + hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false }); + wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + it('renders disabled button if masquerading', () => { + hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); describe('behavior', () => { it('default render', () => { - const wrapper = createWrapper({ hasAccess: true, canChange: true, hasSessions: true }); - expect(wrapper.prop('disabled')).toEqual(false); - expect(wrapper.prop('href')).toEqual('resumeUrl'); - expect(wrapper.prop('onClick').getMockName()) + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(false); + expect(wrapper.prop(htmlProps.onClick).getMockName()) .toEqual(hooks.useUpdateSelectSessionModalCallback().getMockName()); }); - it('disabled if learner doesn\'t have access, cannot change sessions, or does not have sessions', () => { - const noAccess = createWrapper({ hasAccess: false, canChange: true, hasSessions: true }); - expect(noAccess.prop('disabled')).toEqual(true); - - const cannotChange = createWrapper({ hasAccess: true, canChange: false, hasSessions: true }); - expect(cannotChange.prop('disabled')).toEqual(true); - - const noSessions = createWrapper({ hasAccess: true, canChange: true, hasSessions: false }); - expect(noSessions.prop('disabled')).toEqual(true); + describe('disabled states', () => { + test('learner does not have access', () => { + hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('learner cannot change sessions', () => { + hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: false, hasSessions: true }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('entitlement does not have available sessions', () => { + hooks.useCardEntitlementData.mockReturnValueOnce({ canChange: true, hasSessions: false }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('user is masquerading', () => { + hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); }); }); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx index 1add552..aaa5e5f 100644 --- a/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/UpgradeButton.test.jsx @@ -1,12 +1,14 @@ import { shallow } from 'enzyme'; +import { htmlProps } from 'testKeys'; import { hooks } from 'data/redux'; import UpgradeButton from './UpgradeButton'; jest.mock('data/redux', () => ({ hooks: { + useMasqueradeData: jest.fn(() => ({ isMasquerading: false })), useCardCourseRunData: jest.fn(), - useCardEnrollmentData: jest.fn(), + useCardEnrollmentData: jest.fn(() => ({ canUpgrade: true })), }, })); @@ -18,14 +20,21 @@ describe('UpgradeButton', () => { hooks.useCardCourseRunData.mockReturnValue({ upgradeUrl }); describe('snapshot', () => { test('can upgrade', () => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: true }); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(false); }); test('cannot upgrade', () => { hooks.useCardEnrollmentData.mockReturnValueOnce({ canUpgrade: false }); const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('masquerading', () => { + hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); }); }); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx index e75c608..74bfe15 100644 --- a/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx +++ b/src/containers/CourseCard/components/CourseCardActions/ViewCourseButton.test.jsx @@ -1,49 +1,53 @@ import { shallow } from 'enzyme'; +import { htmlProps } from 'testKeys'; import { hooks } from 'data/redux'; import ViewCourseButton from './ViewCourseButton'; jest.mock('data/redux', () => ({ hooks: { - useCardCourseRunData: jest.fn(), - useCardEnrollmentData: jest.fn(), - useCardEntitlementData: jest.fn(), + useCardCourseRunData: jest.fn(() => ({ marketingUrl: 'marketing-url' })), + useCardEnrollmentData: jest.fn(() => ({ hasAccess: true })), + useCardEntitlementData: jest.fn(() => ({ isEntitlement: false, isExpired: false })), + useMasqueradeData: jest.fn(() => ({ isMasquerading: false })), }, })); +let wrapper; + describe('ViewCourseButton', () => { const props = { cardId: 'cardId', }; - const marketingUrl = 'marketingUrl'; - hooks.useCardCourseRunData.mockReturnValue({ marketingUrl }); - const createWrapper = ({ hasAccess, isEntitlement, isExpired }) => { - hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess }); - hooks.useCardEntitlementData.mockReturnValueOnce({ isEntitlement, isExpired }); - return shallow(); - }; + const { marketingUrl } = hooks.useCardCourseRunData(); describe('snapshot', () => { test('default button', () => { - const wrapper = createWrapper({ hasAccess: true, isEntitlement: false, isExpired: false }); - expect(wrapper).toMatchSnapshot(); - }); - test('disabled button', () => { - const wrapper = createWrapper({ hasAccess: false, isEntitlement: false, isExpired: false }); + wrapper = shallow(); expect(wrapper).toMatchSnapshot(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(false); + expect(wrapper.prop(htmlProps.href)).toEqual(marketingUrl); }); }); describe('behavior', () => { - it('disabled button without access', () => { - const wrapper = createWrapper({ hasAccess: false, isEntitlement: false, isExpired: false }); - expect(wrapper.prop('disabled')).toEqual(true); - }); - it('disabled button with access', () => { - const wrapper = createWrapper({ hasAccess: true, isEntitlement: true, isExpired: true }); - expect(wrapper.prop('disabled')).toEqual(true); - }); - it('enabled button', () => { - const wrapper = createWrapper({ hasAccess: true, isEntitlement: false, isExpired: false }); - expect(wrapper.prop('disabled')).toEqual(false); + describe('disabled states', () => { + test('learner does not have access', () => { + hooks.useCardEnrollmentData.mockReturnValueOnce({ hasAccess: false }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('expired entitlement', () => { + hooks.useCardEntitlementData.mockReturnValueOnce({ + isEntitlement: true, + isExpired: true, + }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); + test('masquerading', () => { + hooks.useMasqueradeData.mockReturnValueOnce({ isMasquerading: true }); + wrapper = shallow(); + expect(wrapper.prop(htmlProps.disabled)).toEqual(true); + }); }); }); }); diff --git a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/BeginCourseButton.test.jsx.snap b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/BeginCourseButton.test.jsx.snap index e4e5e0a..33b14b9 100644 --- a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/BeginCourseButton.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/BeginCourseButton.test.jsx.snap @@ -4,17 +4,7 @@ exports[`BeginCourseButton snapshot renders default button when learner has acce -`; - -exports[`BeginCourseButton snapshot renders disabled button when learner does not have access to the course 1`] = ` - diff --git a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ResumeButton.test.jsx.snap b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ResumeButton.test.jsx.snap index 6a5fa40..b1687b4 100644 --- a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ResumeButton.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ResumeButton.test.jsx.snap @@ -3,16 +3,7 @@ exports[`ResumeButton snapshot renders default button when learner has access to the course 1`] = ` -`; - -exports[`ResumeButton snapshot renders disabled button when learner does not have access to the course 1`] = ` - +`; + +exports[`SelectSessionButton snapshot renders disabled button if masquerading 1`] = ` + `; + +exports[`UpgradeButton snapshot masquerading 1`] = ` + +`; diff --git a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap index 3cb251f..832f22c 100644 --- a/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap +++ b/src/containers/CourseCard/components/CourseCardActions/__snapshots__/ViewCourseButton.test.jsx.snap @@ -4,17 +4,7 @@ exports[`ViewCourseButton snapshot default button 1`] = ` -`; - -exports[`ViewCourseButton snapshot disabled button 1`] = ` - diff --git a/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap b/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap index dbc3ae1..35d4e79 100644 --- a/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap +++ b/src/containers/CourseCard/components/RelatedProgramsBadge/__snapshots__/index.test.jsx.snap @@ -3,13 +3,17 @@ exports[`RelatedProgramsBadge component snapshot: 3 programs 1`] = `

My Courses

@@ -30,14 +30,7 @@ exports[`CourseList snapshots with filters 1`] = `
- -
+ /> `; @@ -49,7 +42,7 @@ exports[`CourseList snapshots with multiple courses and pages 1`] = ` id="course-list-heading-container" >

My Courses

@@ -76,6 +69,7 @@ exports[`CourseList snapshots with multiple courses and pages 1`] = ` key="baz" />

My Courses

@@ -106,13 +100,6 @@ exports[`CourseList snapshots with no filters 1`] = `
- -
+ /> `; diff --git a/src/containers/CourseList/hooks.js b/src/containers/CourseList/hooks.js index 84162a0..af4b3c0 100644 --- a/src/containers/CourseList/hooks.js +++ b/src/containers/CourseList/hooks.js @@ -1,31 +1,34 @@ import React from 'react'; +import { useDispatch } from 'react-redux'; import { useCheckboxSetValues } from '@edx/paragon'; import { StrictDict } from 'utils'; -import { hooks as appHooks } from 'data/redux'; +import { actions, hooks as appHooks } from 'data/redux'; import { ListPageSize, SortKeys } from 'data/constants/app'; import * as module from './hooks'; export const state = StrictDict({ - pageNumber: (val) => React.useState(val), // eslint-disable-line sortBy: (val) => React.useState(val), // eslint-disable-line }); export const useCourseListData = () => { - const [pageNumber, setPageNumber] = module.state.pageNumber(1); - const [sortBy, setSortBy] = module.state.sortBy(SortKeys.title); + const dispatch = useDispatch(); + const pageNumber = appHooks.usePageNumber(); const [filters, setFilters] = useCheckboxSetValues([]); + const [sortBy, setSortBy] = module.state.sortBy(SortKeys.title); + const { numPages, visible } = appHooks.useCurrentCourseList({ sortBy, isAscending: true, filters, - pageNumber, pageSize: ListPageSize, }); const handleRemoveFilter = (filter) => () => setFilters.remove(filter); + const setPageNumber = (value) => dispatch(actions.app.setPageNumber(value)); return { + pageNumber, numPages, setPageNumber, visibleList: visible, diff --git a/src/containers/CourseList/hooks.test.js b/src/containers/CourseList/hooks.test.js index 726c812..dfc9c22 100644 --- a/src/containers/CourseList/hooks.test.js +++ b/src/containers/CourseList/hooks.test.js @@ -1,55 +1,102 @@ -import { MockUseState } from 'testUtils'; +import { useDispatch } from 'react-redux'; +import * as paragon from '@edx/paragon'; -import { hooks as appHooks } from 'data/redux'; +import { MockUseState } from 'testUtils'; +import { actions, hooks as appHooks } from 'data/redux'; +import { ListPageSize, SortKeys } from 'data/constants/app'; import * as hooks from './hooks'; jest.mock('data/redux', () => ({ + actions: { + app: { + setPageNumber: (value) => ({ setPageNumber: value }), + }, + }, hooks: { useCurrentCourseList: jest.fn(), + usePageNumber: jest.fn(() => 23), }, })); const state = new MockUseState(hooks); +const dispatch = useDispatch(); + +const testList = ['a', 'b']; +const testListData = { + numPages: 52, + visible: testList, +}; +const testSortBy = 'fake sort option'; +const testFilters = ['some', 'fake', 'filters']; +const testSetFilters = { add: jest.fn(), remove: jest.fn() }; +const testCheckboxSetValues = [testFilters, testSetFilters]; + describe('CourseList hooks', () => { let out; describe('state values', () => { - state.testGetter(state.keys.pageNumber); state.testGetter(state.keys.sortBy); + jest.clearAllMocks(); }); describe('useCourseListData', () => { - beforeEach(() => state.mock()); afterEach(state.restore); - - test('empty initializes', () => { - appHooks.useCurrentCourseList.mockReturnValueOnce({ - numPages: 1, - visible: [], - }); + beforeEach(() => { + state.mock(); + state.mockVal(state.keys.sortBy, testSortBy); + paragon.useCheckboxSetValues.mockImplementationOnce(() => testCheckboxSetValues); + appHooks.useCurrentCourseList.mockReturnValueOnce(testListData); out = hooks.useCourseListData(); - expect(out.numPages).toEqual(1); - expect(out.visibleList).toEqual([]); }); - - test('page count and visble list', () => { - const result = { - numPages: 2, - visible: ['a', 'b'], - }; - appHooks.useCurrentCourseList.mockReturnValueOnce(result); - out = hooks.useCourseListData(); - expect(out.numPages).toEqual(result.numPages); - expect(out.visibleList).toEqual(result.visible); - }); - test('handle remove filter', () => { - appHooks.useCurrentCourseList.mockReturnValueOnce({ - numPages: 1, - visible: [], + describe('behavior', () => { + it('initializes sort with title', () => { + state.expectInitializedWith(state.keys.sortBy, SortKeys.title); + }); + it('loads current course list with sortBy, isAscending, filters, and page size', () => { + expect(appHooks.useCurrentCourseList).toHaveBeenCalledWith({ + sortBy: testSortBy, + isAscending: true, + filters: testFilters, + pageSize: ListPageSize, + }); + }); + }); + describe('output', () => { + test('pageNumber loads from usePageNumber hook', () => { + expect(out.pageNumber).toEqual(appHooks.usePageNumber()); + }); + test('numPages and visible list load from useCurrentCourseList hook', () => { + expect(out.numPages).toEqual(testListData.numPages); + expect(out.visibleList).toEqual(testListData.visible); + }); + test('showFilters is true iff filters is not empty', () => { + expect(out.showFilters).toEqual(true); + state.mockVal(state.keys.sortBy, testSortBy); + appHooks.useCurrentCourseList.mockReturnValueOnce(testListData); + out = hooks.useCourseListData(); + // checkbox values default to returning a list, and were only overridden once. + // Thus this time, the values for the list should be empty. + expect(out.showFilters).toEqual(false); + }); + describe('filterOptions', () => { + test('sortBy and setSortBy are connected to the state value', () => { + expect(out.filterOptions.sortBy).toEqual(testSortBy); + expect(out.filterOptions.setSortBy).toEqual(state.setState.sortBy); + }); + test('filters and setFilters passed by useCheckboxSetValues', () => { + expect(out.filterOptions.filters).toEqual(testFilters); + expect(out.filterOptions.setFilters).toEqual(testSetFilters); + }); + test('handleRemoveFilter creates callback to call setFilter.remove', () => { + const cb = out.filterOptions.handleRemoveFilter(testFilters[0]); + expect(testSetFilters.remove).not.toHaveBeenCalled(); + cb(); + expect(testSetFilters.remove).toHaveBeenCalledWith(testFilters[0]); + }); + test('setPageNumber dispatches setPageNumber action with passed value', () => { + expect(out.setPageNumber(2)).toEqual(dispatch(actions.app.setPageNumber(2))); + }); }); - out = hooks.useCourseListData(); - out.filterOptions.handleRemoveFilter('a')(); - expect(out.filterOptions.setFilters.remove).toHaveBeenCalledWith('a'); }); }); }); diff --git a/src/containers/EmptyCourse/SuggestedCourses/__snapshots__/index.test.jsx.snap b/src/containers/EmptyCourse/SuggestedCourses/__snapshots__/index.test.jsx.snap index 962a281..62d818c 100644 --- a/src/containers/EmptyCourse/SuggestedCourses/__snapshots__/index.test.jsx.snap +++ b/src/containers/EmptyCourse/SuggestedCourses/__snapshots__/index.test.jsx.snap @@ -21,14 +21,20 @@ exports[`SuggestedCourses snapshot has 3 suggested courses 1`] = ` srcAlt="Course image banner" /> + - + + + + - + + + + - + + + diff --git a/src/containers/EmptyCourse/SuggestedCourses/index.jsx b/src/containers/EmptyCourse/SuggestedCourses/index.jsx index 2954dc6..04f9a5f 100644 --- a/src/containers/EmptyCourse/SuggestedCourses/index.jsx +++ b/src/containers/EmptyCourse/SuggestedCourses/index.jsx @@ -1,7 +1,12 @@ import React from 'react'; import { useIntl } from '@edx/frontend-platform/i18n'; -import { Card, Button, Container } from '@edx/paragon'; +import { + ActionRow, + Card, + Button, + Container, +} from '@edx/paragon'; import { hooks as appHooks } from 'data/redux'; import messages from './messages'; @@ -20,9 +25,12 @@ export const SuggestedCourses = () => { src={course.bannerImgSrc} srcAlt={formatMessage(messages.courseImageAlt)} /> - + + - + + + ))} diff --git a/src/containers/LearnerDashboardHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap b/src/containers/LearnerDashboardHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap index e619386..f5bd523 100644 --- a/src/containers/LearnerDashboardHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap +++ b/src/containers/LearnerDashboardHeader/__snapshots__/AuthenticatedUserDropdown.test.jsx.snap @@ -3,6 +3,7 @@ exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = ` Account - + Help @@ -70,6 +73,7 @@ exports[`AuthenticatedUserDropdown snapshots with enterprise dashboard 1`] = ` exports[`AuthenticatedUserDropdown snapshots without enterprise dashboard and expanded 1`] = ` Account - + Help diff --git a/src/containers/RelatedProgramsModal/__snapshots__/index.test.jsx.snap b/src/containers/RelatedProgramsModal/__snapshots__/index.test.jsx.snap index d6b8535..5298e8a 100644 --- a/src/containers/RelatedProgramsModal/__snapshots__/index.test.jsx.snap +++ b/src/containers/RelatedProgramsModal/__snapshots__/index.test.jsx.snap @@ -22,7 +22,7 @@ exports[`RelatedProgramsModal snapshot: closed 1`] = ` className="programs-header p-0" />

Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in @@ -105,7 +105,7 @@ exports[`RelatedProgramsModal snapshot: open 1`] = ` className="programs-header p-0" />

Are you looking to expand your knowledge? Enrolling in a Program lets you take a series of courses in the subject that you're interested in diff --git a/src/containers/RelatedProgramsModal/components/ProgramCard.jsx b/src/containers/RelatedProgramsModal/components/ProgramCard.jsx index 256d910..b05aaac 100644 --- a/src/containers/RelatedProgramsModal/components/ProgramCard.jsx +++ b/src/containers/RelatedProgramsModal/components/ProgramCard.jsx @@ -25,6 +25,8 @@ export const ProgramCard = ({ data }) => { {courseName} - +

{formatMessage(messages.description)}

diff --git a/src/containers/WidgetSidebar/__snapshots__/index.test.jsx.snap b/src/containers/WidgetSidebar/__snapshots__/index.test.jsx.snap index d14702c..26e3699 100644 --- a/src/containers/WidgetSidebar/__snapshots__/index.test.jsx.snap +++ b/src/containers/WidgetSidebar/__snapshots__/index.test.jsx.snap @@ -2,7 +2,7 @@ exports[`WidgetSidebar snapshots default 1`] = `
- Explore courses + , + } + } + /> diff --git a/src/data/redux/app/reducer.js b/src/data/redux/app/reducer.js index 2e73e6b..32e21b3 100644 --- a/src/data/redux/app/reducer.js +++ b/src/data/redux/app/reducer.js @@ -1,12 +1,14 @@ -import { StrictDict } from 'utils'; import { createSlice } from '@reduxjs/toolkit'; +import { StrictDict } from 'utils'; + const initialState = { + pageNumber: 1, enrollments: [], courseData: {}, entitlement: [], emailConfirmation: {}, - enterpriseDashboards: {}, + enterpriseDashboard: {}, platformSettings: {}, suggestedCourses: [], filterState: {}, @@ -41,6 +43,7 @@ const app = createSlice({ ...state, selectSessionModal: { cardId: payload }, }), + setPageNumber: (state, { payload }) => ({ ...state, pageNumber: payload }), }, }); diff --git a/src/data/redux/app/selectors.js b/src/data/redux/app/selectors.js index 46883d1..f4a1ed5 100644 --- a/src/data/redux/app/selectors.js +++ b/src/data/redux/app/selectors.js @@ -17,6 +17,7 @@ export const simpleSelectors = { emailConfirmation: mkSimpleSelector(app => app.emailConfirmation), enterpriseDashboard: mkSimpleSelector(app => app.enterpriseDashboard), selectSessionModal: mkSimpleSelector(app => app.selectSessionModal), + pageNumber: mkSimpleSelector(app => app.pageNumber), }; export const numCourses = createSelector( @@ -130,9 +131,9 @@ export const currentList = (state, { sortBy, isAscending, filters, - pageNumber, pageSize, }) => { + const pageNumber = module.simpleSelectors.pageNumber(state); let list = Object.values(module.simpleSelectors.courseData(state)); const hasFilter = filters.reduce((obj, filter) => ({ ...obj, [filter]: true }), {}); if (filters.length) { diff --git a/src/data/redux/hooks.js b/src/data/redux/hooks.js index bdb0bcd..f24e325 100644 --- a/src/data/redux/hooks.js +++ b/src/data/redux/hooks.js @@ -6,6 +6,7 @@ import requestSelectors from './requests/selectors'; const { courseCard } = appSelectors; +export const usePageNumber = () => useSelector(appSelectors.pageNumber); export const useEmailConfirmationData = () => useSelector(appSelectors.emailConfirmation); export const useEnterpriseDashboardData = () => useSelector(appSelectors.enterpriseDashboard); export const usePlatformSettingsData = () => useSelector(appSelectors.platformSettings); diff --git a/src/data/redux/thunkActions/app.js b/src/data/redux/thunkActions/app.js index 5655239..147796a 100644 --- a/src/data/redux/thunkActions/app.js +++ b/src/data/redux/thunkActions/app.js @@ -11,25 +11,25 @@ import requests from './requests'; // import { } from './requests'; import * as module from './app'; +export const loadData = ({ courses, ...globalData }) => dispatch => { + dispatch(actions.app.setPageNumber(1)); + dispatch(actions.app.loadGlobalData(globalData)); + dispatch(actions.app.loadCourses({ courses })); +}; + /** * initialize the app, loading ora and course metadata from the api, and loading the initial * submission list data. */ export const initialize = () => (dispatch) => ( dispatch(requests.initializeList({ - onSuccess: (({ courses, ...globalData }) => { - dispatch(actions.app.loadGlobalData(globalData)); - dispatch(actions.app.loadCourses({ courses })); - }), + onSuccess: (response) => dispatch(module.loadData(response)), })) ); export const refreshList = () => (dispatch) => ( dispatch(requests.initializeList({ - onSuccess: (({ courses, ...globalData }) => { - dispatch(actions.app.loadGlobalData(globalData)); - dispatch(actions.app.loadCourses({ courses })); - }), + onSuccess: (response) => dispatch(module.loadData(response)), })) ); @@ -95,6 +95,7 @@ export const clearMasquerade = () => (dispatch) => { }; export default StrictDict({ + loadData, initialize, refreshList, sendConfirmEmail, diff --git a/src/data/services/lms/fakeData/courses.js b/src/data/services/lms/fakeData/courses.js index 5a5e9ce..1cc065c 100644 --- a/src/data/services/lms/fakeData/courses.js +++ b/src/data/services/lms/fakeData/courses.js @@ -25,6 +25,25 @@ export const relatedPrograms = [ programTypeUrl: 'www.edx/my-program-type', numberOfCourses: 3, }, + { + provider: 'HarvardX', + bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/course/image/327c8e4f-315a-417b-9857-046dfc90c243-677b97464958.small.jpg', + logoImgSrc: 'https://prod-discovery.edx-cdn.org/organization/certificate_logos/44022f13-20df-4666-9111-cede3e5dc5b6-770e00385e7e.png', + title: 'Relativity in Modern Mechanics', + programUrl: 'www.edx/my-program-3', + programType: 'MicroBachelors Program', + numberOfCourses: 3, + }, + { + provider: 'University of Maryland', + bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg', + logoImgSrc: 'https://prod-discovery.edx-cdn.org/organization/certificate_logos/b9dc96da-b3fc-45a6-b6b7-b8e12eb79335-ac60112330e3.png', + title: 'Pandering for Modern Professionals', + programUrl: 'www.edx/my-program-4', + programType: 'MicroBachelors Program', + programTypeUrl: 'www.edx/my-program-type', + numberOfCourses: 3, + }, ]; export const genCardId = (index) => `card-id${index}`; @@ -62,7 +81,7 @@ const globalData = { }, { bannerImgSrc: 'https://prod-discovery.edx-cdn.org/media/programs/banner_images/9a310b98-8f27-439e-be85-12d6460245c9-f2efca129273.small.jpg', - courseName: 'Suggested course 2', + courseName: 'Suggested course 2 with a really really really long name for some reason', courseUrl: 'www.edx/suggested-course', }, { @@ -700,14 +719,6 @@ export const entitlementData = entitlementCourses.map( }, ); -console.log('%j', { - courses: [ - ...courseRunData, - ...entitlementData, - ], - ...globalData, -}); - export default { courseRunData, entitlementData, diff --git a/src/testKeys.js b/src/testKeys.js new file mode 100644 index 0000000..d2c6c1f --- /dev/null +++ b/src/testKeys.js @@ -0,0 +1,13 @@ +import { StrictDict } from 'utils'; + +export const htmlProps = StrictDict({ + disabled: 'disabled', + href: 'href', + onClick: 'onClick', + onChange: 'onChange', + onBlur: 'onBlur', +}); + +export default { + htmlProps, +};