From d3ef3065870c929f4be675895608a2a193f43950 Mon Sep 17 00:00:00 2001 From: Marcos Rigoli Date: Fri, 19 Apr 2024 14:49:46 -0300 Subject: [PATCH] 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 74d12a8ebe..b72b4f90b1 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 @@ -92,9 +92,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(); + }); +});