From bd18e874b572d61a74848dacd71ba4436b0fac2d Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Thu, 17 Jul 2025 13:45:22 -0700 Subject: [PATCH] Fix broken StudioHome tests (#2291) There were a ton of problems with these tests, but the main one was the use of `waitFor` without `await`, causing all of the code inside each `waitFor` block to essentially be ignored. Other problems fixed: * Rendering a router inside a router was causing most of the render() calls to fail (our custom `render()` already provides a router so there's no need to provide one in the test case) * Use of `testid` instead of queries based on what users see * Tests could match on content in the body when trying to make assertions about the header * Mock imported via `index.js` was causing `jest-haste-map` to print warnings about duplicate mock names (this is still happening for other mocks) * Passing `courses: null` instead of `courses: []` was causing a broken render on two of the tests. I also made other cleanups to follow best practices. --- src/course-rerun/CourseRerun.test.jsx | 4 +- .../CourseRerunForm.test.jsx | 2 +- .../CreateOrRerunCourseForm.test.jsx | 2 +- .../factories/mockApiResponses.jsx | 4 +- .../LibraryAuthoringPage.test.tsx | 4 +- .../add-content/AddContentWorkflow.test.tsx | 4 +- .../PickLibraryContentModal.test.tsx | 6 +- .../create-library/CreateLibrary.test.tsx | 8 +- src/library-authoring/routes.test.tsx | 4 +- src/studio-home/StudioHome.test.jsx | 289 ------------------ src/studio-home/StudioHome.test.tsx | 237 ++++++++++++++ src/studio-home/StudioHome.tsx | 1 - src/studio-home/__mocks__/index.js | 1 - .../{studioHomeMock.js => studioHomeMock.ts} | 2 +- src/studio-home/card-item/CardItem.test.tsx | 8 +- .../CollapsibleStateWithAction.test.jsx | 2 +- .../CourseNewCourseForm.test.jsx | 2 +- .../home-sidebar/HomeSidebar.test.jsx | 6 +- .../ProcessingCourses.test.jsx | 2 +- .../tabs-section/TabsSection.test.tsx | 2 +- .../tabs-section/courses-tab/index.test.tsx | 6 +- 21 files changed, 271 insertions(+), 325 deletions(-) delete mode 100644 src/studio-home/StudioHome.test.jsx create mode 100644 src/studio-home/StudioHome.test.tsx delete mode 100644 src/studio-home/__mocks__/index.js rename src/studio-home/__mocks__/{studioHomeMock.js => studioHomeMock.ts} (99%) diff --git a/src/course-rerun/CourseRerun.test.jsx b/src/course-rerun/CourseRerun.test.jsx index 6f5cc97d4..cc1373bfa 100644 --- a/src/course-rerun/CourseRerun.test.jsx +++ b/src/course-rerun/CourseRerun.test.jsx @@ -5,8 +5,8 @@ import { fireEvent, render, waitFor, -} from '../testUtils'; -import { studioHomeMock } from '../studio-home/__mocks__'; +} from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { getStudioHomeApiUrl } from '../studio-home/data/api'; import { RequestStatus } from '../data/constants'; import messages from './messages'; diff --git a/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx b/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx index 5b73f1db3..f26b1943f 100644 --- a/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx +++ b/src/course-rerun/course-rerun-form/CourseRerunForm.test.jsx @@ -5,7 +5,7 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; -import { studioHomeMock } from '../../studio-home/__mocks__'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import initializeStore from '../../store'; import CourseRerunForm from '.'; diff --git a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx index 682e8e636..e79b38fb6 100644 --- a/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx +++ b/src/generic/create-or-rerun-course/CreateOrRerunCourseForm.test.jsx @@ -14,7 +14,7 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { AppProvider } from '@edx/frontend-platform/react'; import MockAdapter from 'axios-mock-adapter'; -import { studioHomeMock } from '../../studio-home/__mocks__'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { getStudioHomeApiUrl } from '../../studio-home/data/api'; import { fetchStudioHomeData } from '../../studio-home/data/thunks'; import { RequestStatus } from '../../data/constants'; diff --git a/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx index ca3e0b347..db008301b 100644 --- a/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx +++ b/src/generic/create-or-rerun-course/factories/mockApiResponses.jsx @@ -1,5 +1,5 @@ -import { RequestStatus } from '../../../data/constants'; -import { studioHomeMock } from '../../../studio-home/__mocks__'; +import { RequestStatus } from '@src/data/constants'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; export const courseId = 'course-v1:edX+DemoX+Demo_Course'; diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index dd3ed50a9..d84ff75d7 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -8,7 +8,8 @@ import { screen, waitFor, within, -} from '../testUtils'; +} from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import mockResult from './__mocks__/library-search.json'; import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json'; import { @@ -19,7 +20,6 @@ import { mockXBlockFields, } from './data/api.mocks'; import { mockContentSearchConfig } from '../search-manager/data/api.mock'; -import { studioHomeMock } from '../studio-home/__mocks__'; import { getStudioHomeApiUrl } from '../studio-home/data/api'; import { LibraryLayout } from '.'; import { getLibraryCollectionsApiUrl, getLibraryContainersApiUrl } from './data/api'; diff --git a/src/library-authoring/add-content/AddContentWorkflow.test.tsx b/src/library-authoring/add-content/AddContentWorkflow.test.tsx index 8244fc716..dab4dee98 100644 --- a/src/library-authoring/add-content/AddContentWorkflow.test.tsx +++ b/src/library-authoring/add-content/AddContentWorkflow.test.tsx @@ -8,7 +8,8 @@ import { waitFor, screen, initializeMocks, -} from '../../testUtils'; +} from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import mockResult from '../__mocks__/library-search.json'; import editorCmsApi from '../../editors/data/services/cms/api'; import * as textEditorHooks from '../../editors/containers/TextEditor/hooks'; @@ -19,7 +20,6 @@ import { } from '../data/api.mocks'; import { mockClipboardEmpty } from '../../generic/data/api.mock'; import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/data/api.mock'; -import { studioHomeMock } from '../../studio-home/__mocks__'; import { getStudioHomeApiUrl } from '../../studio-home/data/api'; import LibraryLayout from '../LibraryLayout'; diff --git a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx index 03126f610..0fed30629 100644 --- a/src/library-authoring/add-content/PickLibraryContentModal.test.tsx +++ b/src/library-authoring/add-content/PickLibraryContentModal.test.tsx @@ -1,12 +1,12 @@ -import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/data/api.mock'; +import { mockContentSearchConfig, mockSearchResult } from '@src/search-manager/data/api.mock'; import { fireEvent, render as baseRender, waitFor, screen, initializeMocks, -} from '../../testUtils'; -import { studioHomeMock } from '../../studio-home/__mocks__'; +} from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { getStudioHomeApiUrl } from '../../studio-home/data/api'; import mockResult from '../__mocks__/library-search.json'; import { LibraryProvider } from '../common/context/LibraryContext'; diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index f279f13a1..eb45d8007 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -8,10 +8,10 @@ import { render, screen, waitFor, -} from '../../testUtils'; -import { studioHomeMock } from '../../studio-home/__mocks__'; -import { getStudioHomeApiUrl } from '../../studio-home/data/api'; -import { getApiWaffleFlagsUrl } from '../../data/api'; +} from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; +import { getStudioHomeApiUrl } from '@src/studio-home/data/api'; +import { getApiWaffleFlagsUrl } from '@src/data/api'; import { CreateLibrary } from '.'; import { getContentLibraryV2CreateApiUrl } from './data/api'; diff --git a/src/library-authoring/routes.test.tsx b/src/library-authoring/routes.test.tsx index cc819602d..c07de78c6 100644 --- a/src/library-authoring/routes.test.tsx +++ b/src/library-authoring/routes.test.tsx @@ -1,13 +1,13 @@ import { useEffect } from 'react'; import fetchMock from 'fetch-mock-jest'; +import { initializeMocks, render } from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { mockContentLibrary, } from './data/api.mocks'; import { LibraryLayout } from '.'; import { ContentType, useLibraryRoutes } from './routes'; import mockResult from './__mocks__/library-search.json'; -import { initializeMocks, render } from '../testUtils'; -import { studioHomeMock } from '../studio-home/__mocks__'; import { getStudioHomeApiUrl } from '../studio-home/data/api'; const mockNavigate = jest.fn(); diff --git a/src/studio-home/StudioHome.test.jsx b/src/studio-home/StudioHome.test.jsx deleted file mode 100644 index 8e6eca953..000000000 --- a/src/studio-home/StudioHome.test.jsx +++ /dev/null @@ -1,289 +0,0 @@ -import { useSelector } from 'react-redux'; -import { MemoryRouter, Routes, Route } from 'react-router-dom'; - -import { RequestStatus } from '../data/constants'; -import { COURSE_CREATOR_STATES } from '../constants'; -import { studioHomeMock } from './__mocks__'; -import { getStudioHomeApiUrl } from './data/api'; -import { - act, - fireEvent, - render, - waitFor, - initializeMocks, -} from '../testUtils'; -import messages from './messages'; -import createNewCourseMessages from './create-new-course-form/messages'; -import createOrRerunCourseMessages from '../generic/create-or-rerun-course/messages'; -import { StudioHome } from '.'; - -const { - studioShortName, - studioRequestEmail, -} = studioHomeMock; - -jest.mock('react-redux', () => ({ - ...jest.requireActual('react-redux'), - useSelector: jest.fn(), -})); - -const mockNavigate = jest.fn(); - -jest.mock('react-router-dom', () => ({ - ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts - useNavigate: () => mockNavigate, -})); - -const RootWrapper = () => ( - - - } - /> - } - /> - } - /> - , - -); - -describe('', () => { - describe('api fetch fails', () => { - beforeEach(async () => { - const mocks = initializeMocks(); - mocks.axiosMock.onGet(getStudioHomeApiUrl()).reply(404); - useSelector.mockReturnValue({ studioHomeLoadingStatus: RequestStatus.FAILED }); - }); - - it('should render fetch error', () => { - const { getByText } = render(); - waitFor(() => { - expect(getByText(messages.homePageLoadFailedMessage.defaultMessage)).toBeInTheDocument(); - }); - }); - - it('should render Studio home title', () => { - const { getByText } = render(); - waitFor(() => { - expect(getByText('Studio home')).toBeInTheDocument(); - }); - }); - }); - - describe('api fetch succeeds', () => { - beforeEach(async () => { - const mocks = initializeMocks(); - mocks.axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); - useSelector.mockReturnValue(studioHomeMock); - }); - - it('should render page and page title correctly', () => { - const { getByText } = render(); - waitFor(() => { - expect(getByText(`${studioShortName} home`)).toBeInTheDocument(); - }); - }); - - it('should render email staff header button', async () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite, - }); - - const { getByRole } = render(); - waitFor(() => { - expect(getByRole('link', { name: messages.emailStaffBtnText.defaultMessage })) - .toHaveAttribute('href', `mailto:${studioRequestEmail}`); - }); - }); - - it('should render create new course button', async () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courseCreatorStatus: COURSE_CREATOR_STATES.granted, - }); - - const { getByRole } = render(); - waitFor(() => { - expect(getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage })).toBeInTheDocument(); - }); - }); - - it('should show verify email layout if user inactive', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - userIsActive: false, - }); - - const { getByText } = render(); - waitFor(() => { - expect(getByText('Thanks for signing up, abc123!', { exact: false })).toBeInTheDocument(); - }); - }); - - it('shows the spinner before the query is complete', async () => { - useSelector.mockReturnValue({ - studioHomeLoadingStatus: RequestStatus.IN_PROGRESS, - userIsActive: true, - }); - - await act(async () => { - const { getByRole } = render(); - waitFor(() => { - const spinner = getByRole('status'); - expect(spinner.textContent).toEqual('Loading...'); - }); - }); - }); - - describe('render new library button', () => { - it('should navigate to home_library when libraries-v2 disabled', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courseCreatorStatus: COURSE_CREATOR_STATES.granted, - librariesV2Enabled: false, - }); - const studioBaseUrl = 'http://localhost:18010'; - - const { getByTestId } = render(); - waitFor(() => { - const createNewLibraryButton = getByTestId('new-library-button'); - - const { open } = window; - window.open = jest.fn(); - fireEvent.click(createNewLibraryButton); - expect(window.open).toHaveBeenCalledWith(`${studioBaseUrl}/home_library`); - window.open = open; - }); - }); - - it('should navigate to the library authoring page in course authoring', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - librariesV1Enabled: false, - }); - const { getByTestId } = render(); - waitFor(() => { - const createNewLibraryButton = getByTestId('new-library-button'); - fireEvent.click(createNewLibraryButton); - expect(mockNavigate).toHaveBeenCalledWith('/library/create'); - }); - }); - }); - - it('do not render new library button for "v1 only" mode if showNewLibraryButton is False', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - showNewLibraryButton: false, - librariesV2Enabled: false, - }); - const { queryByTestId } = render(); - expect(queryByTestId('new-library-button')).not.toBeInTheDocument(); - }); - - it('render new library button for "v2 only" mode even if showNewLibraryButton is False', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - showNewLibraryButton: false, - librariesV1Enabled: false, - }); - const { queryByTestId } = render(); - waitFor(() => { - expect(queryByTestId('new-library-button')).toBeInTheDocument(); - }); - }); - - it('should render create new course container', async () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courseCreatorStatus: COURSE_CREATOR_STATES.granted, - }); - - const { getByRole, getByText } = render(); - waitFor(() => { - const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage }); - - act(() => fireEvent.click(createNewCourseButton)); - expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument(); - }); - }); - - it('should hide create new course container', async () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courseCreatorStatus: COURSE_CREATOR_STATES.granted, - }); - - const { getByRole, queryByText, getByText } = render(); - - waitFor(() => { - const createNewCourseButton = getByRole('button', { name: messages.addNewCourseBtnText.defaultMessage }); - fireEvent.click(createNewCourseButton); - expect(getByText(createNewCourseMessages.createNewCourse.defaultMessage)).toBeInTheDocument(); - }); - - waitFor(() => { - const cancelButton = getByRole('button', { name: createOrRerunCourseMessages.cancelButton.defaultMessage }); - fireEvent.click(cancelButton); - expect(queryByText(createNewCourseMessages.createNewCourse.defaultMessage)).not.toBeInTheDocument(); - }); - }); - - describe('contact administrator card', () => { - it('should show contact administrator card with no add course buttons', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courses: null, - courseCreatorStatus: COURSE_CREATOR_STATES.pending, - }); - const { getByText, queryByText } = render(); - const defaultTitleMessage = messages.defaultSection_1_Title.defaultMessage; - waitFor(() => { - const titleWithStudioName = defaultTitleMessage.replace('{studioShortName}', 'Studio'); - const administratorCardTitle = getByText(titleWithStudioName); - - expect(administratorCardTitle).toBeVisible(); - - const addCourseButton = queryByText(messages.btnAddNewCourseText.defaultMessage); - expect(addCourseButton).toBeNull(); - }); - }); - - it('should show contact administrator card with add course buttons', () => { - useSelector.mockReturnValue({ - ...studioHomeMock, - courses: null, - courseCreatorStatus: COURSE_CREATOR_STATES.granted, - }); - const { getByText, getByTestId } = render(); - const defaultTitleMessage = messages.defaultSection_1_Title.defaultMessage; - waitFor(() => { - const titleWithStudioName = defaultTitleMessage.replace('{studioShortName}', 'Studio'); - const administratorCardTitle = getByText(titleWithStudioName); - - expect(administratorCardTitle).toBeVisible(); - - const addCourseButton = getByTestId('contact-admin-create-course'); - expect(addCourseButton).toBeVisible(); - - fireEvent.click(addCourseButton); - expect(getByTestId('create-course-form')).toBeVisible(); - }); - }); - }); - - it('should show footer', () => { - const { getByText } = render(); - waitFor(() => { - expect(getByText('Looking for help with Studio?')).toBeInTheDocument(); - expect(getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL); - }); - }); - }); -}); diff --git a/src/studio-home/StudioHome.test.tsx b/src/studio-home/StudioHome.test.tsx new file mode 100644 index 000000000..40b104d9f --- /dev/null +++ b/src/studio-home/StudioHome.test.tsx @@ -0,0 +1,237 @@ +import * as reactRedux from 'react-redux'; + +import { + fireEvent, + render, + screen, + waitFor, + initializeMocks, + within, +} from '@src/testUtils'; +import { RequestStatus } from '../data/constants'; +import { COURSE_CREATOR_STATES } from '../constants'; +import studioHomeMock from './__mocks__/studioHomeMock'; +import { getStudioHomeApiUrl } from './data/api'; +import { StudioHome } from '.'; + +const { + studioShortName, + studioRequestEmail, +} = studioHomeMock; + +const mockUseSelector = jest.fn(); +jest.spyOn(reactRedux, 'useSelector').mockImplementation(mockUseSelector); +const mockNavigate = jest.fn(); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useNavigate: () => mockNavigate, +})); + +/** Helper function to get the Studio header in the rendered HTML */ +function getHeaderElement(): HTMLElement { + const header = screen.getByRole('banner'); + expect(header.tagName).toEqual('HEADER'); + return header; +} + +describe('', () => { + describe('api fetch fails', () => { + beforeEach(async () => { + const mocks = initializeMocks(); + mocks.axiosMock.onGet(getStudioHomeApiUrl()).reply(404); + mockUseSelector.mockReturnValue({ studioHomeLoadingStatus: RequestStatus.FAILED }); + }); + + it('should render fetch error', async () => { + render(, { path: '/home' }); + expect(screen.getByText('Failed to load Studio home. Please try again later.')).toBeInTheDocument(); + }); + + it('should render Studio home title', async () => { + render(, { path: '/home' }); + // Search only within the header; don't match on the similar text in the body's error message. + const header = getHeaderElement(); + expect(within(header).getByText('Studio home')).toBeInTheDocument(); + }); + }); + + describe('api fetch succeeds', () => { + beforeEach(async () => { + const mocks = initializeMocks(); + mocks.axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + mockUseSelector.mockReturnValue(studioHomeMock); + }); + + it('should render page and page title correctly', async () => { + render(, { path: '/home' }); + const header = getHeaderElement(); + expect(within(header).getByText(`${studioShortName} home`)).toBeInTheDocument(); + }); + + it('should render "email staff" header button for users without create permission', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.disallowedForThisSite, + }); + + render(, { path: '/home' }); + const header = getHeaderElement(); + const link = within(header).getByRole('link', { name: 'Email staff to create course' }); + expect(link).toHaveAttribute('href', `mailto:${studioRequestEmail}`); + }); + + it('should render create new course button for users with create permission', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + }); + + render(, { path: '/home' }); + const header = getHeaderElement(); + within(header).getByRole('button', { name: 'New course' }); // will error if not found + }); + + it('should show verify email layout if user inactive', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + userIsActive: false, + }); + + render(, { path: '/home' }); + screen.getByText('Thanks for signing up, abc123!', { exact: false }); // will error if not found + }); + + it('shows the spinner before the query is complete', async () => { + mockUseSelector.mockReturnValue({ + studioHomeLoadingStatus: RequestStatus.IN_PROGRESS, + userIsActive: true, + }); + + render(, { path: '/home' }); + const spinner = screen.getByRole('status'); + expect(spinner.textContent).toEqual('Loading...'); + }); + + describe('render new library button', () => { + it('should navigate to home_library when libraries-v2 disabled', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + librariesV2Enabled: false, + }); + const studioBaseUrl = 'http://localhost:18010'; + + render(, { path: '/home' }); + await waitFor(() => { + const createNewLibraryButton = screen.getByRole('button', { name: 'New library' }); + + const mockWindowOpen = jest.spyOn(window, 'open'); + fireEvent.click(createNewLibraryButton); + expect(mockWindowOpen).toHaveBeenCalledWith(`${studioBaseUrl}/home_library`); + mockWindowOpen.mockRestore(); + }); + }); + + it('should navigate to the library authoring page in course authoring', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + librariesV1Enabled: false, + }); + render(, { path: '/home' }); + const createNewLibraryButton = screen.getByRole('button', { name: 'New library' }); + fireEvent.click(createNewLibraryButton); + expect(mockNavigate).toHaveBeenCalledWith('/library/create'); + }); + }); + + it('does not render new library button for "v1 only" mode if showNewLibraryButton is False', () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + showNewLibraryButton: false, + librariesV2Enabled: false, + }); + render(, { path: '/home' }); + expect(screen.queryByRole('button', { name: 'New library' })).not.toBeInTheDocument(); + }); + + it('render new library button for "v2 only" mode even if showNewLibraryButton is False', () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + showNewLibraryButton: false, + librariesV1Enabled: false, + }); + render(, { path: '/home' }); + expect(screen.queryByRole('button', { name: 'New library' })).toBeInTheDocument(); + }); + + it('should render "create new course" container', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + }); + + const newCourseContainerText = 'Create a new course'; + render(, { path: '/home' }); + + expect(screen.queryByText(newCourseContainerText)).not.toBeInTheDocument(); + const createNewCourseButton = screen.getByRole('button', { name: 'New course' }); + fireEvent.click(createNewCourseButton); + expect(screen.queryByText(newCourseContainerText)).toBeInTheDocument(); + }); + + it('should hide "create new course" container', async () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + }); + + const newCourseContainerText = 'Create a new course'; + render(, { path: '/home' }); + + const createNewCourseButton = screen.getByRole('button', { name: 'New course' }); + fireEvent.click(createNewCourseButton); + expect(screen.queryByText(newCourseContainerText)).toBeInTheDocument(); + + const cancelButton = screen.getByRole('button', { name: 'Cancel' }); + fireEvent.click(cancelButton); + expect(screen.queryByText(newCourseContainerText)).not.toBeInTheDocument(); + }); + + describe('contact administrator card', () => { + const adminCardTitleText = 'Are you staff on an existing Studio course?'; + + it('should show the "contact administrator" card with no "add course" buttons', () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courses: [], + courseCreatorStatus: COURSE_CREATOR_STATES.pending, + }); + render(, { path: '/home' }); + const administratorCardTitle = screen.getByText(adminCardTitleText); + expect(administratorCardTitle).toBeVisible(); + expect(screen.queryByText('Create your first course')).not.toBeInTheDocument(); + }); + + it('should show contact administrator card with add course buttons', () => { + mockUseSelector.mockReturnValue({ + ...studioHomeMock, + courses: [], + courseCreatorStatus: COURSE_CREATOR_STATES.granted, + }); + render(, { path: '/home' }); + const administratorCardTitle = screen.getByText(adminCardTitleText); + expect(administratorCardTitle).toBeVisible(); + const addCourseButton = screen.getByTestId('contact-admin-create-course'); + expect(addCourseButton).toBeVisible(); + fireEvent.click(addCourseButton); + expect(screen.getByTestId('create-course-form')).toBeVisible(); + }); + }); + + it('should show footer', () => { + render(, { path: '/home' }); + expect(screen.getByText('Looking for help with Studio?')).toBeInTheDocument(); + expect(screen.getByText('LMS')).toHaveAttribute('href', process.env.LMS_BASE_URL); + }); + }); +}); diff --git a/src/studio-home/StudioHome.tsx b/src/studio-home/StudioHome.tsx index 57c6ad90d..32915bf25 100644 --- a/src/studio-home/StudioHome.tsx +++ b/src/studio-home/StudioHome.tsx @@ -102,7 +102,6 @@ const StudioHome = () => { iconBefore={AddIcon} size="sm" onClick={newLibraryClick} - data-testid="new-library-button" > {intl.formatMessage(messages.addNewLibraryBtnText)} , diff --git a/src/studio-home/__mocks__/index.js b/src/studio-home/__mocks__/index.js deleted file mode 100644 index 76d31279a..000000000 --- a/src/studio-home/__mocks__/index.js +++ /dev/null @@ -1 +0,0 @@ -export { default as studioHomeMock } from './studioHomeMock'; diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.ts similarity index 99% rename from src/studio-home/__mocks__/studioHomeMock.js rename to src/studio-home/__mocks__/studioHomeMock.ts index 25e210eee..28a70ab3a 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.ts @@ -1,4 +1,4 @@ -module.exports = { +export default { activeTab: 'courses', allowCourseReruns: true, allowedOrganizations: ['edx', 'org'], diff --git a/src/studio-home/card-item/CardItem.test.tsx b/src/studio-home/card-item/CardItem.test.tsx index 189b112f1..b033f789d 100644 --- a/src/studio-home/card-item/CardItem.test.tsx +++ b/src/studio-home/card-item/CardItem.test.tsx @@ -1,15 +1,15 @@ 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'; +} from '@src/testUtils'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; +import messages from '../messages'; +import { trimSlashes } from './utils'; import CardItem from '.'; jest.spyOn(reactRedux, 'useSelector').mockImplementation(() => studioHomeMock); diff --git a/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx b/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx index 96cc477bc..382ba7568 100644 --- a/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx +++ b/src/studio-home/collapsible-state-with-action/CollapsibleStateWithAction.test.jsx @@ -9,12 +9,12 @@ import { AppProvider } from '@edx/frontend-platform/react'; import MockAdapter from 'axios-mock-adapter'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { COURSE_CREATOR_STATES } from '../../constants'; import initializeStore from '../../store'; import { executeThunk } from '../../utils'; import { requestCourseCreatorQuery } from '../data/thunks'; import { getRequestCourseCreatorUrl } from '../data/api'; -import { studioHomeMock } from '../__mocks__'; import messages from './messages'; import CollapsibleStateWithAction from '.'; diff --git a/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx b/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx index 26105c39a..f9837f163 100644 --- a/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx +++ b/src/studio-home/create-new-course-form/CourseNewCourseForm.test.jsx @@ -5,7 +5,7 @@ import { initializeMockApp } from '@edx/frontend-platform'; import { IntlProvider } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; -import { studioHomeMock } from '../__mocks__'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import initializeStore from '../../store'; import messages from './messages'; import CourseNewCourseForm from '.'; diff --git a/src/studio-home/home-sidebar/HomeSidebar.test.jsx b/src/studio-home/home-sidebar/HomeSidebar.test.jsx index a6dca1281..a751a2b71 100644 --- a/src/studio-home/home-sidebar/HomeSidebar.test.jsx +++ b/src/studio-home/home-sidebar/HomeSidebar.test.jsx @@ -1,8 +1,8 @@ import { useSelector } from 'react-redux'; -import { initializeMocks, render } from '../../testUtils'; -import { COURSE_CREATOR_STATES } from '../../constants'; -import { studioHomeMock } from '../__mocks__'; +import { initializeMocks, render } from '@src/testUtils'; +import { COURSE_CREATOR_STATES } from '@src/constants'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import HomeSidebar from '.'; jest.mock('react-redux', () => ({ diff --git a/src/studio-home/processing-courses/ProcessingCourses.test.jsx b/src/studio-home/processing-courses/ProcessingCourses.test.jsx index c9f259568..cee84b160 100644 --- a/src/studio-home/processing-courses/ProcessingCourses.test.jsx +++ b/src/studio-home/processing-courses/ProcessingCourses.test.jsx @@ -5,8 +5,8 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { initializeMockApp } from '@edx/frontend-platform'; import { AppProvider } from '@edx/frontend-platform/react'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import initializeStore from '../../store'; -import { studioHomeMock } from '../__mocks__'; import messages from './messages'; import ProcessingCourses from '.'; diff --git a/src/studio-home/tabs-section/TabsSection.test.tsx b/src/studio-home/tabs-section/TabsSection.test.tsx index 0537af17a..67450dc7f 100644 --- a/src/studio-home/tabs-section/TabsSection.test.tsx +++ b/src/studio-home/tabs-section/TabsSection.test.tsx @@ -1,7 +1,7 @@ import { Routes, Route, useLocation } from 'react-router-dom'; import { getConfig, setConfig } from '@edx/frontend-platform'; -import { studioHomeMock } from '../__mocks__'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import messages from '../messages'; import tabMessages from './messages'; import TabsSection from '.'; diff --git a/src/studio-home/tabs-section/courses-tab/index.test.tsx b/src/studio-home/tabs-section/courses-tab/index.test.tsx index 4adb7663c..050c94c8a 100644 --- a/src/studio-home/tabs-section/courses-tab/index.test.tsx +++ b/src/studio-home/tabs-section/courses-tab/index.test.tsx @@ -3,9 +3,9 @@ import { initializeMocks, render, screen, -} from '../../../testUtils'; -import { COURSE_CREATOR_STATES } from '../../../constants'; -import { studioHomeMock } from '../../__mocks__'; +} from '@src/testUtils'; +import { COURSE_CREATOR_STATES } from '@src/constants'; +import studioHomeMock from '@src/studio-home/__mocks__/studioHomeMock'; import { initialState } from '../../factories/mockApiResponses'; import CoursesTab from '.';