From 154f9f2b7db95ecdd2c5c7b8fd38696321487635 Mon Sep 17 00:00:00 2001 From: Long Lin Date: Tue, 26 Oct 2021 17:03:39 -0400 Subject: [PATCH] feat: implement subscription page using paragon components --- package-lock.json | 9 +- package.json | 2 +- src/components/BulkEnrollmentPage/index.jsx | 17 +- .../BulkEnrollmentPage/index.test.jsx | 12 +- .../MultipleSubscriptionPicker.jsx | 39 ++-- .../MultipleSubscriptionsPage.jsx | 17 +- .../subscriptions/SubscriptionCard.jsx | 170 ++++++++++++------ .../SubscriptionManagementPage.jsx | 31 +++- .../subscriptions/data/constants.js | 11 ++ src/components/subscriptions/data/utils.js | 35 ++++ .../styles/_SubscriptionManagementPage.scss | 26 ++- .../tests/MultipleSubscriptionsPage.test.jsx | 4 +- .../MultipleSubscriptionsPicker.test.jsx | 13 +- .../tests/SubscriptionCard.test.jsx | 98 +++++----- .../tests/SubscriptionManagementPage.test.jsx | 77 ++++++++ .../subscriptions/tests/data/utils.test.js | 46 +++++ src/index.scss | 2 +- 17 files changed, 456 insertions(+), 153 deletions(-) create mode 100644 src/components/subscriptions/tests/SubscriptionManagementPage.test.jsx create mode 100644 src/components/subscriptions/tests/data/utils.test.js diff --git a/package-lock.json b/package-lock.json index 958c6aab11..8e0f7c0d4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17479,13 +17479,14 @@ } }, "react-responsive": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-6.1.2.tgz", - "integrity": "sha512-AXentVC/kN3KED9zhzJv2pu4vZ0i6cSHdTtbCScVV1MT6F5KXaG2qs5D7WLmhdaOvmiMX8UfmS4ZSO+WPwDt4g==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-responsive/-/react-responsive-8.2.0.tgz", + "integrity": "sha512-iagCqVrw4QSjhxKp3I/YK6+ODkWY6G+YPElvdYKiUUbywwh9Ds0M7r26Fj2/7dWFFbOpcGnJE6uE7aMck8j5Qg==", "requires": { "hyphenate-style-name": "^1.0.0", "matchmediaquery": "^0.3.0", - "prop-types": "^15.6.1" + "prop-types": "^15.6.1", + "shallow-equal": "^1.1.0" } }, "react-router": { diff --git a/package.json b/package.json index 8c18bf4fc6..925c777609 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "react-loading-skeleton": "2.2.0", "react-markdown": "^6.0.0", "react-redux": "5.1.2", - "react-responsive": "6.1.2", + "react-responsive": "^8.2.0", "react-router": "5.2.0", "react-router-dom": "5.2.0", "react-textarea-autosize": "7.1.2", diff --git a/src/components/BulkEnrollmentPage/index.jsx b/src/components/BulkEnrollmentPage/index.jsx index d55f6d2acd..42791346d6 100644 --- a/src/components/BulkEnrollmentPage/index.jsx +++ b/src/components/BulkEnrollmentPage/index.jsx @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import { Container } from '@edx/paragon'; import { Switch, Route } from 'react-router-dom'; +import moment from 'moment'; import Hero from '../Hero'; import { MultipleSubscriptionsPage, SubscriptionData } from '../subscriptions'; import CourseSearch from './CourseSearch'; @@ -25,7 +26,21 @@ function BulkEnrollmentPage({ enterpriseId }) { redirectPage={ROUTE_NAMES.bulkEnrollment} useCatalog leadText="Choose a subscription to enroll your learners in courses" - buttonText="Enroll learners" + createActions={(subscription) => { + const { params: { enterpriseSlug } } = routeProps.match; + const isExpired = moment().isAfter(subscription.expirationDate); + const actions = []; + + if (!isExpired) { + actions.push({ + variant: 'primary', + to: `/${enterpriseSlug}/admin/${ROUTE_NAMES.bulkEnrollment}/${subscription.uuid}`, + buttonText: 'Enroll learners', + }); + } + + return actions; + }} /> )} diff --git a/src/components/BulkEnrollmentPage/index.test.jsx b/src/components/BulkEnrollmentPage/index.test.jsx index e96e9ba91c..a53a17432d 100644 --- a/src/components/BulkEnrollmentPage/index.test.jsx +++ b/src/components/BulkEnrollmentPage/index.test.jsx @@ -169,10 +169,20 @@ describe('', () => { }); it('renders the subscription picker', async () => { renderWithRouter(, { route: `/${testSlug}/admin/${ROUTE_NAMES.bulkEnrollment}` }); - expect(await screen.findByText('Cohorts')).toBeInTheDocument(); + expect(await screen.findByText('Plans')).toBeInTheDocument(); expect(await screen.findByText(sub1.title)).toBeInTheDocument(); expect(await screen.findByText(sub2.title)).toBeInTheDocument(); }); + it('renders enroll learner button if the plan is not expired', async () => { + renderWithRouter(, { route: `/${testSlug}/admin/${ROUTE_NAMES.bulkEnrollment}` }); + expect((await screen.findAllByText('Enroll learners')).length).toEqual(1); + }); + it('does not render enroll learner button if the plan is expired', async () => { + LicenseManagerApiService.fetchSubscriptions.mockImplementation(() => singleSubscription); + + renderWithRouter(, { route: `/${testSlug}/admin/${ROUTE_NAMES.bulkEnrollment}` }); + expect((await screen.queryAllByText('Enroll learners')).length).toEqual(0); + }); it('renders the course search page', async () => { renderWithRouter(, { route: `/${testSlug}/admin/${ROUTE_NAMES.bulkEnrollment}/${sub1.uuid}` }); await act(() => subscriptions); diff --git a/src/components/subscriptions/MultipleSubscriptionPicker.jsx b/src/components/subscriptions/MultipleSubscriptionPicker.jsx index 7a419ebf32..865ce94e44 100644 --- a/src/components/subscriptions/MultipleSubscriptionPicker.jsx +++ b/src/components/subscriptions/MultipleSubscriptionPicker.jsx @@ -1,57 +1,46 @@ import React from 'react'; import PropTypes from 'prop-types'; import { - CardGrid, Row, Col, } from '@edx/paragon'; import SubscriptionCard from './SubscriptionCard'; import { DEFAULT_LEAD_TEXT } from './data/constants'; -import { ROUTE_NAMES } from '../EnterpriseApp/constants'; const MultipleSubscriptionsPicker = ({ - enterpriseSlug, leadText, buttonText, redirectPage, subscriptions, + leadText, subscriptions, createActions, }) => ( <> - -

