diff --git a/src/CourseAuthoringPage.jsx b/src/CourseAuthoringPage.jsx index b11dcc7948..3e6b50f2e3 100644 --- a/src/CourseAuthoringPage.jsx +++ b/src/CourseAuthoringPage.jsx @@ -9,6 +9,7 @@ import { StudioFooter } from '@edx/frontend-component-footer'; import Header from './header'; import { fetchCourseDetail } from './data/thunks'; import { useModel } from './generic/model-store'; +import NotFoundAlert from './generic/NotFoundAlert'; import PermissionDeniedAlert from './generic/PermissionDeniedAlert'; import { getCourseAppsApiStatus } from './pages-and-resources/data/selectors'; import { RequestStatus } from './data/constants'; @@ -50,10 +51,16 @@ const CourseAuthoringPage = ({ courseId, children }) => { const courseOrg = courseDetail ? courseDetail.org : null; const courseTitle = courseDetail ? courseDetail.name : courseId; const courseAppsApiStatus = useSelector(getCourseAppsApiStatus); - const inProgress = useSelector(state => state.courseDetail.status) === RequestStatus.IN_PROGRESS; + const courseDetailStatus = useSelector(state => state.courseDetail.status); + const inProgress = courseDetailStatus === RequestStatus.IN_PROGRESS; const { pathname } = useLocation(); const showHeader = !pathname.includes('/editor'); + if (courseDetailStatus === RequestStatus.NOT_FOUND) { + return ( + + ); + } if (courseAppsApiStatus === RequestStatus.DENIED) { return ( diff --git a/src/CourseAuthoringPage.test.jsx b/src/CourseAuthoringPage.test.jsx index 3e982c5929..c7eeeb9be8 100644 --- a/src/CourseAuthoringPage.test.jsx +++ b/src/CourseAuthoringPage.test.jsx @@ -12,6 +12,7 @@ import CourseAuthoringPage from './CourseAuthoringPage'; import PagesAndResources from './pages-and-resources/PagesAndResources'; import { executeThunk } from './utils'; import { fetchCourseApps } from './pages-and-resources/data/thunks'; +import { fetchCourseDetail } from './data/thunks'; const courseId = 'course-v1:edX+TestX+Test_Course'; let mockPathname = '/evilguy/'; @@ -24,6 +25,19 @@ jest.mock('react-router-dom', () => ({ let axiosMock; let store; +beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + store = initializeStore(); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); +}); + describe('Editor Pages Load no header', () => { const mockStoreSuccess = async () => { const apiBaseUrl = getConfig().STUDIO_BASE_URL; @@ -33,18 +47,6 @@ describe('Editor Pages Load no header', () => { }); await executeThunk(fetchCourseApps(courseId), store.dispatch); }; - beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - store = initializeStore(); - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); - }); test('renders no loading wheel on editor pages', async () => { mockPathname = '/editor/'; await mockStoreSuccess(); @@ -76,3 +78,56 @@ describe('Editor Pages Load no header', () => { expect(wrapper.queryByRole('status')).toBeInTheDocument(); }); }); + +describe('Course authoring page', () => { + const lmsApiBaseUrl = getConfig().LMS_BASE_URL; + const courseDetailApiUrl = `${lmsApiBaseUrl}/api/courses/v1/courses`; + const mockStoreNotFound = async () => { + axiosMock.onGet( + `${courseDetailApiUrl}/${courseId}?username=abc123`, + ).reply(404, { + response: { status: 404 }, + }); + await executeThunk(fetchCourseDetail(courseId), store.dispatch); + }; + const mockStoreError = async () => { + axiosMock.onGet( + `${courseDetailApiUrl}/${courseId}?username=abc123`, + ).reply(500, { + response: { status: 500 }, + }); + await executeThunk(fetchCourseDetail(courseId), store.dispatch); + }; + test('renders not found page on non-existent course key', async () => { + await mockStoreNotFound(); + const wrapper = render( + + + + + + , + ); + expect(await wrapper.findByTestId('notFoundAlert')).toBeInTheDocument(); + }); + test('does not render not found page on other kinds of error', async () => { + await mockStoreError(); + // Currently, loading errors are not handled, so we wait for the child + // content to be rendered -which happens when request status is no longer + // IN_PROGRESS but also not NOT_FOUND or DENIED- then check that the not + // found alert is not present. + const contentTestId = 'courseAuthoringPageContent'; + const wrapper = render( + + + +
+ + + + , + ); + expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument(); + expect(wrapper.queryByTestId('notFoundAlert')).not.toBeInTheDocument(); + }); +}); diff --git a/src/data/constants.js b/src/data/constants.js index 5191ea1dfa..bd01f09dda 100644 --- a/src/data/constants.js +++ b/src/data/constants.js @@ -13,6 +13,7 @@ export const RequestStatus = { PENDING: 'pending', CLEAR: 'clear', PARTIAL: 'partial', + NOT_FOUND: 'not-found', }; /** diff --git a/src/data/thunks.js b/src/data/thunks.js index 9a52d4d89d..9c797dc6a5 100644 --- a/src/data/thunks.js +++ b/src/data/thunks.js @@ -21,7 +21,11 @@ export function fetchCourseDetail(courseId) { canChangeProviders: getAuthenticatedUser().administrator || new Date(courseDetail.start) > new Date(), })); } catch (error) { - dispatch(updateStatus({ courseId, status: RequestStatus.FAILED })); + if (error.response && error.response.status === 404) { + dispatch(updateStatus({ courseId, status: RequestStatus.NOT_FOUND })); + } else { + dispatch(updateStatus({ courseId, status: RequestStatus.FAILED })); + } } }; } diff --git a/src/generic/NotFoundAlert.jsx b/src/generic/NotFoundAlert.jsx new file mode 100644 index 0000000000..8ff9cf4fff --- /dev/null +++ b/src/generic/NotFoundAlert.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { Alert } from '@edx/paragon'; + +const NotFoundAlert = () => ( + + + +); + +export default NotFoundAlert;