({ 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.
-
+
```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;