diff --git a/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap b/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap deleted file mode 100644 index c90f819697..0000000000 --- a/src/courseware/course/sequence/Unit/__snapshots__/index.test.jsx.snap +++ /dev/null @@ -1,65 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Unit component output BookmarkButton props bookmarked, bookmark update pending snapshot 1`] = ` - -`; - -exports[`Unit component output BookmarkButton props not bookmarked, bookmark update loading snapshot 1`] = ` - -`; - -exports[`Unit component output snapshot: not bookmarked, do not show content 1`] = ` -
-
-
-

- unit-title -

- -
-
-

- Level 2 headings may be created by course providers in the future. -

- - - -
-`; diff --git a/src/courseware/course/sequence/Unit/index.jsx b/src/courseware/course/sequence/Unit/index.jsx index 660ccd6ed5..37eb396d88 100644 --- a/src/courseware/course/sequence/Unit/index.jsx +++ b/src/courseware/course/sequence/Unit/index.jsx @@ -8,7 +8,6 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import { useModel } from '@src/generic/model-store'; import { usePluginsCallback } from '@src/generic/plugin-store'; -import BookmarkButton from '../../bookmark/BookmarkButton'; import messages from '../messages'; import ContentIFrame from './ContentIFrame'; import UnitSuspense from './UnitSuspense'; @@ -33,7 +32,6 @@ const Unit = ({ const examAccess = useExamAccess({ id }); const shouldDisplayHonorCode = useShouldDisplayHonorCode({ courseId, id }); const unit = useModel(modelKeys.units, id); - const isProcessing = unit.bookmarkedUpdateState === 'loading'; const view = authenticatedUser ? views.student : views.public; const shouldDisplayUnitPreview = pathname.startsWith('/preview') && isOriginalUserStaff; @@ -50,19 +48,7 @@ const Unit = ({ return (
-
-
-

{unit.title}

- -
- {isEnabledOutlineSidebar && renderUnitNavigation(true)} -
-

{formatMessage(messages.headerPlaceholder)}

- + ({ useUnitData: jest.fn() })); -jest.mock('react-router-dom'); - -jest.mock('@edx/frontend-platform/i18n', () => { - const utils = jest.requireActual('@edx/react-unit-test-utils/dist'); - return { - useIntl: () => ({ formatMessage: utils.formatMessage }), - defineMessages: m => m, - }; -}); - -jest.mock('@src/generic/PageLoading', () => 'PageLoading'); -jest.mock('../../bookmark/BookmarkButton', () => 'BookmarkButton'); -jest.mock('./ContentIFrame', () => 'ContentIFrame'); -jest.mock('./UnitSuspense', () => 'UnitSuspense'); -jest.mock('../honor-code', () => 'HonorCode'); -jest.mock('../lock-paywall', () => 'LockPaywall'); - -jest.mock('@src/generic/model-store', () => ({ - useModel: jest.fn(), -})); - -jest.mock('react', () => ({ - ...jest.requireActual('react'), - useContext: jest.fn(v => v), -})); - -jest.mock('./hooks', () => ({ - useExamAccess: jest.fn(), - useShouldDisplayHonorCode: jest.fn(), -})); - -jest.mock('./urls', () => ({ - getIFrameUrl: jest.fn(), -})); +import { views } from './constants'; +import Unit from '.'; -const props = { +const defaultProps = { courseId: 'test-course-id', format: 'test-format', onLoaded: jest.fn().mockName('props.onLoaded'), - id: 'test-props-id', - isStaff: false, -}; - -const context = { authenticatedUser: { test: 'user' } }; -React.useContext.mockReturnValue(context); - -const examAccess = { - accessToken: 'test-token', - blockAccess: false, + id: 'unit-id', + isOriginalUserStaff: false, + isEnabledOutlineSidebar: false, + renderUnitNavigation: jest.fn(enabled => enabled && 'UnitNaviagtion'), }; -hooks.useExamAccess.mockReturnValue(examAccess); -hooks.useShouldDisplayHonorCode.mockReturnValue(false); const unit = { id: 'unit-id', @@ -74,134 +24,99 @@ const unit = { bookmarked: false, bookmarkedUpdateState: 'pending', }; -const mockCoursewareMetaFn = jest.fn(() => ({ wholeCourseTranslationEnabled: false })); -const mockUnitsFn = jest.fn(() => unit); - -when(useModel) - .calledWith('courseHomeMeta', props.courseId) - .mockImplementation(mockCoursewareMetaFn) - .calledWith(modelKeys.units, props.id) - .mockImplementation(mockUnitsFn); - -let el; -describe('Unit component', () => { - const searchParams = { get: (prop) => prop }; - const setSearchParams = jest.fn(); - - beforeEach(() => { - useSearchParams.mockImplementation(() => [searchParams, setSearchParams]); - useLocation.mockImplementation(() => ({ pathname: `/course/${props.courseId}` })); - jest.clearAllMocks(); - el = shallow(); + +let store; + +const renderComponent = (props) => { + render( + + + , + { store, wrapWithRouter: false }, + ); +}; + +initializeMockApp(); + +async function setupStoreState() { + const courseMetadata = Factory.build('courseMetadata'); + const unitBlocks = [Factory.build( + 'block', + { type: 'vertical', ...unit }, + { courseId: courseMetadata.id }, + )]; + + store = await initializeTestStore({ courseMetadata, unitBlocks }); +} + +describe('', () => { + beforeEach(async () => { + await setupStoreState(); }); - describe('behavior', () => { - it('initializes hooks', () => { - expect(hooks.useShouldDisplayHonorCode).toHaveBeenCalledWith({ - courseId: props.courseId, - id: props.id, - }); + + describe('unit title', () => { + it('has two children', () => { + renderComponent(defaultProps); + const unitTitleWrapper = screen.getByTestId('unit_title_slot').children[0]; + + expect(unitTitleWrapper.children).toHaveLength(3); + }); + + it('renders bookmark button', () => { + renderComponent(defaultProps); + + expect(screen.getByText('Bookmark this page')).toBeInTheDocument(); + }); + + it('does not render unit navigation buttons', () => { + renderComponent(defaultProps); + + const nextButton = screen.queryByText('UnitNaviagtion'); + + expect(nextButton).toBeNull(); + }); + + it('renders unit navigation buttons when isEnabledOutlineSidebar is true', () => { + const props = { ...defaultProps, isEnabledOutlineSidebar: true }; + renderComponent(props); + + const nextButton = screen.getByText('UnitNaviagtion'); + + expect(nextButton).toBeVisible(); }); }); - describe('output', () => { - let component; - test('snapshot: not bookmarked, do not show content', () => { - el = shallow(); - expect(el.snapshot).toMatchSnapshot(); + + describe('UnitSuspense', () => { + it('renders loading message', () => { + renderComponent(defaultProps); + + expect(screen.getByText('Loading', { exact: false })).toBeInTheDocument(); }); - describe('BookmarkButton props', () => { - const renderComponent = () => { - el = shallow(); - [component] = el.instance.findByType(BookmarkButton); - }; - describe('not bookmarked, bookmark update loading', () => { - beforeEach(() => { - useModel.mockReturnValueOnce({ ...unit, bookmarkedUpdateState: 'loading' }); - renderComponent(); - }); - test('snapshot', () => { - expect(component.snapshot).toMatchSnapshot(); - }); - test('props', () => { - expect(component.props.isBookmarked).toEqual(false); - expect(component.props.isProcessing).toEqual(true); - expect(component.props.unitId).toEqual(unit.id); - }); - }); - describe('bookmarked, bookmark update pending', () => { - beforeEach(() => { - mockUnitsFn.mockReturnValueOnce({ ...unit, bookmarked: true }); - renderComponent(); - }); - test('snapshot', () => { - expect(component.snapshot).toMatchSnapshot(); - }); - test('props', () => { - expect(component.props.isBookmarked).toEqual(true); - expect(component.props.isProcessing).toEqual(false); - expect(component.props.unitId).toEqual(unit.id); - }); - }); + }); + + describe('ContentIFrame', () => { + let iframe; + beforeEach(() => { + renderComponent(defaultProps); + iframe = screen.getByTestId('content-iframe-test-id'); }); - test('UnitSuspense props', () => { - el = shallow(); - [component] = el.instance.findByType(UnitSuspense); - expect(component.props.courseId).toEqual(props.courseId); - expect(component.props.id).toEqual(props.id); + + it('renders content iframe', () => { + expect(iframe).toBeVisible(); }); - describe('ContentIFrame props', () => { - const testComponentProps = () => { - expect(component.props.elementId).toEqual('unit-iframe'); - expect(component.props.id).toEqual(props.id); - expect(component.props.loadingMessage).toEqual(formatMessage(messages.loadingSequence)); - expect(component.props.onLoaded).toEqual(props.onLoaded); - expect(component.props.title).toEqual(unit.title); - }; - const loadComponent = () => { - el = shallow(); - [component] = el.instance.findByType(ContentIFrame); - }; - describe('shouldShowContent', () => { - test('do not show content if displaying honor code', () => { - hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true); - loadComponent(); - testComponentProps(); - expect(component.props.shouldShowContent).toEqual(false); - }); - test('do not show content if examAccess is blocked', () => { - hooks.useExamAccess.mockReturnValueOnce({ ...examAccess, blockAccess: true }); - loadComponent(); - testComponentProps(); - expect(component.props.shouldShowContent).toEqual(false); - }); - test('show content if not displaying honor code or blocked by exam access', () => { - loadComponent(); - testComponentProps(); - expect(component.props.shouldShowContent).toEqual(true); - }); - }); - describe('iframeUrl', () => { - test('loads iframe url with student view if authenticated user', () => { - loadComponent(); - testComponentProps(); - expect(component.props.iframeUrl).toEqual(getIFrameUrl({ - id: props.id, - view: views.student, - format: props.format, - examAccess, - })); - }); - test('loads iframe url with public view if no authenticated user', () => { - React.useContext.mockReturnValueOnce({}); - loadComponent(); - testComponentProps(); - expect(component.props.iframeUrl).toEqual(getIFrameUrl({ - id: props.id, - view: views.public, - format: props.format, - examAccess, - })); - }); - }); + + it('generates correct iframeUrl', () => { + expect(iframe.getAttribute('src')).toEqual(getIFrameUrl({ + id: defaultProps.id, + view: views.student, + format: defaultProps.format, + examAccess: { + accessToken: '', + blockAccess: false, + }, + jumpToId: null, + preview: 0, + })); }); }); }); diff --git a/src/plugin-slots/UnitTitleSlot/README.md b/src/plugin-slots/UnitTitleSlot/README.md index b6f1a137c9..6daf3b736d 100644 --- a/src/plugin-slots/UnitTitleSlot/README.md +++ b/src/plugin-slots/UnitTitleSlot/README.md @@ -2,19 +2,20 @@ ### Slot ID: `unit_title_slot` ### Props: -* `courseId` * `unitId` -* `unitTitle` +* `unit` +* `isEnabledOutlineSidebar` +* `renderUnitNavigation` ## Description -This slot is used for adding content after the Unit title. +This slot is used for adding content before or after the Unit title. ## Example -The following `env.config.jsx` will render the `course_id`, `unit_id` and `unitTitle` of the course as `

