Skip to content

Commit

Permalink
feat: Added PluginSlot wrapping UpgradeNotification components (#1366)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
rijuma authored Apr 19, 2024
1 parent 377da57 commit d3ef306
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 31 deletions.
35 changes: 22 additions & 13 deletions src/course-home/outline-tab/OutlineTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<>
<div data-learner-type={learnerType} className="row w-100 mx-0 my-3 justify-content-between">
Expand Down Expand Up @@ -194,19 +209,13 @@ const OutlineTab = ({ intl }) => {
/>
)}
<CourseTools />
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
<PluginSlot
id="outline_tab"
pluginProps={{ upgradeNotificationProps }}
testId="outline-tab-slot"
>
<UpgradeNotification {...upgradeNotificationProps} />
</PluginSlot>
<CourseDates />
<CourseHandouts />
</div>
Expand Down
15 changes: 15 additions & 0 deletions src/course-home/outline-tab/OutlineTab.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<div className="border border-light-400 rounded-sm" data-testid="notification-widget">
<UpgradeNotification
offer={offer}
verifiedMode={verifiedMode}
accessExpiration={accessExpiration}
contentTypeGatingEnabled={contentTypeGatingEnabled}
marketingUrl={marketingUrl}
upsellPageName="in_course"
userTimezone={userTimezone}
shouldDisplayBorder={false}
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
upgradeNotificationCurrentState={upgradeNotificationCurrentState}
setupgradeNotificationCurrentState={setUpgradeNotificationCurrentState}
toggleSidebar={() => toggleSidebar(currentSidebar, WIDGETS.NOTIFICATIONS)}
/>
<PluginSlot
id="notification_widget"
pluginProps={{ upgradeNotificationProps }}
testId="notification-widget-slot"
>
<UpgradeNotification {...upgradeNotificationProps} />
</PluginSlot>
</div>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,14 @@ describe('NotificationsWidget', () => {
<NotificationsWidget />
</SidebarContext.Provider>,
);
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();
});
Expand Down
3 changes: 2 additions & 1 deletion src/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => ({
Expand Down
25 changes: 25 additions & 0 deletions src/tests/MockedPluginSlot.jsx
Original file line number Diff line number Diff line change
@@ -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 <div data-testid={testId}>{children}</div>;
};

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;
43 changes: 43 additions & 0 deletions src/tests/MockedPluginSlot.test.jsx
Original file line number Diff line number Diff line change
@@ -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(<MockedPluginSlot />);

const component = screen.getByText('PluginSlot');
expect(component).toBeInTheDocument();
});

it('renders as the slot children directly if there is content within and no testId', () => {
render(
<div role="article">
<MockedPluginSlot>
<q role="note">How much wood could a woodchuck chuck if a woodchuck could chuck wood?</q>
</MockedPluginSlot>
</div>,
);

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(
<MockedPluginSlot testId="guybrush">
<q role="note">I am selling these fine leather jackets.</q>
</MockedPluginSlot>,
);

const component = screen.getByTestId('guybrush');
expect(component).toBeInTheDocument();

const quote = component.querySelector('[role=note]');
expect(quote).toBeInTheDocument();
});
});

0 comments on commit d3ef306

Please sign in to comment.