Cohorts

+ +

Plans

{leadText}

+ + {subscriptions.map(subscription => ( + + ))} +
- - {subscriptions.map(subscription => ( - - ))} - ); MultipleSubscriptionsPicker.defaultProps = { - redirectPage: ROUTE_NAMES.subscriptionManagement, leadText: DEFAULT_LEAD_TEXT, - buttonText: null, + createActions: null, }; MultipleSubscriptionsPicker.propTypes = { - buttonText: PropTypes.string, - enterpriseSlug: PropTypes.string.isRequired, leadText: PropTypes.string, - redirectPage: PropTypes.string, subscriptions: PropTypes.arrayOf(PropTypes.shape()).isRequired, + createActions: PropTypes.func, }; export default MultipleSubscriptionsPicker; diff --git a/src/components/subscriptions/MultipleSubscriptionsPage.jsx b/src/components/subscriptions/MultipleSubscriptionsPage.jsx index 2c94185726..7113da843a 100644 --- a/src/components/subscriptions/MultipleSubscriptionsPage.jsx +++ b/src/components/subscriptions/MultipleSubscriptionsPage.jsx @@ -6,11 +6,14 @@ import { SubscriptionContext } from './SubscriptionData'; import SubscriptionExpiration from './expiration/SubscriptionExpiration'; import MultipleSubscriptionsPicker from './MultipleSubscriptionPicker'; -import { DEFAULT_LEAD_TEXT } from './data/constants'; +import { + DEFAULT_LEAD_TEXT, +} from './data/constants'; +import { sortSubscriptionsByStatus } from './data/utils'; import { ROUTE_NAMES } from '../EnterpriseApp/constants'; const MultipleSubscriptionsPage = ({ - match, redirectPage, leadText, buttonText, + match, redirectPage, leadText, createActions, }) => { const { params: { enterpriseSlug } } = match; const { data } = useContext(SubscriptionContext); @@ -26,15 +29,17 @@ const MultipleSubscriptionsPage = ({ ); } + const sortedSubscriptions = sortSubscriptionsByStatus(subscriptions); + return ( <> ); @@ -43,7 +48,7 @@ const MultipleSubscriptionsPage = ({ MultipleSubscriptionsPage.defaultProps = { redirectPage: ROUTE_NAMES.subscriptionManagement, leadText: DEFAULT_LEAD_TEXT, - buttonText: null, + createActions: null, }; MultipleSubscriptionsPage.propTypes = { @@ -54,7 +59,7 @@ MultipleSubscriptionsPage.propTypes = { }).isRequired, }).isRequired, leadText: PropTypes.string, - buttonText: PropTypes.string, + createActions: PropTypes.func, }; export default MultipleSubscriptionsPage; diff --git a/src/components/subscriptions/SubscriptionCard.jsx b/src/components/subscriptions/SubscriptionCard.jsx index a59bef568e..23d3ddde18 100644 --- a/src/components/subscriptions/SubscriptionCard.jsx +++ b/src/components/subscriptions/SubscriptionCard.jsx @@ -2,78 +2,136 @@ import React from 'react'; import PropTypes from 'prop-types'; import moment from 'moment'; import { Link } from 'react-router-dom'; -import { Card, Badge, Button } from '@edx/paragon'; -import { ROUTE_NAMES } from '../EnterpriseApp/constants'; +import { + Card, Badge, Button, + Row, + Col, +} from '@edx/paragon'; + +import classNames from 'classnames'; +import { getSubscriptionStatus } from './data/utils'; +import { ACTIVE, SCHEDULED, SUBSCRIPTION_STATUS_BADGE_MAP } from './data/constants'; const SubscriptionCard = ({ - uuid, - title, - enterpriseSlug, - startDate, - expirationDate, - redirectPage, - buttonText, - licenses: { - allocated, - total, - }, + subscription, + createActions, }) => { + const { + title, + startDate, + expirationDate, + licenses = {}, + } = subscription; + const formattedStartDate = moment(startDate).format('MMMM D, YYYY'); const formattedExpirationDate = moment(expirationDate).format('MMMM D, YYYY'); - const isExpired = moment().isAfter(expirationDate); - const buttonDisplayText = buttonText || `${isExpired ? 'View' : 'Manage'} learners`; + const subscriptionStatus = getSubscriptionStatus(subscription); - return ( - - - - {title} - - {isExpired && ( -
- - Expired - -
- )} -

+ const renderDaysUntilPlanStartText = (className) => { + if (!(subscriptionStatus === SCHEDULED)) { + return null; + } + + const now = moment(); + const planStart = moment(startDate); + const daysUntilPlanStart = planStart.diff(now, 'days'); + const hoursUntilPlanStart = planStart.diff(now, 'hours'); + + return ( + + Plan begins in { + daysUntilPlanStart > 0 ? `${daysUntilPlanStart} day${daysUntilPlanStart > 1 ? 's' : ''}` + : `${hoursUntilPlanStart} hour${hoursUntilPlanStart > 1 ? 's' : ''}` + } + + ); + }; + + const renderActions = () => { + const actions = createActions?.(subscription) || []; + + return actions.length > 0 && actions.map(action => ( + + )); + }; + + const renderCardHeader = () => { + const subtitle = ( +

+ + {subscriptionStatus} + + {formattedStartDate} - {formattedExpirationDate} -

-

- License assignments -

-

- {allocated} of {total} -

-
-
- -
-
- +
+
+ ); + + return ( + + {renderActions() || renderDaysUntilPlanStartText('mt-4')} + + )} + /> + ); + }; + + const renderCardBody = () => (subscriptionStatus === ACTIVE && ( + + Licenses + + {['Assigned', 'Activated', 'Allocated', 'Unassigned'].map(licenseStatus => ( + + {licenseStatus} + {licenses[licenseStatus.toLowerCase()]} of {licenses.total} + + ))} + + + )); + + return ( + + {renderCardHeader()} + {renderCardBody()} ); }; SubscriptionCard.defaultProps = { - redirectPage: ROUTE_NAMES.subscriptionManagement, - buttonText: null, + createActions: null, }; SubscriptionCard.propTypes = { - uuid: PropTypes.string.isRequired, - title: PropTypes.string.isRequired, - enterpriseSlug: PropTypes.string.isRequired, - startDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired, - expirationDate: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired, - licenses: PropTypes.shape({ - allocated: PropTypes.number.isRequired, - total: PropTypes.number.isRequired, + subscription: PropTypes.shape({ + startDate: PropTypes.string.isRequired, + expirationDate: PropTypes.string.isRequired, + title: PropTypes.string.isRequired, + licenses: PropTypes.shape({ + assigned: PropTypes.number.isRequired, + activated: PropTypes.number.isRequired, + allocated: PropTypes.number.isRequired, + unassigned: PropTypes.number.isRequired, + total: PropTypes.number.isRequired, + }), }).isRequired, - redirectPage: PropTypes.string, - buttonText: PropTypes.string, + createActions: PropTypes.func, }; export default SubscriptionCard; diff --git a/src/components/subscriptions/SubscriptionManagementPage.jsx b/src/components/subscriptions/SubscriptionManagementPage.jsx index 5c0ac03ee3..888bc085b3 100644 --- a/src/components/subscriptions/SubscriptionManagementPage.jsx +++ b/src/components/subscriptions/SubscriptionManagementPage.jsx @@ -3,8 +3,11 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import { Helmet } from 'react-helmet'; import { Route, Switch } from 'react-router-dom'; -import { Container } from '@edx/paragon'; +import { + Container, +} from '@edx/paragon'; +import moment from 'moment'; import Hero from '../Hero'; import SubscriptionData from './SubscriptionData'; import MultipleSubscriptionsPage from './MultipleSubscriptionsPage'; @@ -23,7 +26,31 @@ function SubscriptionManagementPage({ enterpriseId }) { ( + { + const { params: { enterpriseSlug } } = routeProps.match; + const now = moment(); + const isScheduled = now.isBefore(subscription.startDate); + const isExpired = now.isAfter(subscription.expirationDate); + const buttonText = `${isExpired ? 'View' : 'Manage'} learners`; + const buttonVariant = isExpired ? 'outline-primary' : 'primary'; + + const actions = []; + + if (!isScheduled) { + actions.push({ + variant: buttonVariant, + to: `/${enterpriseSlug}/admin/${ROUTE_NAMES.subscriptionManagement}/${subscription.uuid}`, + buttonText, + }); + } + + return actions; + }} + /> + )} exact /> licenseStatus === ASSIGNED || licenseStatus === ACTIVATED; export const canRemindLicense = (licenseStatus) => licenseStatus === ASSIGNED; + +export const getSubscriptionStatus = (subscription) => { + const now = moment(); + + if (now.isBefore(subscription.startDate)) { + return SCHEDULED; + } if (now.isAfter(subscription.expirationDate)) { + return ENDED; + } + + return ACTIVE; +}; + +// Sort plans by statuses, active -> scheduled -> ended. +export const sortSubscriptionsByStatus = (subscriptions) => subscriptions.slice().sort( + (sub1, sub2) => { + const orderByStatus = { + [ACTIVE]: 0, + [SCHEDULED]: 1, + [ENDED]: 2, + }; + const sub1Status = getSubscriptionStatus(sub1); + const sub2Status = getSubscriptionStatus(sub2); + + if (sub1Status === sub2Status) { + return moment(sub1.startDate) - moment(sub2.startDate); + } + + return orderByStatus[sub1Status] - orderByStatus[sub2Status]; + }, +); diff --git a/src/components/subscriptions/styles/_SubscriptionManagementPage.scss b/src/components/subscriptions/styles/_SubscriptionManagementPage.scss index 5fdb189535..4cb2935771 100644 --- a/src/components/subscriptions/styles/_SubscriptionManagementPage.scss +++ b/src/components/subscriptions/styles/_SubscriptionManagementPage.scss @@ -9,13 +9,37 @@ } } - .subscription-card, .support-card { .card-title { @extend .h4; } } + .subscription-card { + margin-bottom: ($spacer * 1.875); + + @include media-breakpoint-down(sm) { + .pgn__card-header { + flex-direction: column; + } + + .pgn__card-header-actions { + text-align: right; + } + + .card-body { + & > span { + margin-left: 1rem; + } + + .row { + margin: 1rem 0 !important; + } + } + } + + } + @include media-breakpoint-up(lg) { .add-users-dropdown { float: right; diff --git a/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx b/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx index 8b9d1d6f72..9dd5b31bb9 100644 --- a/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx +++ b/src/components/subscriptions/tests/MultipleSubscriptionsPage.test.jsx @@ -78,7 +78,7 @@ describe('MultipleSubscriptionsPage', () => { route: `/${fakeSlug}/admin/${ROUTE_NAMES.subscriptionManagement}`, path: `/:enterpriseSlug/admin/${ROUTE_NAMES.subscriptionManagement}`, }); - expect(screen.getByText('Cohorts')).toBeInTheDocument(); + expect(screen.getByText('Plans')).toBeInTheDocument(); }); it('returns null if there are no subscriptions', () => { const subscriptions = { data: { results: [] } }; @@ -86,7 +86,7 @@ describe('MultipleSubscriptionsPage', () => { route: `/${fakeSlug}/admin/${ROUTE_NAMES.subscriptionManagement}`, path: `/:enterpriseSlug/admin/${ROUTE_NAMES.subscriptionManagement}`, }); - expect(screen.queryByText('Cohorts')).not.toBeInTheDocument(); + expect(screen.queryByText('Plans')).not.toBeInTheDocument(); }); it('redirects if there is only one subscription, default redirectPage', () => { const subsUuid = 'bestuuid'; diff --git a/src/components/subscriptions/tests/MultipleSubscriptionsPicker.test.jsx b/src/components/subscriptions/tests/MultipleSubscriptionsPicker.test.jsx index ae5f1ce1c2..70bc730d67 100644 --- a/src/components/subscriptions/tests/MultipleSubscriptionsPicker.test.jsx +++ b/src/components/subscriptions/tests/MultipleSubscriptionsPicker.test.jsx @@ -1,12 +1,10 @@ import { screen } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import React from 'react'; -import userEvent from '@testing-library/user-event'; import { renderWithRouter } from '../../test/testUtils'; import { DEFAULT_LEAD_TEXT } from '../data/constants'; import MultipleSubscriptionsPicker from '../MultipleSubscriptionPicker'; -import { ROUTE_NAMES } from '../../EnterpriseApp/constants'; const firstCatalogUuid = 'catalogID1'; const firstEnterpriseUuid = 'ided'; @@ -41,7 +39,7 @@ const defaultProps = { describe('MultipleSubscriptionsPicker', () => { it('displays a title', () => { renderWithRouter(); - expect(screen.getByText('Cohorts')).toBeInTheDocument(); + expect(screen.getByText('Plans')).toBeInTheDocument(); }); it('displays default lead text by default', () => { renderWithRouter(); @@ -58,13 +56,4 @@ describe('MultipleSubscriptionsPicker', () => { expect(screen.getByText(subscription.title)).toBeInTheDocument(); }); }); - it('sets the correct url on the button link', () => { - const buttonText = 'Click me!'; - const { history } = renderWithRouter( - , - ); - const button = screen.queryAllByText(buttonText)[0]; - userEvent.click(button); - expect(history.location.pathname).toEqual(`/${defaultProps.enterpriseSlug}/admin/${ROUTE_NAMES.subscriptionManagement}/${firstEnterpriseUuid}`); - }); }); diff --git a/src/components/subscriptions/tests/SubscriptionCard.test.jsx b/src/components/subscriptions/tests/SubscriptionCard.test.jsx index a8d4d20bfd..3f93e9f66b 100644 --- a/src/components/subscriptions/tests/SubscriptionCard.test.jsx +++ b/src/components/subscriptions/tests/SubscriptionCard.test.jsx @@ -2,65 +2,81 @@ import { screen, } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; -import userEvent from '@testing-library/user-event'; import React from 'react'; +import moment from 'moment'; +import { Context as ResponsiveContext } from 'react-responsive'; +import { + breakpoints, +} from '@edx/paragon'; import { renderWithRouter } from '../../test/testUtils'; - import SubscriptionCard from '../SubscriptionCard'; -import { ROUTE_NAMES } from '../../EnterpriseApp/constants'; -const defaultProps = { +const defaultSubscription = { uuid: 'ided', title: 'Select something', - enterpriseSlug: 'sluggy', startDate: '2021-04-13', expirationDate: '2024-04-13', +}; +const defaultProps = { + subscription: defaultSubscription, licenses: { + assigned: 5, + unassigned: 2, + activated: 3, allocated: 10, total: 20, }, }; +jest.mock('moment', () => (date) => { + if (date) { + return jest.requireActual('moment')(date); + } + return jest.requireActual('moment')('2020-01-01T00:00:00.000Z'); +}); + describe('SubscriptionCard', () => { - it('displays a title', () => { + it('displays subscription information', () => { renderWithRouter(); - expect(screen.getByText(defaultProps.title)).toBeInTheDocument(); + const { title } = defaultSubscription; + expect(screen.getByText(title)); }); - describe('button link', () => { - it('sets the correct default link', () => { - const buttonText = 'click me!'; - const { history } = renderWithRouter( - , - ); - const button = screen.getByText(buttonText); - userEvent.click(button); - expect(history.location.pathname).toEqual(`/${defaultProps.enterpriseSlug}/admin/${ROUTE_NAMES.subscriptionManagement}/${defaultProps.uuid}`); - }); - it('sets the correct link from props, custom redirect', () => { - const buttonText = 'click me!'; - const redirectPage = 'customredirect'; - const { history } = renderWithRouter( - , - ); - const button = screen.getByText(buttonText); - userEvent.click(button); - expect(history.location.pathname).toEqual(`/${defaultProps.enterpriseSlug}/admin/${redirectPage}/${defaultProps.uuid}`); - }); + + it.each([ + [moment().add(1, 'days').toISOString(), '1 day'], + [moment().add(3, 'days').toISOString(), '3 days'], + [moment().add(1, 'hours').toISOString(), '1 hour'], + [moment().add(3, 'hours').toISOString(), '3 hours'], + ])('displays days until plan starts text if there are no actions and the plan is scheduled', (startDate, expectedText) => { + renderWithRouter( + + + , + ); + + expect(screen.getByText(`Plan begins in ${expectedText}`)); }); - describe('button text', () => { - it('displays text received as a prop', () => { - const buttonText = 'click me!'; - renderWithRouter(); - expect(screen.getByText(buttonText)).toBeInTheDocument(); - }); - it('displays the correct text if license is not expired', () => { - renderWithRouter(); - expect(screen.getByText('Manage learners')).toBeInTheDocument(); - }); - it('displays the correct text for an expired license', () => { - renderWithRouter(); - expect(screen.getByText('View learners')).toBeInTheDocument(); - }); + + it('displays actions', () => { + const mockCreateActions = jest.fn(() => ([{ + variant: 'primary', + to: '/', + buttonText: 'action 1', + }])); + renderWithRouter( + , + ); + expect(mockCreateActions).toHaveBeenCalledWith(defaultSubscription); + expect(screen.getByText('action 1')); }); }); diff --git a/src/components/subscriptions/tests/SubscriptionManagementPage.test.jsx b/src/components/subscriptions/tests/SubscriptionManagementPage.test.jsx new file mode 100644 index 0000000000..46322318c8 --- /dev/null +++ b/src/components/subscriptions/tests/SubscriptionManagementPage.test.jsx @@ -0,0 +1,77 @@ +import { + screen, +} from '@testing-library/react'; +import '@testing-library/jest-dom/extend-expect'; + +import React from 'react'; +import { Provider } from 'react-redux'; + +import moment from 'moment'; +import { + TEST_ENTERPRISE_CUSTOMER_SLUG, createMockStore, +} from './TestUtilities'; +import SubscriptionManagementPage from '../SubscriptionManagementPage'; +import { ROUTE_NAMES } from '../../EnterpriseApp/constants'; +import { renderWithRouter } from '../../test/testUtils'; +import * as hooks from '../data/hooks'; + +describe('SubscriptionManagementPage', () => { + describe('multiple subscriptions', () => { + const mockStore = createMockStore(); + const defaultSubscriptions = [ + { + uuid: 'active', + title: 'Enterprise A', + startDate: moment().toISOString(), + expirationDate: moment().add(3, 'days').toISOString(), + licenses: { + allocated: 10, + total: 20, + }, + showExpirationNotifications: true, + }, + { + uuid: 'expired', + title: 'Enterprise B', + startDate: moment().toISOString(), + expirationDate: moment().subtract(3, 'days').toISOString(), + licenses: { + allocated: 11, + total: 30, + }, + showExpirationNotifications: true, + }, + ]; + + // eslint-disable-next-line react/prop-types + const SubscriptionManagementPageWrapper = ({ subscriptions = defaultSubscriptions }) => { + jest.spyOn(hooks, 'useSubscriptionData').mockImplementation(() => ({ + subscriptions: { + count: 1, + next: null, + previous: null, + results: subscriptions, + }, + errors: {}, + setErrors: () => {}, + forceRefresh: () => {}, + })); + + return ( + + + + ); + }; + + it('renders the correct button text on subscription cards', () => { + renderWithRouter(, + { + route: `/${TEST_ENTERPRISE_CUSTOMER_SLUG}/admin/${ROUTE_NAMES.subscriptionManagement}`, + path: `/:enterpriseSlug/admin/${ROUTE_NAMES.subscriptionManagement}`, + }); + expect(screen.getByText('Manage learners')); + expect(screen.getByText('View learners')); + }); + }); +}); diff --git a/src/components/subscriptions/tests/data/utils.test.js b/src/components/subscriptions/tests/data/utils.test.js new file mode 100644 index 0000000000..f7a1db5a3b --- /dev/null +++ b/src/components/subscriptions/tests/data/utils.test.js @@ -0,0 +1,46 @@ +import moment from 'moment'; +import { ACTIVE, SCHEDULED, ENDED } from '../../data/constants'; +import { sortSubscriptionsByStatus, getSubscriptionStatus } from '../../data/utils'; + +describe('utils', () => { + const scheduledSub = { + startDate: moment().add(1, 'days'), + expirationDate: moment().add(10, 'days'), + }; + const activeSub = { + startDate: moment().subtract(1, 'days'), + expirationDate: moment().add(10, 'days'), + }; + const expiredSub = { + startDate: moment().subtract(10, 'days'), + expirationDate: moment().subtract(1, 'days'), + }; + + describe('getSubscriptionStatus', () => { + it('should get the subscription plan status', () => { + expect(getSubscriptionStatus(scheduledSub)).toEqual(SCHEDULED); + expect(getSubscriptionStatus(activeSub)).toEqual(ACTIVE); + expect(getSubscriptionStatus(expiredSub)).toEqual(ENDED); + }); + }); + + describe('sortSubscriptionsByStatus', () => { + it('should sort subscriptions by status', () => { + const initialOrder = [expiredSub, activeSub, scheduledSub]; + const expectedOrder = [activeSub, scheduledSub, expiredSub]; + + expect(sortSubscriptionsByStatus(initialOrder)).toEqual(expectedOrder); + }); + + it('should sort subscriptions by start date after status', () => { + const activeSub2 = { + startDate: moment().subtract(2, 'days'), + expirationDate: moment().add(10, 'days'), + }; + const initialOrder = [expiredSub, activeSub, scheduledSub, activeSub2]; + const expectedOrder = [activeSub2, activeSub, scheduledSub, expiredSub]; + + expect(sortSubscriptionsByStatus(initialOrder)).toEqual(expectedOrder); + }); + }); +}); diff --git a/src/index.scss b/src/index.scss index d80fedaaa4..cf9e571842 100644 --- a/src/index.scss +++ b/src/index.scss @@ -4,8 +4,8 @@ $modal-max-width: 650px; @import "~@edx/brand/paragon/fonts"; @import "~@edx/brand/paragon/variables"; @import "~@edx/paragon/scss/core/core"; -@import "~@edx/brand/paragon/overrides"; @import "~@edx/frontend-enterprise-catalog-search"; +@import "~@edx/brand/paragon/overrides"; $fa-font-path: "~font-awesome/fonts"; @import "~font-awesome/scss/font-awesome";