` elements. +The following `env.config.jsx` will render `unit_id` and `unitTitle` of the course as `

` elements. -![Screenshot of Content added after the Unit Title](./images/post_unit_title.png) +![Screenshot of Content added before and after the Unit Title](./images/screenshot_custom.png) ```js import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework'; @@ -29,11 +30,11 @@ const config = { widget: { id: 'custom_unit_title_content', type: DIRECT_PLUGIN, - RenderWidget: ({courseId, unitId, unitTitle}) => ( + RenderWidget: ({ unitId, unit, isEnabledOutlineSidebar, renderUnitNavigation }) => ( <> -

📚: {courseId}

+ {isEnabledOutlineSidebar && renderUnitNavigation(true)} +

📙: {unit.title}

📙: {unitId}

-

📙: {unitTitle}

), }, diff --git a/src/plugin-slots/UnitTitleSlot/images/post_unit_title.png b/src/plugin-slots/UnitTitleSlot/images/post_unit_title.png deleted file mode 100644 index b0adc94c20..0000000000 Binary files a/src/plugin-slots/UnitTitleSlot/images/post_unit_title.png and /dev/null differ diff --git a/src/plugin-slots/UnitTitleSlot/images/screenshot_custom.png b/src/plugin-slots/UnitTitleSlot/images/screenshot_custom.png new file mode 100644 index 0000000000..fe6794b205 Binary files /dev/null and b/src/plugin-slots/UnitTitleSlot/images/screenshot_custom.png differ diff --git a/src/plugin-slots/UnitTitleSlot/index.jsx b/src/plugin-slots/UnitTitleSlot/index.jsx index 23d501a079..8f035eaf72 100644 --- a/src/plugin-slots/UnitTitleSlot/index.jsx +++ b/src/plugin-slots/UnitTitleSlot/index.jsx @@ -1,21 +1,55 @@ import PropTypes from 'prop-types'; import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { useIntl } from '@edx/frontend-platform/i18n'; -const UnitTitleSlot = ({ courseId, unitId, unitTitle }) => ( - -); +import { BookmarkButton } from '@src/courseware/course/bookmark'; +import messages from '@src/courseware/course/sequence/messages'; + +const UnitTitleSlot = ({ + unitId, + unit, + isEnabledOutlineSidebar, + renderUnitNavigation, +}) => { + const { formatMessage } = useIntl(); + const isProcessing = unit.bookmarkedUpdateState === 'loading'; + + return ( + +
+
+

{unit.title}

+
+ {isEnabledOutlineSidebar && renderUnitNavigation(true)} +
+

{formatMessage(messages.headerPlaceholder)}

+ +
+ ); +}; UnitTitleSlot.propTypes = { - courseId: PropTypes.string.isRequired, unitId: PropTypes.string.isRequired, - unitTitle: PropTypes.string.isRequired, + unit: PropTypes.shape({ + id: PropTypes.string.isRequired, + bookmarked: PropTypes.bool.isRequired, + title: PropTypes.string.isRequired, + bookmarkedUpdateState: PropTypes.string.isRequired, + }).isRequired, + isEnabledOutlineSidebar: PropTypes.bool.isRequired, + renderUnitNavigation: PropTypes.func.isRequired, }; export default UnitTitleSlot;