From 07315a877bc58b0c5d56bac43dd42f868bf10e04 Mon Sep 17 00:00:00 2001 From: Alison Langston <46360176+alangsto@users.noreply.github.com> Date: Tue, 9 Apr 2024 13:38:28 -0400 Subject: [PATCH 1/4] feat: add plugin slot for fbe lock paywall (#1347) --- .../course/sequence/Unit/UnitSuspense.jsx | 22 ++++++++++++------- .../sequence/Unit/UnitSuspense.test.jsx | 7 +++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/courseware/course/sequence/Unit/UnitSuspense.jsx b/src/courseware/course/sequence/Unit/UnitSuspense.jsx index 59e34333ce..0e1871ec68 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); }); From f39f729ed587e020b44521c472f026452915a335 Mon Sep 17 00:00:00 2001 From: Marcos Rigoli Date: Fri, 19 Apr 2024 14:49:46 -0300 Subject: [PATCH 2/4] feat: Added PluginSlot wrapping UpgradeNotification components (#1366) * chore: Updated PluginSlot mock to support children and test ids * chore: Updated mocked PluginSlot * chore: Added unit test for MockedPluginSlot * fix: Updated slot name ids --- src/course-home/outline-tab/OutlineTab.jsx | 35 +++++++++------ .../outline-tab/OutlineTab.test.jsx | 15 +++++++ .../notifications/NotificationsWidget.jsx | 41 +++++++++++------- .../NotificationsWidget.test.jsx | 7 ++- src/setupTest.js | 3 +- src/tests/MockedPluginSlot.jsx | 25 +++++++++++ src/tests/MockedPluginSlot.test.jsx | 43 +++++++++++++++++++ 7 files changed, 138 insertions(+), 31 deletions(-) create mode 100644 src/tests/MockedPluginSlot.jsx create mode 100644 src/tests/MockedPluginSlot.test.jsx diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 15ac5dd605..2ea019d972 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'; @@ -123,6 +124,20 @@ const OutlineTab = ({ intl }) => { } }, [location.search]); + const upgradeNotificationProps = { + offer, + verifiedMode, + accessExpiration, + contentTypeGatingEnabled: datesBannerInfo.contentTypeGatingEnabled, + marketingUrl, + upsellPageName: 'course_home', + userTimezone, + timeOffsetMillis, + courseId, + org, + shouldDisplayBorder: true, + }; + return ( <>
@@ -194,19 +209,13 @@ 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..5395da4506 100644 --- a/src/course-home/outline-tab/OutlineTab.test.jsx +++ b/src/course-home/outline-tab/OutlineTab.test.jsx @@ -132,6 +132,21 @@ describe('Outline Tab', () => { expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true'); }); + it('renders the Notification wrapper', async () => { + const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true }); + setTabData({ + course_blocks: { blocks: courseBlocks.blocks }, + }); + await fetchAndRender(); + + const pluginSlot = screen.getByTestId('outline-tab-slot'); + expect(pluginSlot).toBeInTheDocument(); + + // The Upgrade Notification should be inside the PluginSlot. + const UpgradeNotification = pluginSlot.querySelector('.upgrade-notification'); + expect(UpgradeNotification).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..3c61332db6 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'; @@ -66,24 +67,32 @@ const NotificationsWidget = () => { if (hideNotificationbar || !isNotificationbarAvailable) { return null; } + const upgradeNotificationProps = { + offer, + verifiedMode, + accessExpiration, + contentTypeGatingEnabled, + marketingUrl, + upsellPageName: 'in_course', + userTimezone, + timeOffsetMillis, + courseId, + org, + upgradeNotificationCurrentState, + setupgradeNotificationCurrentState: setUpgradeNotificationCurrentState, // TODO: Check typo in component? + shouldDisplayBorder: false, + toggleSidebar: () => toggleSidebar(currentSidebar, WIDGETS.NOTIFICATIONS), + }; + 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..d56b15f17e 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 @@ -90,9 +90,14 @@ describe('NotificationsWidget', () => { , ); - const UpgradeNotification = document.querySelector('.upgrade-notification'); + const pluginSlot = screen.getByTestId('notification-widget-slot'); + expect(pluginSlot).toBeInTheDocument(); + + // The Upgrade Notification should be inside the PluginSlot. + const UpgradeNotification = pluginSlot.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/setupTest.js b/src/setupTest.js index b3039f6e10..3f999b8744 100755 --- a/src/setupTest.js +++ b/src/setupTest.js @@ -28,11 +28,12 @@ import { fetchCourse, fetchSequence } from './courseware/data'; 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..e1409ca88b --- /dev/null +++ b/src/tests/MockedPluginSlot.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const MockedPluginSlot = ({ children, testId }) => { + if (!testId) { return children ?? 'PluginSlot'; } // Return its content if PluginSlot slot is wrapping any. + + return
{children}
; +}; + +MockedPluginSlot.displayName = 'PluginSlot'; + +MockedPluginSlot.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]), + testId: PropTypes.string, +}; + +MockedPluginSlot.defaultProps = { + children: undefined, + testId: undefined, +}; + +export default MockedPluginSlot; diff --git a/src/tests/MockedPluginSlot.test.jsx b/src/tests/MockedPluginSlot.test.jsx new file mode 100644 index 0000000000..cc492a0d11 --- /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 as plain "PluginSlot" text node if no clildren nor testId is', () => { + render(); + + const component = screen.getByText('PluginSlot'); + expect(component).toBeInTheDocument(); + }); + + it('renders as the slot children directly if there is content within and no testId', () => { + 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 a div when a testId is provided ', () => { + 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(); + }); +}); From 1006af5820f675925adc8a12746985667fbe5d53 Mon Sep 17 00:00:00 2001 From: Marcos Rigoli Date: Mon, 22 Apr 2024 16:09:48 -0300 Subject: [PATCH 3/4] feat: Added Plugin Slot wrapping UpgradeNotification in NotificationTray (#1367) --- .../notifications/NotificationTray.jsx | 39 ++++++++++++------- .../notifications/NotificationTray.test.jsx | 10 +++-- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx index 76f1e38f94..6adb7b3e32 100644 --- a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx +++ b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx @@ -2,6 +2,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import classNames from 'classnames'; 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'; @@ -65,6 +66,22 @@ const NotificationTray = ({ intl }) => { sendTrackEvent('edx.ui.course.upgrade.old_sidebar.notifications', notificationTrayEventProperties); }, []); + const upgradeNotificationProps = { + offer, + verifiedMode, + accessExpiration, + contentTypeGatingEnabled, + marketingUrl, + upsellPageName: 'in_course', + userTimezone, + shouldDisplayBorder: false, + timeOffsetMillis, + courseId, + org, + upgradeNotificationCurrentState, + setupgradeNotificationCurrentState: setUpgradeNotificationCurrentState, // TODO: Check typo in component? + }; + return ( { >
{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..3e353fbfb2 100644 --- a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx +++ b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.test.jsx @@ -91,10 +91,14 @@ describe('NotificationTray', () => { , ); - const UpgradeNotification = document.querySelector('.upgrade-notification'); - expect(UpgradeNotification) - .toBeInTheDocument(); + const pluginSlot = screen.getByTestId('notification-tray-slot'); + expect(pluginSlot).toBeInTheDocument(); + + // The Upgrade Notification should be inside the PluginSlot. + const UpgradeNotification = pluginSlot.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.')) From ebb12de09c1ae2c7452c87f173b13e9094538396 Mon Sep 17 00:00:00 2001 From: Marcos Rigoli Date: Tue, 23 Apr 2024 13:36:59 -0300 Subject: [PATCH 4/4] fix: Removed PluginSlot prop scoping for UpgradeNotification (#1369) --- src/course-home/outline-tab/OutlineTab.jsx | 2 +- .../notifications/NotificationsWidget.jsx | 2 +- .../course/sidebar/sidebars/notifications/NotificationTray.jsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/course-home/outline-tab/OutlineTab.jsx b/src/course-home/outline-tab/OutlineTab.jsx index 2ea019d972..9b79479517 100644 --- a/src/course-home/outline-tab/OutlineTab.jsx +++ b/src/course-home/outline-tab/OutlineTab.jsx @@ -211,7 +211,7 @@ const OutlineTab = ({ intl }) => { 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 3c61332db6..d7e6b5e064 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 @@ -88,7 +88,7 @@ const NotificationsWidget = () => {
diff --git a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx index 6adb7b3e32..5c133bc60b 100644 --- a/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx +++ b/src/courseware/course/sidebar/sidebars/notifications/NotificationTray.jsx @@ -94,7 +94,7 @@ const NotificationTray = ({ intl }) => { ? (