Skip to content

Commit

Permalink
feat: error page on invalid course key
Browse files Browse the repository at this point in the history
  • Loading branch information
ArturGaspar committed Dec 21, 2023
1 parent 782fadd commit b0064d5
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 14 deletions.
9 changes: 8 additions & 1 deletion src/CourseAuthoringPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 (
<NotFoundAlert />
);
}
if (courseAppsApiStatus === RequestStatus.DENIED) {
return (
<PermissionDeniedAlert />
Expand Down
78 changes: 66 additions & 12 deletions src/CourseAuthoringPage.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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/';
Expand All @@ -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;
Expand All @@ -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();
Expand Down Expand Up @@ -76,3 +78,55 @@ 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(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId} />
</IntlProvider>
</AppProvider>
,
);
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.
const contentTestId = 'courseAuthoringPageContent';
const wrapper = render(
<AppProvider store={store}>
<IntlProvider locale="en">
<CourseAuthoringPage courseId={courseId}>
<div data-testid={contentTestId} />
</CourseAuthoringPage>
</IntlProvider>
</AppProvider>
,
);
expect(await wrapper.findByTestId(contentTestId)).toBeInTheDocument();
expect(wrapper.queryByTestId('notFoundAlert')).not.toBeInTheDocument();
});
});
1 change: 1 addition & 0 deletions src/data/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const RequestStatus = {
PENDING: 'pending',
CLEAR: 'clear',
PARTIAL: 'partial',
NOT_FOUND: 'not-found',
};

/**
Expand Down
6 changes: 5 additions & 1 deletion src/data/thunks.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
}
};
}
14 changes: 14 additions & 0 deletions src/generic/NotFoundAlert.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert } from '@edx/paragon';

const NotFoundAlert = () => (
<Alert variant="danger" data-testid="notFoundAlert">
<FormattedMessage
id="authoring.alert.error.notfound"
defaultMessage="Not found."
/>
</Alert>
);

export default NotFoundAlert;

0 comments on commit b0064d5

Please sign in to comment.