diff --git a/src/generic/alert-message/AlertMessage.test.jsx b/src/generic/alert-message/AlertMessage.test.tsx
similarity index 100%
rename from src/generic/alert-message/AlertMessage.test.jsx
rename to src/generic/alert-message/AlertMessage.test.tsx
diff --git a/src/generic/alert-message/index.jsx b/src/generic/alert-message/index.jsx
deleted file mode 100644
index 41efb771d..000000000
--- a/src/generic/alert-message/index.jsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from 'react';
-import { Alert } from '@openedx/paragon';
-import PropTypes from 'prop-types';
-
-const AlertMessage = ({ title, description, ...props }) => (
-
- {title}
- {description}
-
-);
-
-AlertMessage.propTypes = {
- title: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
- description: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
-};
-
-AlertMessage.defaultProps = {
- title: undefined,
- description: undefined,
-};
-
-export default AlertMessage;
diff --git a/src/generic/alert-message/index.tsx b/src/generic/alert-message/index.tsx
new file mode 100644
index 000000000..6b3c3214c
--- /dev/null
+++ b/src/generic/alert-message/index.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { Alert } from '@openedx/paragon';
+
+interface Props extends React.ComponentPropsWithoutRef {
+ title?: string | React.ReactNode;
+ description?: string | React.ReactNode;
+}
+
+const AlertMessage: React.FC = ({ title, description, ...props }) => (
+
+ {title}
+ {description}
+
+);
+
+export default AlertMessage;
diff --git a/src/studio-home/StudioHome.jsx b/src/studio-home/StudioHome.tsx
similarity index 95%
rename from src/studio-home/StudioHome.jsx
rename to src/studio-home/StudioHome.tsx
index cc62a40a3..9af6ccb2b 100644
--- a/src/studio-home/StudioHome.jsx
+++ b/src/studio-home/StudioHome.tsx
@@ -8,7 +8,7 @@ import {
Row,
} from '@openedx/paragon';
import { Add as AddIcon, Error } from '@openedx/paragon/icons';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { StudioFooter } from '@edx/frontend-component-footer';
import { getConfig } from '@edx/frontend-platform';
import { useLocation, useNavigate } from 'react-router-dom';
@@ -28,7 +28,8 @@ import messages from './messages';
import { useStudioHome } from './hooks';
import AlertMessage from '../generic/alert-message';
-const StudioHome = ({ intl }) => {
+const StudioHome = () => {
+ const intl = useIntl();
const location = useLocation();
const navigate = useNavigate();
@@ -46,7 +47,6 @@ const StudioHome = ({ intl }) => {
hasAbilityToCreateNewCourse,
isFiltered,
setShowNewCourseContainer,
- dispatch,
} = useStudioHome(isPaginationCoursesEnabled);
const libMode = getConfig().LIBRARY_MODE;
@@ -64,7 +64,7 @@ const StudioHome = ({ intl }) => {
} = studioHomeData;
const getHeaderButtons = useCallback(() => {
- const headerButtons = [];
+ const headerButtons: JSX.Element[] = [];
if (isFailedLoadingPage || !userIsActive) {
return headerButtons;
@@ -160,11 +160,9 @@ const StudioHome = ({ intl }) => {
)}
{isShowOrganizationDropdown && }
setShowNewCourseContainer(true)}
isShowProcessing={isShowProcessing && !isFiltered}
- dispatch={dispatch}
isPaginationCoursesEnabled={isPaginationCoursesEnabled}
/>
@@ -203,8 +201,4 @@ const StudioHome = ({ intl }) => {
);
};
-StudioHome.propTypes = {
- intl: intlShape.isRequired,
-};
-
-export default injectIntl(StudioHome);
+export default StudioHome;
diff --git a/src/studio-home/card-item/CardItem.test.jsx b/src/studio-home/card-item/CardItem.test.jsx
deleted file mode 100644
index 74eb6e870..000000000
--- a/src/studio-home/card-item/CardItem.test.jsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React from 'react';
-import { useSelector } from 'react-redux';
-import { render, fireEvent } from '@testing-library/react';
-import { IntlProvider } from '@edx/frontend-platform/i18n';
-import { AppProvider } from '@edx/frontend-platform/react';
-import { initializeMockApp, getConfig } from '@edx/frontend-platform';
-
-import { studioHomeMock } from '../__mocks__';
-import messages from '../messages';
-import initializeStore from '../../store';
-import { trimSlashes } from './utils';
-import CardItem from '.';
-
-jest.mock('react-redux', () => ({
- ...jest.requireActual('react-redux'),
- useSelector: jest.fn(),
-}));
-
-let store;
-
-const RootWrapper = (props) => (
-
-
-
-
-
-);
-
-describe('', () => {
- beforeEach(() => {
- initializeMockApp({
- authenticatedUser: {
- userId: 3,
- username: 'abc123',
- administrator: true,
- roles: [],
- },
- });
- store = initializeStore();
- useSelector.mockReturnValue(studioHomeMock);
- });
- it('should render course details for non-library course', () => {
- const props = studioHomeMock.archivedCourses[0];
- const { getByText } = render();
- expect(getByText(`${props.org} / ${props.number} / ${props.run}`)).toBeInTheDocument();
- });
-
- it('should render correct links for non-library course', () => {
- const props = studioHomeMock.archivedCourses[0];
- const { getByText } = render();
- const courseTitleLink = getByText(props.displayName);
- expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
- const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage);
- expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
- const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
- expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
- });
-
- it('should render correct links for non-library course pagination', () => {
- const props = studioHomeMock.archivedCourses[0];
- const { getByText, getByTestId } = render();
- const courseTitleLink = getByText(props.displayName);
- expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
- const dropDownMenu = getByTestId('toggle-dropdown');
- fireEvent.click(dropDownMenu);
- const btnReRunCourse = getByText(messages.btnReRunText.defaultMessage);
- expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
- const viewLiveLink = getByText(messages.viewLiveBtnText.defaultMessage);
- expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
- });
- it('should render course details for library course', () => {
- const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
- const { getByText } = render();
- const courseTitleLink = getByText(props.displayName);
- expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
- expect(getByText(`${props.org} / ${props.number}`)).toBeInTheDocument();
- });
- it('should hide rerun button if disallowed', () => {
- const props = studioHomeMock.archivedCourses[0];
- useSelector.mockReturnValue({ ...studioHomeMock, allowCourseReruns: false });
- const { queryByText } = render();
- expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
- });
- it('should be read only course if old mongo course', () => {
- const props = studioHomeMock.courses[1];
- const { queryByText } = render();
- expect(queryByText(props.displayName)).not.toHaveAttribute('href');
- expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
- expect(queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument();
- });
-
- it('should render course key if displayname is empty', () => {
- const props = studioHomeMock.courses[1];
- const courseKeyTest = 'course-key';
- const { getByText } = render(
- ,
- );
- expect(getByText(courseKeyTest)).toBeInTheDocument();
- });
-});
diff --git a/src/studio-home/card-item/CardItem.test.tsx b/src/studio-home/card-item/CardItem.test.tsx
new file mode 100644
index 000000000..e2cd39345
--- /dev/null
+++ b/src/studio-home/card-item/CardItem.test.tsx
@@ -0,0 +1,89 @@
+import * as reactRedux from 'react-redux';
+import { getConfig } from '@edx/frontend-platform';
+
+import { studioHomeMock } from '../__mocks__';
+import messages from '../messages';
+import { trimSlashes } from './utils';
+import {
+ fireEvent,
+ initializeMocks,
+ render,
+ screen,
+} from '../../testUtils';
+import CardItem from '.';
+
+jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => studioHomeMock);
+
+describe('', () => {
+ beforeEach(() => {
+ initializeMocks();
+ });
+ it('should render course details for non-library course', () => {
+ const props = studioHomeMock.archivedCourses[0];
+ render();
+ expect(screen.getByText(`${props.org} / ${props.number} / ${props.run}`)).toBeInTheDocument();
+ });
+
+ it('should render correct links for non-library course', () => {
+ const props = studioHomeMock.archivedCourses[0];
+ render();
+ const courseTitleLink = screen.getByText(props.displayName);
+ expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
+ const btnReRunCourse = screen.getByText(messages.btnReRunText.defaultMessage);
+ expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
+ const viewLiveLink = screen.getByText(messages.viewLiveBtnText.defaultMessage);
+ expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
+ });
+
+ it('should render correct links for non-library course pagination', () => {
+ const props = studioHomeMock.archivedCourses[0];
+ render();
+ const courseTitleLink = screen.getByText(props.displayName);
+ expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
+ const dropDownMenu = screen.getByTestId('toggle-dropdown');
+ fireEvent.click(dropDownMenu);
+ const btnReRunCourse = screen.getByText(messages.btnReRunText.defaultMessage);
+ expect(btnReRunCourse).toHaveAttribute('href', trimSlashes(props.rerunLink));
+ const viewLiveLink = screen.getByText(messages.viewLiveBtnText.defaultMessage);
+ expect(viewLiveLink).toHaveAttribute('href', props.lmsLink);
+ });
+ it('should render course details for library course', () => {
+ const props = { ...studioHomeMock.archivedCourses[0], isLibraries: true };
+ render();
+ const courseTitleLink = screen.getByText(props.displayName);
+ expect(courseTitleLink).toHaveAttribute('href', `${getConfig().STUDIO_BASE_URL}${props.url}`);
+ expect(screen.getByText(`${props.org} / ${props.number}`)).toBeInTheDocument();
+ });
+ it('should hide rerun button if disallowed', () => {
+ const props = studioHomeMock.archivedCourses[0];
+ // Update our mocked redux data:
+ jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => (
+ { ...studioHomeMock, allowCourseReruns: false }
+ ));
+ const { queryByText } = render();
+ expect(queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
+ });
+ it('should be read only course if old mongo course', () => {
+ const props = studioHomeMock.courses[1];
+ render();
+ expect(screen.queryByText(props.displayName)).not.toHaveAttribute('href');
+ expect(screen.queryByText(messages.btnReRunText.defaultMessage)).not.toBeInTheDocument();
+ expect(screen.queryByText(messages.viewLiveBtnText.defaultMessage)).not.toBeInTheDocument();
+ });
+
+ it('should render course key if displayname is empty', () => {
+ const props = studioHomeMock.courses[1];
+ const courseKeyTest = 'course-key';
+ render(
+ ,
+ );
+ expect(screen.getByText(courseKeyTest)).toBeInTheDocument();
+ });
+});
diff --git a/src/studio-home/card-item/index.jsx b/src/studio-home/card-item/index.tsx
similarity index 69%
rename from src/studio-home/card-item/index.jsx
rename to src/studio-home/card-item/index.tsx
index 11f932737..35883bb90 100644
--- a/src/studio-home/card-item/index.jsx
+++ b/src/studio-home/card-item/index.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { useSelector } from 'react-redux';
-import PropTypes from 'prop-types';
import {
Card,
Hyperlink,
@@ -9,16 +8,40 @@ import {
ActionRow,
} from '@openedx/paragon';
import { MoreHoriz } from '@openedx/paragon/icons';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
+import { Link } from 'react-router-dom';
import { COURSE_CREATOR_STATES } from '../../constants';
import { getStudioHomeData } from '../data/selectors';
import messages from '../messages';
import { trimSlashes } from './utils';
-const CardItem = ({
- intl,
+interface BaseProps {
+ displayName: string;
+ org: string;
+ number: string;
+ run?: string;
+ lmsLink?: string | null;
+ rerunLink?: string | null;
+ courseKey?: string;
+ isLibraries?: boolean;
+ isPaginated?: boolean;
+}
+type Props = BaseProps & (
+ /** If we should open this course/library in this MFE, this is the path to the edit page, e.g. '/course/foo' */
+ { path: string, url?: never } |
+ /**
+ * If we might be redirecting to the legacy Studio view, this is the URL to redirect to.
+ * URLs starting with '/' are assumed to be relative to the legacy Studio root.
+ */
+ { url: string, path?: never }
+);
+
+/**
+ * A card on the Studio home page that represents a Course or a Library
+ */
+const CardItem: React.FC = ({
displayName,
lmsLink = '',
rerunLink = '',
@@ -28,14 +51,16 @@ const CardItem = ({
isLibraries = false,
courseKey = '',
isPaginated = false,
+ path,
url,
}) => {
+ const intl = useIntl();
const {
allowCourseReruns,
courseCreatorStatus,
rerunCreatorStatus,
} = useSelector(getStudioHomeData);
- const destinationUrl = () => new URL(url, getConfig().STUDIO_BASE_URL);
+ const destinationUrl: string = path ?? new URL(url, getConfig().STUDIO_BASE_URL).toString();
const subtitle = isLibraries ? `${org} / ${number}` : `${org} / ${number} / ${run}`;
const readOnlyItem = !(lmsLink || rerunLink || url);
const showActions = !(readOnlyItem || isLibraries);
@@ -49,12 +74,12 @@ const CardItem = ({
{hasDisplayName}
-
+
) : (
{displayName}
)}
@@ -70,7 +95,7 @@ const CardItem = ({
/>
{isShowRerunLink && (
-
+
{messages.btnReRunText.defaultMessage}
)}
@@ -84,7 +109,7 @@ const CardItem = ({
{isShowRerunLink && (
{intl.formatMessage(messages.btnReRunText)}
@@ -92,7 +117,7 @@ const CardItem = ({
)}
{intl.formatMessage(messages.viewLiveBtnText)}
@@ -105,18 +130,4 @@ const CardItem = ({
);
};
-CardItem.propTypes = {
- intl: intlShape.isRequired,
- displayName: PropTypes.string.isRequired,
- lmsLink: PropTypes.string,
- rerunLink: PropTypes.string,
- org: PropTypes.string.isRequired,
- run: PropTypes.string,
- number: PropTypes.string.isRequired,
- url: PropTypes.string.isRequired,
- isLibraries: PropTypes.bool,
- courseKey: PropTypes.string,
- isPaginated: PropTypes.bool,
-};
-
-export default injectIntl(CardItem);
+export default CardItem;
diff --git a/src/studio-home/card-item/utils.js b/src/studio-home/card-item/utils.ts
similarity index 71%
rename from src/studio-home/card-item/utils.js
rename to src/studio-home/card-item/utils.ts
index 8bbc7c2e2..be873d9fe 100644
--- a/src/studio-home/card-item/utils.js
+++ b/src/studio-home/card-item/utils.ts
@@ -4,4 +4,4 @@
* @returns {string} The trimmed string.
*/
// eslint-disable-next-line import/prefer-default-export
-export const trimSlashes = (str) => str.replace(/^\/|\/$/g, '');
+export const trimSlashes = (str: string): string => str.replace(/^\/|\/$/g, '');
diff --git a/src/studio-home/data/api.js b/src/studio-home/data/api.js
index 630bb52b6..b70af23dd 100644
--- a/src/studio-home/data/api.js
+++ b/src/studio-home/data/api.js
@@ -16,6 +16,7 @@ export async function getStudioHomeData() {
return camelCaseObject(data);
}
+/** Get list of courses from the deprecated non-paginated API */
export async function getStudioHomeCourses(search) {
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/contentstore/v1/home/courses${search}`);
return camelCaseObject(data);
diff --git a/src/studio-home/factories/mockApiResponses.jsx b/src/studio-home/factories/mockApiResponses.jsx
index 5d75f9f59..a9993b2ca 100644
--- a/src/studio-home/factories/mockApiResponses.jsx
+++ b/src/studio-home/factories/mockApiResponses.jsx
@@ -48,36 +48,32 @@ export const generateGetStudioHomeDataApiResponse = () => ({
allowToCreateNewOrg: false,
});
+/** Mock for the deprecated /api/contentstore/v1/home/courses endpoint. Note this endpoint is NOT paginated. */
export const generateGetStudioCoursesApiResponse = () => ({
- count: 5,
- next: null,
- previous: null,
- numPages: 2,
- results: {
- courses: [
- {
- courseKey: 'course-v1:HarvardX+123+2023',
- displayName: 'Managing Risk in the Information Age',
- lmsLink: '//localhost:18000/courses/course-v1:HarvardX+123+2023/jump_to/block-v1:HarvardX+123+2023+type@course+block@course',
- number: '123',
- org: 'HarvardX',
- rerunLink: '/course_rerun/course-v1:HarvardX+123+2023',
- run: '2023',
- url: '/course/course-v1:HarvardX+123+2023',
- },
- {
- courseKey: 'org.0/course_0/Run_0',
- displayName: 'Run 0',
- lmsLink: null,
- number: 'course_0',
- org: 'org.0',
- rerunLink: null,
- run: 'Run_0',
- url: null,
- },
- ],
- inProcessCourseActions: [],
- },
+ archivedCourses: /** @type {any[]} */([]),
+ courses: [
+ {
+ courseKey: 'course-v1:HarvardX+123+2023',
+ displayName: 'Managing Risk in the Information Age',
+ lmsLink: '//localhost:18000/courses/course-v1:HarvardX+123+2023/jump_to/block-v1:HarvardX+123+2023+type@course+block@course',
+ number: '123',
+ org: 'HarvardX',
+ rerunLink: '/course_rerun/course-v1:HarvardX+123+2023',
+ run: '2023',
+ url: '/course/course-v1:HarvardX+123+2023',
+ },
+ {
+ courseKey: 'org.0/course_0/Run_0',
+ displayName: 'Run 0',
+ lmsLink: null,
+ number: 'course_0',
+ org: 'org.0',
+ rerunLink: null,
+ run: 'Run_0',
+ url: null,
+ },
+ ],
+ inProcessCourseActions: [],
});
export const generateGetStudioCoursesApiResponseV2 = () => ({
diff --git a/src/studio-home/hooks.jsx b/src/studio-home/hooks.jsx
index e596e18be..08662810f 100644
--- a/src/studio-home/hooks.jsx
+++ b/src/studio-home/hooks.jsx
@@ -93,7 +93,6 @@ const useStudioHome = (isPaginated = false) => {
isShowOrganizationDropdown,
hasAbilityToCreateNewCourse,
isFiltered,
- dispatch,
setShowNewCourseContainer,
};
};
diff --git a/src/studio-home/index.js b/src/studio-home/index.ts
similarity index 100%
rename from src/studio-home/index.js
rename to src/studio-home/index.ts
diff --git a/src/studio-home/messages.js b/src/studio-home/messages.ts
similarity index 100%
rename from src/studio-home/messages.js
rename to src/studio-home/messages.ts
diff --git a/src/studio-home/tabs-section/TabsSection.test.jsx b/src/studio-home/tabs-section/TabsSection.test.tsx
similarity index 82%
rename from src/studio-home/tabs-section/TabsSection.test.jsx
rename to src/studio-home/tabs-section/TabsSection.test.tsx
index 6f40c9212..23540af71 100644
--- a/src/studio-home/tabs-section/TabsSection.test.jsx
+++ b/src/studio-home/tabs-section/TabsSection.test.tsx
@@ -1,16 +1,6 @@
-import React from 'react';
-import { MemoryRouter, Routes, Route } from 'react-router-dom';
-import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
-import { getConfig, initializeMockApp, setConfig } from '@edx/frontend-platform';
-import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
-import {
- waitFor, render, fireEvent, screen, act,
-} from '@testing-library/react';
-import { IntlProvider } from '@edx/frontend-platform/i18n';
-import { AppProvider } from '@edx/frontend-platform/react';
-import MockAdapter from 'axios-mock-adapter';
+import { Routes, Route } from 'react-router-dom';
+import { getConfig, setConfig } from '@edx/frontend-platform';
-import initializeStore from '../../store';
import { studioHomeMock } from '../__mocks__';
import messages from '../messages';
import tabMessages from './messages';
@@ -27,6 +17,13 @@ import { executeThunk } from '../../utils';
import { fetchLibraryData, fetchStudioHomeData } from '../data/thunks';
import { getContentLibraryV2ListApiUrl } from '../../library-authoring/data/api';
import contentLibrariesListV2 from '../../library-authoring/__mocks__/contentLibrariesListV2';
+import {
+ initializeMocks,
+ render as baseRender,
+ fireEvent,
+ screen,
+ waitFor,
+} from '../../testUtils';
const { studioShortName } = studioHomeMock;
@@ -36,56 +33,39 @@ const courseApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/courses`;
const courseApiLinkV2 = `${getApiBaseUrl()}/api/contentstore/v2/home/courses`;
const libraryApiLink = `${getApiBaseUrl()}/api/contentstore/v1/home/libraries`;
-const mockDispatch = jest.fn();
-
-const queryClient = new QueryClient();
-
const tabSectionComponent = (overrideProps) => (
{}}
+ isShowProcessing
{...overrideProps}
/>
);
-const RootWrapper = (overrideProps) => (
-
-
-
-
-
-
-
-
-
-
-
-
-
+const render = (overrideProps = {}) => baseRender(
+
+
+
+
+ ,
+ { routerProps: { initialEntries: ['/home'] } },
);
describe('', () => {
beforeEach(() => {
- initializeMockApp({
- authenticatedUser: {
- userId: 3,
- username: 'abc123',
- administrator: true,
- roles: [],
- },
- });
- store = initializeStore(initialState);
- axiosMock = new MockAdapter(getAuthenticatedHttpClient());
+ const newMocks = initializeMocks({ initialState });
+ store = newMocks.reduxStore;
+ axiosMock = newMocks.axiosMock;
setConfig({
...getConfig(),
LIBRARY_MODE: 'mixed',
@@ -94,7 +74,7 @@ describe('', () => {
});
it('should render all tabs correctly', async () => {
- const data = generateGetStudioHomeDataApiResponse();
+ const data: any = generateGetStudioHomeDataApiResponse();
data.archivedCourses = [{
courseKey: 'course-v1:MachineLearning+123+2023',
displayName: 'Machine Learning',
@@ -106,7 +86,7 @@ describe('', () => {
url: '/course/course-v1:MachineLearning+123+2023',
}];
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -127,7 +107,7 @@ describe('', () => {
const data = generateGetStudioHomeDataApiResponse();
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -148,7 +128,7 @@ describe('', () => {
const data = generateGetStudioHomeDataApiResponse();
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -163,8 +143,8 @@ describe('', () => {
describe('course tab', () => {
it('should render specific course details', async () => {
- render();
- const { results: data } = generateGetStudioCoursesApiResponse();
+ render();
+ const data = generateGetStudioCoursesApiResponse();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLink).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -178,9 +158,9 @@ describe('', () => {
it('should render default sections when courses are empty', async () => {
const data = generateGetStudioCoursesApiResponse();
- data.results.courses = [];
+ data.courses = [];
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLink).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -195,7 +175,7 @@ describe('', () => {
});
it('should render course fetch failure alert', async () => {
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLink).reply(404);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -204,7 +184,7 @@ describe('', () => {
});
it('should render pagination when there are courses', async () => {
- render();
+ render({ isPaginationCoursesEnabled: true });
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLinkV2).reply(200, generateGetStudioCoursesApiResponseV2());
await executeThunk(fetchStudioHomeData('', true, {}, true), store.dispatch);
@@ -224,7 +204,7 @@ describe('', () => {
it('should not render pagination when there are not courses', async () => {
const data = generateGetStudioCoursesApiResponseV2();
data.results.courses = [];
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLinkV2).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -236,7 +216,7 @@ describe('', () => {
it('should set the url path to "/home" when switching away then back to courses tab', async () => {
const data = generateGetStudioCoursesApiResponseV2();
data.results.courses = [];
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLinkV2).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -250,9 +230,7 @@ describe('', () => {
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
await executeThunk(fetchLibraryData(), store.dispatch);
const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(librariesTab);
- });
+ fireEvent.click(librariesTab);
// confirm that the url path has changed
expect(librariesTab).toHaveClass('active');
@@ -262,9 +240,7 @@ describe('', () => {
// switch back to courses tab
const coursesTab = screen.getByText(tabMessages.coursesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(coursesTab);
- });
+ fireEvent.click(coursesTab);
// confirm that the url path is /home
expect(coursesTab).toHaveClass('active');
@@ -281,7 +257,7 @@ describe('', () => {
ENABLE_TAGGING_TAXONOMY_PAGES: 'false',
});
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -295,7 +271,7 @@ describe('', () => {
ENABLE_TAGGING_TAXONOMY_PAGES: 'true',
});
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -310,8 +286,8 @@ describe('', () => {
describe('archived tab', () => {
it('should switch to Archived tab and render specific archived course details', async () => {
- render();
- const { results: data } = generateGetStudioCoursesApiResponse();
+ render();
+ const data = generateGetStudioCoursesApiResponse();
data.archivedCourses = studioHomeMock.archivedCourses;
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLink).reply(200, data);
@@ -331,7 +307,7 @@ describe('', () => {
const data = generateGetStudioCoursesApiResponse();
data.archivedCourses = [];
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(courseApiLink).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -347,34 +323,33 @@ describe('', () => {
});
describe('library tab', () => {
+ beforeEach(() => {
+ axiosMock.onGet(courseApiLink).reply(200, generateGetStudioCoursesApiResponse());
+ });
it('should switch to Legacy Libraries tab and render specific v1 library details', async () => {
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
await executeThunk(fetchLibraryData(), store.dispatch);
const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(librariesTab);
- });
+ fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
- expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
+ expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
});
it('should switch to Libraries tab and render specific v2 library details', async () => {
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(librariesTab);
- });
+ fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
@@ -397,20 +372,18 @@ describe('', () => {
LIBRARY_MODE: 'v1 only',
});
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(libraryApiLink).reply(200, generateGetStudioHomeLibrariesApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
await executeThunk(fetchLibraryData(), store.dispatch);
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(librariesTab);
- });
+ fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
- expect(screen.getByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
+ expect(await screen.findByText(studioHomeMock.libraries[0].displayName)).toBeVisible();
expect(screen.getByText(`${studioHomeMock.libraries[0].org} / ${studioHomeMock.libraries[0].number}`)).toBeVisible();
});
@@ -421,14 +394,12 @@ describe('', () => {
LIBRARY_MODE: 'v2 only',
});
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
await executeThunk(fetchStudioHomeData(), store.dispatch);
const librariesTab = screen.getByText(tabMessages.librariesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(librariesTab);
- });
+ fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
@@ -449,7 +420,7 @@ describe('', () => {
const data = generateGetStudioHomeDataApiResponse();
data.librariesEnabled = false;
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -461,7 +432,7 @@ describe('', () => {
const data = generateGetStudioHomeDataApiResponse();
data.redirectToLibraryAuthoringMfe = true;
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, data);
await executeThunk(fetchStudioHomeData(), store.dispatch);
@@ -474,20 +445,18 @@ describe('', () => {
});
it('should render libraries fetch failure alert', async () => {
- render();
+ render();
axiosMock.onGet(getStudioHomeApiUrl()).reply(200, generateGetStudioHomeDataApiResponse());
axiosMock.onGet(libraryApiLink).reply(404);
await executeThunk(fetchStudioHomeData(), store.dispatch);
await executeThunk(fetchLibraryData(), store.dispatch);
const librariesTab = screen.getByText(tabMessages.legacyLibrariesTabTitle.defaultMessage);
- await act(async () => {
- fireEvent.click(librariesTab);
- });
+ fireEvent.click(librariesTab);
expect(librariesTab).toHaveClass('active');
- expect(screen.getByText(tabMessages.librariesTabErrorMessage.defaultMessage)).toBeVisible();
+ expect(await screen.findByText(tabMessages.librariesTabErrorMessage.defaultMessage)).toBeVisible();
});
});
});
diff --git a/src/studio-home/tabs-section/courses-tab/index.test.jsx b/src/studio-home/tabs-section/courses-tab/index.test.tsx
similarity index 98%
rename from src/studio-home/tabs-section/courses-tab/index.test.jsx
rename to src/studio-home/tabs-section/courses-tab/index.test.tsx
index 3c4811edc..81c72307e 100644
--- a/src/studio-home/tabs-section/courses-tab/index.test.jsx
+++ b/src/studio-home/tabs-section/courses-tab/index.test.tsx
@@ -10,8 +10,6 @@ import { initialState } from '../../factories/mockApiResponses';
import CoursesTab from '.';
-const mockDispatch = jest.fn();
-
const onClickNewCourse = jest.fn();
const isShowProcessing = false;
const isLoading = false;
@@ -23,7 +21,7 @@ const showNewCourseContainer = true;
const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
// Generate a custom initial state based on studioHomeCoursesRequestParams
- const customInitialState = {
+ const customInitialState: any = { // TODO: remove 'any' once our redux state has proper types
...initialState,
studioHome: {
...initialState.studioHome,
@@ -38,7 +36,6 @@ const renderComponent = (overrideProps = {}, studioHomeState = {}) => {
void;
+ isShowProcessing: boolean;
+ isLoading: boolean;
+ isFailed: boolean;
+ numPages: number;
+ coursesCount: number;
+ isEnabledPagination?: boolean;
+}
+
+const CoursesTab: React.FC = ({
coursesDataItems,
showNewCourseContainer,
onClickNewCourse,
isShowProcessing,
isLoading,
isFailed,
- dispatch,
- numPages,
- coursesCount,
- isEnabledPagination,
+ numPages = 0,
+ coursesCount = 0,
+ isEnabledPagination = false,
}) => {
+ const dispatch = useDispatch();
const intl = useIntl();
const location = useLocation();
const {
@@ -136,7 +156,6 @@ const CoursesTab = ({
number,
run,
url,
- cmsLink,
}) => (
),
@@ -197,34 +215,4 @@ const CoursesTab = ({
);
};
-CoursesTab.defaultProps = {
- numPages: 0,
- coursesCount: 0,
- isEnabledPagination: false,
-};
-
-CoursesTab.propTypes = {
- coursesDataItems: PropTypes.arrayOf(
- PropTypes.shape({
- courseKey: PropTypes.string.isRequired,
- displayName: PropTypes.string.isRequired,
- lmsLink: PropTypes.string.isRequired,
- number: PropTypes.string.isRequired,
- org: PropTypes.string.isRequired,
- rerunLink: PropTypes.string.isRequired,
- run: PropTypes.string.isRequired,
- url: PropTypes.string.isRequired,
- }),
- ).isRequired,
- showNewCourseContainer: PropTypes.bool.isRequired,
- onClickNewCourse: PropTypes.func.isRequired,
- isShowProcessing: PropTypes.bool.isRequired,
- isLoading: PropTypes.bool.isRequired,
- isFailed: PropTypes.bool.isRequired,
- dispatch: PropTypes.func.isRequired,
- numPages: PropTypes.number,
- coursesCount: PropTypes.number,
- isEnabledPagination: PropTypes.bool,
-};
-
export default CoursesTab;
diff --git a/src/studio-home/tabs-section/index.jsx b/src/studio-home/tabs-section/index.tsx
similarity index 95%
rename from src/studio-home/tabs-section/index.jsx
rename to src/studio-home/tabs-section/index.tsx
index 75a3ae12b..44e1e9ea2 100644
--- a/src/studio-home/tabs-section/index.jsx
+++ b/src/studio-home/tabs-section/index.tsx
@@ -1,9 +1,9 @@
import React, { useMemo, useState, useEffect } from 'react';
-import { useSelector } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import PropTypes from 'prop-types';
import { Tab, Tabs } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
-import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { useIntl } from '@edx/frontend-platform/i18n';
import { useNavigate, useLocation } from 'react-router-dom';
import { getLoadingStatuses, getStudioHomeData } from '../data/selectors';
@@ -17,13 +17,13 @@ import { fetchLibraryData } from '../data/thunks';
import { isMixedOrV1LibrariesMode, isMixedOrV2LibrariesMode } from './utils';
const TabsSection = ({
- intl,
showNewCourseContainer,
onClickNewCourse,
isShowProcessing,
- dispatch,
isPaginationCoursesEnabled,
}) => {
+ const dispatch = useDispatch();
+ const intl = useIntl();
const navigate = useNavigate();
const { pathname } = useLocation();
const libMode = getConfig().LIBRARY_MODE;
@@ -33,7 +33,7 @@ const TabsSection = ({
legacyLibraries: 'legacyLibraries',
archived: 'archived',
taxonomies: 'taxonomies',
- };
+ } as const;
const initTabKeyState = (pname) => {
if (pname.includes('/libraries-v1')) {
@@ -80,7 +80,7 @@ const TabsSection = ({
// Controlling the visibility of tabs when using conditional rendering is necessary for
// the correct operation of iterating over child elements inside the Paragon Tabs component.
const visibleTabs = useMemo(() => {
- const tabs = [];
+ const tabs: JSX.Element[] = [];
tabs.push(
, // TODO: proper typing for our redux state
+} = {}) {
initializeMockApp({
authenticatedUser: user,
});
- reduxStore = initializeReduxStore();
+ reduxStore = initializeReduxStore(initialState as any);
queryClient = new QueryClient({
defaultOptions: {
queries: {