@@ -209,35 +211,39 @@ const VideosPage = ({
) : null}
- {isVideoTranscriptEnabled ? (
-
- ) : null}
-
+ {loadingStatus !== RequestStatus.FAILED && (
+ <>
+ {isVideoTranscriptEnabled && (
+
+ )}
+
+ >
+ )}
);
diff --git a/src/files-and-videos/videos-page/VideosPage.test.jsx b/src/files-and-videos/videos-page/VideosPage.test.jsx
index 05e7f32fa..0168b1a4a 100644
--- a/src/files-and-videos/videos-page/VideosPage.test.jsx
+++ b/src/files-and-videos/videos-page/VideosPage.test.jsx
@@ -60,12 +60,14 @@ const mockStore = async (
) => {
const fetchVideosUrl = getVideosUrl(courseId);
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateFetchVideosApiResponse());
+ renderComponent();
await executeThunk(fetchVideos(courseId), store.dispatch);
};
const emptyMockStore = async (status) => {
const fetchVideosUrl = getVideosUrl(courseId);
axiosMock.onGet(fetchVideosUrl).reply(getStatusValue(status), generateEmptyApiResponse());
+ renderComponent();
await executeThunk(fetchVideos(courseId), store.dispatch);
};
@@ -93,25 +95,21 @@ describe('FilesAndUploads', () => {
});
it('should return placeholder component', async () => {
- renderComponent();
await mockStore(RequestStatus.DENIED);
expect(screen.getByTestId('under-construction-placeholder')).toBeVisible();
});
it('should not render transcript settings button', async () => {
- renderComponent();
await emptyMockStore(RequestStatus.SUCCESSFUL);
expect(screen.queryByText(videoMessages.transcriptSettingsButtonLabel.defaultMessage));
});
it('should have Video uploads title', async () => {
- renderComponent();
await emptyMockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByText(videoMessages.heading.defaultMessage)).toBeVisible();
});
it('should render dropzone', async () => {
- renderComponent();
await emptyMockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('files-dropzone')).toBeVisible();
@@ -119,7 +117,6 @@ describe('FilesAndUploads', () => {
});
it('should upload a single file', async () => {
- renderComponent();
await emptyMockStore(RequestStatus.SUCCESSFUL);
const dropzone = screen.getByTestId('files-dropzone');
await act(async () => {
@@ -162,7 +159,6 @@ describe('FilesAndUploads', () => {
describe('table view', () => {
it('should render transcript settings button', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const transcriptSettingsButton = screen.getByText(videoMessages.transcriptSettingsButtonLabel.defaultMessage);
expect(transcriptSettingsButton).toBeVisible();
@@ -175,7 +171,6 @@ describe('FilesAndUploads', () => {
});
it('should render table with gallery card', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('files-data-table')).toBeVisible();
@@ -183,7 +178,6 @@ describe('FilesAndUploads', () => {
});
it('should switch table to list view', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
expect(screen.getByTestId('files-data-table')).toBeVisible();
@@ -201,7 +195,6 @@ describe('FilesAndUploads', () => {
});
it('should update video thumbnail', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
axiosMock.onPost(`${getApiBaseUrl()}/video_images/${courseId}/mOckID1`).reply(200, { image_url: 'url' });
const addThumbnailButton = screen.getByTestId('video-thumbnail-mOckID1');
@@ -217,7 +210,6 @@ describe('FilesAndUploads', () => {
describe('table actions', () => {
it('should upload a single file', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const mockResponseData = { status: '200', ok: true, blob: () => 'Data' };
const mockFetchResponse = Promise.resolve(mockResponseData);
@@ -236,10 +228,8 @@ describe('FilesAndUploads', () => {
});
it('should have disabled action buttons', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
- expect(actionsButton).toBeVisible();
await waitFor(() => {
fireEvent.click(actionsButton);
@@ -250,12 +240,10 @@ describe('FilesAndUploads', () => {
});
it('delete button should be enabled and delete selected file', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const selectCardButton = screen.getAllByTestId('datatable-select-column-checkbox-cell')[0];
fireEvent.click(selectCardButton);
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
- expect(actionsButton).toBeVisible();
await waitFor(() => {
fireEvent.click(actionsButton);
@@ -289,12 +277,10 @@ describe('FilesAndUploads', () => {
});
it('download button should be enabled and download single selected file', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const selectCardButton = screen.getAllByTestId('datatable-select-column-checkbox-cell')[0];
fireEvent.click(selectCardButton);
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
- expect(actionsButton).toBeVisible();
await waitFor(() => {
fireEvent.click(actionsButton);
@@ -311,13 +297,11 @@ describe('FilesAndUploads', () => {
});
it('download button should be enabled and download multiple selected files', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const selectCardButtons = screen.getAllByTestId('datatable-select-column-checkbox-cell');
fireEvent.click(selectCardButtons[0]);
fireEvent.click(selectCardButtons[1]);
const actionsButton = screen.getByText(messages.actionsButtonLabel.defaultMessage);
- expect(actionsButton).toBeVisible();
await waitFor(() => {
fireEvent.click(actionsButton);
@@ -337,7 +321,6 @@ describe('FilesAndUploads', () => {
describe('Sort and filter button', () => {
beforeEach(async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const sortAndFilterButton = screen.getByText(messages.sortButtonLabel.defaultMessage);
@@ -431,12 +414,9 @@ describe('FilesAndUploads', () => {
describe('card menu actions', () => {
describe('Info', () => {
it('should open video info', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- expect(videoMenuButton).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`)
.reply(201, {
@@ -460,11 +440,8 @@ describe('FilesAndUploads', () => {
});
it('should open video info modal and show info tab', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- expect(videoMenuButton).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
@@ -481,11 +458,8 @@ describe('FilesAndUploads', () => {
});
it('should open video info modal and show transcript tab', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- expect(videoMenuButton).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID1/usage`).reply(201, { usageLocations: [] });
await waitFor(() => {
@@ -505,7 +479,6 @@ describe('FilesAndUploads', () => {
});
it('should show transcript error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
@@ -525,12 +498,9 @@ describe('FilesAndUploads', () => {
});
it('download button should download file', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- expect(videoMenuButton).toBeVisible();
await waitFor(() => {
fireEvent.click(within(videoMenuButton).getByLabelText('file-menu-toggle'));
@@ -543,12 +513,9 @@ describe('FilesAndUploads', () => {
});
it('delete button should delete file', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const fileMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- expect(fileMenuButton).toBeVisible();
await waitFor(() => {
axiosMock.onDelete(`${getCourseVideosApiUrl(courseId)}/mOckID1`).reply(204);
@@ -569,11 +536,20 @@ describe('FilesAndUploads', () => {
});
describe('api errors', () => {
+ it('404 intitial fetch should show error', async () => {
+ await mockStore(RequestStatus.FAILED);
+
+ const { loadingStatus } = store.getState().videos;
+ expect(loadingStatus).toEqual(RequestStatus.FAILED);
+
+ expect(screen.getByText('Error')).toBeVisible();
+ });
+
it('invalid file size should show error', async () => {
const errorMessage = 'File download.png exceeds maximum size of 5 GB.';
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
axiosMock.onPost(getCourseVideosApiUrl(courseId)).reply(413, { error: errorMessage });
+
const addFilesButton = screen.getAllByLabelText('file-input')[3];
await act(async () => {
userEvent.upload(addFilesButton, file);
@@ -586,9 +562,9 @@ describe('FilesAndUploads', () => {
});
it('404 add file should show error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
axiosMock.onPost(getCourseVideosApiUrl(courseId)).reply(404);
+
const addFilesButton = screen.getAllByLabelText('file-input')[3];
await act(async () => {
userEvent.upload(addFilesButton, file);
@@ -601,9 +577,9 @@ describe('FilesAndUploads', () => {
});
it('404 add thumbnail should show error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
axiosMock.onPost(`${getApiBaseUrl()}/video_images/${courseId}/mOckID1`).reply(404);
+
const addThumbnailButton = screen.getByTestId('video-thumbnail-mOckID1');
const thumbnail = new File(['test'], 'sOMEUrl.jpg', { type: 'image/jpg' });
await act(async () => {
@@ -617,7 +593,6 @@ describe('FilesAndUploads', () => {
});
it('404 upload file to server should show error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
const mockResponseData = { status: '404', ok: false, blob: () => 'Data' };
const mockFetchResponse = Promise.reject(mockResponseData);
@@ -637,12 +612,9 @@ describe('FilesAndUploads', () => {
});
it('404 delete should show error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID1')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID1');
- expect(videoMenuButton).toBeVisible();
await waitFor(() => {
axiosMock.onDelete(`${getCourseVideosApiUrl(courseId)}/mOckID1`).reply(404);
@@ -664,12 +636,9 @@ describe('FilesAndUploads', () => {
});
it('404 usage path fetch should show error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
- expect(screen.getByTestId('grid-card-mOckID3')).toBeVisible();
const videoMenuButton = screen.getByTestId('file-menu-dropdown-mOckID3');
- expect(videoMenuButton).toBeVisible();
axiosMock.onGet(`${getVideosUrl(courseId)}/mOckID3/usage`).reply(404);
await waitFor(() => {
@@ -685,8 +654,8 @@ describe('FilesAndUploads', () => {
});
it('multiple video files fetch failure should show error', async () => {
- renderComponent();
await mockStore(RequestStatus.SUCCESSFUL);
+
const selectCardButtons = screen.getAllByTestId('datatable-select-column-checkbox-cell');
fireEvent.click(selectCardButtons[0]);
fireEvent.click(selectCardButtons[2]);
diff --git a/src/files-and-videos/videos-page/data/slice.js b/src/files-and-videos/videos-page/data/slice.js
index 6a6410fcd..74574660f 100644
--- a/src/files-and-videos/videos-page/data/slice.js
+++ b/src/files-and-videos/videos-page/data/slice.js
@@ -21,6 +21,7 @@ const slice = createSlice({
download: [],
usageMetrics: [],
transcript: [],
+ loading: '',
},
},
reducers: {
@@ -76,8 +77,12 @@ const slice = createSlice({
},
updateErrors: (state, { payload }) => {
const { error, message } = payload;
- const currentErrorState = state.errors[error];
- state.errors[error] = [...currentErrorState, message];
+ if (error === 'loading') {
+ state.errors.loading = message;
+ } else {
+ const currentErrorState = state.errors[error];
+ state.errors[error] = [...currentErrorState, message];
+ }
},
clearErrors: (state, { payload }) => {
const { error } = payload;
diff --git a/src/files-and-videos/videos-page/data/thunks.js b/src/files-and-videos/videos-page/data/thunks.js
index df0bd214f..245ffd0bb 100644
--- a/src/files-and-videos/videos-page/data/thunks.js
+++ b/src/files-and-videos/videos-page/data/thunks.js
@@ -55,6 +55,7 @@ export function fetchVideos(courseId) {
if (error.response && error.response.status === 403) {
dispatch(updateLoadingStatus({ status: RequestStatus.DENIED }));
} else {
+ dispatch(updateErrors({ error: 'loading', message: 'Failed to load videos' }));
dispatch(updateLoadingStatus({ courseId, status: RequestStatus.FAILED }));
}
}
diff --git a/src/files-and-videos/videos-page/factories/mockApiResponses.jsx b/src/files-and-videos/videos-page/factories/mockApiResponses.jsx
index 3aa621e9d..c20aa9013 100644
--- a/src/files-and-videos/videos-page/factories/mockApiResponses.jsx
+++ b/src/files-and-videos/videos-page/factories/mockApiResponses.jsx
@@ -69,7 +69,7 @@ export const initialState = {
},
transcriptCredentials: { cielo24: false, '3PlayMedia': false },
},
- loadingStatus: RequestStatus.SUCCESSFUL,
+ loadingStatus: RequestStatus.IN_PROGRESS,
updatingStatus: '',
addingStatus: '',
deletingStatus: '',
@@ -82,6 +82,7 @@ export const initialState = {
download: [],
usageMetrics: [],
transcript: [],
+ loading: '',
},
},
models: {
@@ -225,9 +226,77 @@ export const generateAddVideoApiResponse = () => ({
],
});
-export const generateEmptyApiResponse = () => ([{
+export const generateEmptyApiResponse = () => ({
previousUploads: [],
-}]);
+ image_upload_url: '/video_images/course',
+ video_handler_url: '/videos/course',
+ encodings_download_url: '/video_encodings_download/course',
+ default_video_image_url: '/static/studio/images/video-images/default_video_image.png',
+ concurrent_upload_limit: 4,
+ video_supported_file_formats: ['.mp4', '.mov'],
+ video_upload_max_file_size: '5',
+ video_image_settings: {
+ video_image_upload_enabled: true,
+ max_size: 2097152,
+ min_size: 2048,
+ max_width: 1280,
+ max_height: 720,
+ supported_file_formats: {
+ '.bmp': 'image/bmp',
+ '.bmp2': 'image/x-ms-bmp',
+ '.gif': 'image/gif',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.png': 'image/png',
+ },
+ },
+ is_video_transcript_enabled: false,
+ active_transcript_preferences: null,
+ transcript_credentials: {},
+ transcript_available_languages: [{ language_code: 'ab', language_text: 'Abkhazian' }],
+ video_transcript_settings: {
+ transcript_download_handler_url: '/transcript_download/',
+ transcript_upload_handler_url: '/transcript_upload/',
+ transcript_delete_handler_url: '/transcript_delete/course',
+ trancript_download_file_format: 'srt',
+ transcript_preferences_handler_url: '/transcript_preferences/course',
+ transcript_credentials_handler_url: '/transcript_credentials/course',
+ transcription_plans: {
+ Cielo24: {
+ display_name: 'Cielo24',
+ turnaround: { PRIORITY: 'Priority (24 hours)', STANDARD: 'Standard (48 hours)' },
+ fidelity: {
+ MECHANICAL: {
+ display_name: 'Mechanical (75% accuracy)',
+ languages: { nl: 'Dutch', en: 'English', fr: 'French' },
+ },
+ PREMIUM: { display_name: 'Premium (95% accuracy)', languages: { en: 'English' } },
+ PROFESSIONAL: {
+ display_name: 'Professional (99% accuracy)',
+ languages: { ar: 'Arabic', 'zh-tw': 'Chinese - Mandarin (Traditional)' },
+ },
+ },
+ },
+ '3PlayMedia': {
+ display_name: '3Play Media',
+ turnaround: {
+ two_hour: '2 hours',
+ same_day: 'Same day',
+ rush: '24 hours (rush)',
+ expedited: '2 days (expedited)',
+ standard: '4 days (standard)',
+ extended: '10 days (extended)',
+ },
+ languages: { en: 'English', el: 'Greek', zh: 'Chinese' },
+ translations: {
+ es: ['en'],
+ en: ['el', 'en', 'zh'],
+ },
+ },
+ },
+ },
+ pagination_context: {},
+});
export const generateNewVideoApiResponse = () => ({
files: [{
@@ -240,6 +309,8 @@ export const getStatusValue = (status) => {
switch (status) {
case RequestStatus.DENIED:
return 403;
+ case RequestStatus.FAILED:
+ return 404;
default:
return 200;
}