diff --git a/src/pages-and-resources/discussions/app-list/AppCard.jsx b/src/pages-and-resources/discussions/app-list/AppCard.jsx
index c0f554c7f..441fc7587 100644
--- a/src/pages-and-resources/discussions/app-list/AppCard.jsx
+++ b/src/pages-and-resources/discussions/app-list/AppCard.jsx
@@ -2,8 +2,9 @@ import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import Responsive from 'react-responsive';
import {
- Card, CheckboxControl, ExtraSmall,
+ Card, CheckboxControl, breakpoints,
} from '@edx/paragon';
import messages from './messages';
import FeaturesList from './FeaturesList';
@@ -51,12 +52,12 @@ function AppCard({
{supportText}
{intl.formatMessage(messages[`appDescription-${app.id}`])}
-
+
-
+
);
diff --git a/src/pages-and-resources/discussions/app-list/AppList.jsx b/src/pages-and-resources/discussions/app-list/AppList.jsx
index b303a6515..58dcea7a2 100644
--- a/src/pages-and-resources/discussions/app-list/AppList.jsx
+++ b/src/pages-and-resources/discussions/app-list/AppList.jsx
@@ -1,11 +1,10 @@
import React, { useCallback, useEffect } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
-import { CardGrid, Container, LargerThanExtraSmall } from '@edx/paragon';
+import { CardGrid, Container, breakpoints } from '@edx/paragon';
import { useDispatch, useSelector } from 'react-redux';
-
+import Responsive from 'react-responsive';
import { useModels } from '../../../generic/model-store';
import { selectApp, LOADED, LOADING } from '../data/slice';
-
import AppCard from './AppCard';
import messages from './messages';
import FeaturesTable from './FeaturesTable';
@@ -73,7 +72,7 @@ function AppList({ intl }) {
/>
))}
-
+
{intl.formatMessage(messages.supportedFeatures)}
@@ -83,7 +82,7 @@ function AppList({ intl }) {
features={features}
/>
-
+
);
}
diff --git a/src/pages-and-resources/discussions/app-list/AppList.test.jsx b/src/pages-and-resources/discussions/app-list/AppList.test.jsx
index 4a21ffcec..fa2ea9cb2 100644
--- a/src/pages-and-resources/discussions/app-list/AppList.test.jsx
+++ b/src/pages-and-resources/discussions/app-list/AppList.test.jsx
@@ -1,21 +1,107 @@
+import React from 'react';
+
+import { initializeMockApp } from '@edx/frontend-platform';
+import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { AppProvider } from '@edx/frontend-platform/react';
+import { breakpoints } from '@edx/paragon';
+import {
+ queryByText, render, queryAllByRole, queryByRole, getByRole, queryByLabelText, getByLabelText, queryAllByText,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import MockAdapter from 'axios-mock-adapter';
+import { Context as ResponsiveContext } from 'react-responsive';
+
+import initializeStore from '../../../store';
+import executeThunk from '../../../utils';
+import { getAppsUrl } from '../data/api';
+import { fetchApps } from '../data/thunks';
+import { emptyAppApiResponse, legacyApiResponse, piazzaApiResponse } from '../factories/mockApiResponses';
+import AppList from './AppList';
+import messages from './messages';
+
+const courseId = 'course-v1:edX+TestX+Test_Course';
+
describe('AppList', () => {
- test('displays a message when there are no apps available', () => {
+ let axiosMock;
+ let store;
+ let container;
+ function createComponent(screenWidth = breakpoints.extraLarge.minWidth) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ beforeEach(async () => {
+ initializeMockApp({
+ authenticatedUser: {
+ userId: 3,
+ username: 'abc123',
+ administrator: true,
+ roles: [],
+ },
+ });
+
+ store = await initializeStore();
+ axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});
- test('display a card for each available app', () => {
+ const mockStore = async (mockResponse, screenWidth = breakpoints.extraLarge.minWidth) => {
+ axiosMock.onGet(getAppsUrl(courseId)).reply(200, mockResponse);
+ await executeThunk(fetchApps(courseId), store.dispatch);
+ const component = createComponent(screenWidth);
+ const wrapper = render(component);
+ container = wrapper.container;
+ };
+ test('displays a message when there are no apps available', async () => {
+ await mockStore(emptyAppApiResponse);
+ expect(queryByText(container, `${messages.noApps.defaultMessage}`)).toBeInTheDocument();
});
- test('displays the FeaturesTable at desktop sizes', () => {
-
+ test('displays loading state when there is no active App', async () => {
+ await mockStore({});
+ expect(queryByRole(container, 'status')).toBeInTheDocument();
});
- test('hides the FeaturesTable at mobile sizes', () => {
-
+ test('display a card for each available app', async () => {
+ await mockStore(piazzaApiResponse);
+ const appCount = store.getState().discussions.appIds.length;
+ expect(queryAllByRole(container, 'radio')).toHaveLength(appCount);
});
- test('onSelectApp is called when an app is clicked', () => {
+ test('displays the FeaturesTable at desktop sizes', async () => {
+ await mockStore(piazzaApiResponse);
+ expect(queryByRole(container, 'table')).toBeInTheDocument();
+ });
+ test('hides the FeaturesTable at mobile sizes', async () => {
+ await mockStore(piazzaApiResponse, breakpoints.extraSmall.maxWidth);
+ expect(queryByRole(container, 'table')).not.toBeInTheDocument();
+ });
+
+ test('hides the FeaturesList at desktop sizes', async () => {
+ await mockStore(piazzaApiResponse);
+ expect(queryByText(container, messages['supportedFeatureList-mobile-show'].defaultMessage)).not.toBeInTheDocument();
+ });
+
+ test('displays the FeaturesList at mobile sizes', async () => {
+ await mockStore(piazzaApiResponse, breakpoints.extraSmall.maxWidth);
+ const appCount = store.getState().discussions.appIds.length;
+ expect(queryAllByText(container, messages['supportedFeatureList-mobile-show'].defaultMessage)).toHaveLength(appCount);
+ });
+
+ test('selectApp is called when an app is clicked', async () => {
+ await mockStore(legacyApiResponse);
+ userEvent.click(getByLabelText(container, 'Select Piazza'));
+ const clickedCard = getByRole(container, 'radio', { checked: true });
+ expect(queryByLabelText(clickedCard, 'Select Piazza')).toBeInTheDocument();
});
});
diff --git a/src/pages-and-resources/discussions/data/redux.test.js b/src/pages-and-resources/discussions/data/redux.test.js
index 8392b35fb..fd41aa50c 100644
--- a/src/pages-and-resources/discussions/data/redux.test.js
+++ b/src/pages-and-resources/discussions/data/redux.test.js
@@ -10,13 +10,7 @@ import {
import { fetchApps, saveAppConfig } from './thunks';
import { LOADED } from '../../../data/slice';
import { legacyApiResponse, piazzaApiResponse } from '../factories/mockApiResponses';
-
-// Helper, that is used to forcibly finalize all promises
-// in thunk before running matcher against state.
-const executeThunk = async (thunk, dispatch, getState) => {
- await thunk(dispatch, getState);
- await new Promise(setImmediate);
-};
+import executeThunk from '../../../utils';
const courseId = 'course-v1:edX+TestX+Test_Course';
const pagesAndResourcesPath = `/course/${courseId}/pages-and-resources`;
diff --git a/src/pages-and-resources/discussions/factories/mockApiResponses.js b/src/pages-and-resources/discussions/factories/mockApiResponses.js
index da8afccb0..a6b8aaac1 100644
--- a/src/pages-and-resources/discussions/factories/mockApiResponses.js
+++ b/src/pages-and-resources/discussions/factories/mockApiResponses.js
@@ -70,3 +70,18 @@ export const legacyApiResponse = {
},
},
};
+
+export const emptyAppApiResponse = {
+ context_key: '',
+ enabled: null,
+ provider_type: '',
+ features: [],
+ lti_configuration: {},
+ plugin_configuration: {},
+ providers: {
+ active: 'legacy',
+ available: {
+
+ },
+ },
+};
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 000000000..21b8a5580
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,6 @@
+const executeThunk = async (thunk, dispatch, getState) => {
+ await thunk(dispatch, getState);
+ await new Promise(setImmediate);
+};
+
+export default executeThunk;