diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 15ac5dd605..de900251be 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -5,6 +5,7 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { Button } from '@openedx/paragon'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; import { AlertList } from '../../generic/user-messages'; import CourseDates from './widgets/CourseDates'; @@ -194,19 +195,24 @@ const OutlineTab = ({ intl }) => { /> )} - + + + diff --git a/src/course-home/outline-tab/OutlineTab.test.jsx b/src/course-home/outline-tab/OutlineTab.test.jsx index 47ceed621d..7bb8544d96 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -132,6 +132,16 @@ describe('Outline Tab', () => { expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true'); }); + it('includes outline_tab_notifications_plugin slot', async () => { + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true }); + setTabData({ + course_blocks: { blocks: courseBlocks.blocks }, + }); + await fetchAndRender(); + + expect(screen.getByTestId('outline_tab_notifications_plugin')).toBeInTheDocument(); + }); + it('handles expand/collapse all button click', async () => { await fetchAndRender(); // Button renders as "Expand All" diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx index 3f8ab62bbc..7e7087ac5b 100644 --- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx +++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.jsx @@ -1,6 +1,7 @@ import React, { useContext, useEffect, useMemo } from 'react'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; import { useModel } from '../../../../../../generic/model-store'; import UpgradeNotification from '../../../../../../generic/upgrade-notification/UpgradeNotification'; import { WIDGETS } from '../../../../../../constants'; @@ -58,6 +59,10 @@ const NotificationsWidget = () => { verification_status: verificationStatus, }; + const onToggleSidebar = () => { + toggleSidebar(currentSidebar, WIDGETS.NOTIFICATIONS); + }; + // After three seconds, update notificationSeen (to hide red dot) useEffect(() => { setTimeout(onNotificationSeen, 3000); @@ -68,22 +73,32 @@ const NotificationsWidget = () => { return (
- toggleSidebar(currentSidebar, WIDGETS.NOTIFICATIONS)} - /> + + +
); }; diff --git a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx index fb5fb08416..11f9daf740 100644 --- a/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx +++ b/src/courseware/course/new-sidebar/sidebars/discussions-notifications/notifications/NotificationsWidget.test.jsx @@ -78,6 +78,21 @@ describe('NotificationsWidget', () => { }); }); + it('includes notification_widget_slot', async () => { + await fetchAndRender( + + + , + ); + expect(screen.getByTestId('notification_widget_slot')).toBeInTheDocument(); + }); + it('renders upgrade card', async () => { await fetchAndRender( { , ); - const UpgradeNotification = document.querySelector('.upgrade-notification'); + // The Upgrade Notification should be inside the PluginSlot. + const UpgradeNotification = document.querySelector('.upgrade-notification'); expect(UpgradeNotification).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Upgrade for $149' })).toBeInTheDocument(); expect(screen.queryByText('You have no new notifications at this time.')).not.toBeInTheDocument(); }); diff --git a/src/courseware/course/sequence/Unit/UnitSuspense.jsx b/src/courseware/course/sequence/Unit/UnitSuspense.jsx index 59e34333ce..09843a7164 100644 --- a/src/courseware/course/sequence/Unit/UnitSuspense.jsx +++ b/src/courseware/course/sequence/Unit/UnitSuspense.jsx @@ -2,6 +2,7 @@ import React, { Suspense } from 'react'; import PropTypes from 'prop-types'; import { useIntl } from '@edx/frontend-platform/i18n'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; import { useModel } from '@src/generic/model-store'; import PageLoading from '@src/generic/PageLoading'; @@ -24,19 +25,24 @@ const UnitSuspense = ({ meta.contentTypeGatingEnabled && unit.containsContentTypeGatedContent ); - const suspenseComponent = (message, Component) => ( - }> - - - ); - return ( <> {shouldDisplayContentGating && ( - suspenseComponent(messages.loadingLockedContent, LockPaywall) + }> + + + + )} {shouldDisplayHonorCode && ( - suspenseComponent(messages.loadingHonorCode, HonorCode) + }> + + )} ); diff --git a/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx b/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx index 34bc21c073..f47ee6b241 100644 --- a/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx +++ b/src/courseware/course/sequence/Unit/UnitSuspense.test.jsx @@ -64,7 +64,7 @@ describe('UnitSuspense component', () => { describe('output', () => { describe('LockPaywall', () => { const testNoPaywall = () => { - it('does not display LockPaywal', () => { + it('does not display LockPaywall', () => { el = shallow(); expect(el.instance.findByType(LockPaywall).length).toEqual(0); }); @@ -79,8 +79,9 @@ describe('UnitSuspense component', () => { it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => { el = shallow(); const [component] = el.instance.findByType(LockPaywall); - expect(component.parent.type).toEqual('Suspense'); - expect(component.parent.props.fallback) + expect(component.parent.type).toEqual('PluginSlot'); + expect(component.parent.parent.type).toEqual('Suspense'); + expect(component.parent.parent.props.fallback) .toEqual(); expect(component.props.courseId).toEqual(props.courseId); }); diff --git a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx index df51fa7c93..9fe8d1ad21 100644 --- a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx +++ b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx @@ -2,8 +2,9 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import classNames from 'classnames'; import { useContext, useEffect, useMemo } from 'react'; import { sendTrackEvent } from '@edx/frontend-platform/analytics'; -import { useModel } from '@src/generic/model-store'; -import UpgradeNotification from '@src/generic/upgrade-notification/UpgradeNotification'; +import { PluginSlot } from '@openedx/frontend-plugin-framework'; +import { useModel } from '../../../../../generic/model-store'; +import UpgradeNotification from '../../../../../generic/upgrade-notification/UpgradeNotification'; import messages from '../../../messages'; import SidebarBase from '../../common/SidebarBase'; @@ -77,21 +78,30 @@ const NotificationTray = ({ intl }) => { >
{verifiedMode ? ( - + + + ) : (

{intl.formatMessage(messages.noNotificationsMessage)}

)} diff --git a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx index 666874d75c..2e1815f7be 100644 --- a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx +++ b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx @@ -81,6 +81,19 @@ describe('NotificationTray', () => { .toBeInTheDocument(); }); + it('includes notification_tray_plugin slot', async () => { + await fetchAndRender( + + + , + ); + expect(screen.getByTestId('notification_tray_plugin')).toBeInTheDocument(); + }); + it('renders upgrade card', async () => { await fetchAndRender( { , ); - const UpgradeNotification = document.querySelector('.upgrade-notification'); - expect(UpgradeNotification) - .toBeInTheDocument(); + expect(document.querySelector('.upgrade-notification')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Upgrade for $149' })) .toBeInTheDocument(); expect(screen.queryByText('You have no new notifications at this time.')) diff --git a/src/setupTest.js b/src/setupTest.js index e1bfdc79b6..46752b6d25 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -29,11 +29,12 @@ import { getCourseOutlineStructure } from './courseware/data/thunks'; import { appendBrowserTimezoneToUrl, executeThunk } from './utils'; import buildSimpleCourseAndSequenceMetadata from './courseware/data/__factories__/sequenceMetadata.factory'; import { buildOutlineFromBlocks } from './courseware/data/__factories__/learningSequencesOutline.factory'; +import MockedPluginSlot from './tests/MockedPluginSlot'; jest.mock('@openedx/frontend-plugin-framework', () => ({ ...jest.requireActual('@openedx/frontend-plugin-framework'), Plugin: () => 'Plugin', - PluginSlot: () => 'PluginSlot', + PluginSlot: MockedPluginSlot, })); jest.mock('@src/generic/plugin-store', () => ({ diff --git a/src/tests/MockedPluginSlot.jsx b/src/tests/MockedPluginSlot.jsx new file mode 100644 index 0000000000..e86952ee0b --- /dev/null +++ b/src/tests/MockedPluginSlot.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const MockedPluginSlot = ({ children, id }) => ( +
+ PluginSlot_{id} + { children &&
{children}
} +
+); + +MockedPluginSlot.displayName = 'PluginSlot'; + +MockedPluginSlot.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + id: PropTypes.string, +}; + +MockedPluginSlot.defaultProps = { + children: undefined, + id: undefined, +}; + +export default MockedPluginSlot; diff --git a/src/tests/MockedPluginSlot.test.jsx b/src/tests/MockedPluginSlot.test.jsx new file mode 100644 index 0000000000..b830b68fbf --- /dev/null +++ b/src/tests/MockedPluginSlot.test.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import MockedPluginSlot from './MockedPluginSlot'; + +describe('MockedPluginSlot', () => { + it('renders mock plugin with "PluginSlot" text', () => { + render(); + + const component = screen.getByText('PluginSlot_test_plugin'); + expect(component).toBeInTheDocument(); + }); + + it('renders as the slot children directly if there is content within', () => { + render( +
+ + How much wood could a woodchuck chuck if a woodchuck could chuck wood? + +
, + ); + + const component = screen.getByRole('article'); + expect(component).toBeInTheDocument(); + + // Direct children + const quote = component.querySelector(':scope > q'); + expect(quote.getAttribute('role')).toBe('note'); + }); + + it('renders mock plugin with a data-testid ', () => { + render( + + I am selling these fine leather jackets. + , + ); + + const component = screen.getByTestId('guybrush'); + expect(component).toBeInTheDocument(); + + const quote = component.querySelector('[role=note]'); + expect(quote).toBeInTheDocument(); + }); +});