/* * Copyright (C) 2019 - present Instructure, Inc. * * This file is part of Canvas. * * Canvas is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, version 3 of the License. * * Canvas is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License along * with this program. If not, see . */ import {fireEvent, render, screen, waitFor} from '@testing-library/react' import React from 'react' import {vi} from 'vitest' import saveMediaRecording, {saveClosedCaptionsForAttachment} from '../saveMediaRecording' import UploadMedia from '../UploadMedia' vi.mock('../saveMediaRecording', () => ({ default: vi.fn(), saveClosedCaptionsForAttachment: vi.fn(), })) vi.mock('@instructure/studio-player', () => ({ StudioPlayer: vi.fn(() =>
), })) vi.mock('../MediaRecorder', () => ({ default: ({onSave}: {onSave: (file: File) => void}) => ( ), })) const uploadMediaTranslations = { UploadMediaStrings: { ADD_CLOSED_CAPTIONS_OR_SUBTITLES: 'Add CC/Subtitles', CLEAR_FILE_TEXT: 'Remove', CLOSE_TEXT: 'Close', CLOSED_CAPTIONS_CHOOSE_FILE: 'Choose caption file', CLOSED_CAPTIONS_SELECT_LANGUAGE: 'Select Language', COMPUTER_PANEL_TITLE: 'Computer', DRAG_DROP_CLICK_TO_BROWSE: 'Drag and drop, or click to browse your computer', DRAG_FILE_TEXT: 'Drag a file here', INVALID_FILE_TEXT: 'Invalid File', LOADING_MEDIA: 'Loading...', MEDIA_RECORD_NOT_AVAILABLE: 'Recording is not available', PROGRESS_LABEL: 'Uploading', RECORD_PANEL_TITLE: 'Record', SUBMIT_TEXT: 'Upload', UPLOADING_ERROR: 'Upload Error', UPLOAD_MEDIA_LABEL: 'Upload Media', SELECT_SUPPORTED_FILE_TYPE: 'Please select a file of a supported type', CHOOSE_FILE_TO_UPLOAD: 'Please choose a file', ENTER_FILE_NAME: 'Please enter a file name', NO_PREVIEW_AVAILABLE: 'No preview is available for this file.', FILE_NAME: 'File name', }, MediaCaptureStrings: { ARIA_VIDEO_LABEL: 'Video Player', }, SelectStrings: { USE_ARROWS: 'Use arrow keys to navigate options.', LIST_COLLAPSED: 'List collapsed.', LIST_EXPANDED: 'List expanded.', OPTION_SELECTED: '{option} selected.', }, } interface UploadMediaProps { open: boolean tabs: {record: boolean; upload: boolean} onDismiss: () => void onStartUpload?: (file: File) => void onUploadComplete?: (err: unknown, data: unknown, captions?: unknown) => void computerFile?: File | null disableSubmitWhileUploading?: boolean rcsConfig?: object } function renderModal(overrideProps: Partial = {}) { return render( {}} uploadMediaTranslations={uploadMediaTranslations} rcsConfig={{contextId: '1', contextType: 'course'}} {...overrideProps} />, ) } function waitForFileDrop() { return screen.findByLabelText(/Drag a file here/, {selector: 'input'}) } function waitForFilePreview() { return screen.findByPlaceholderText('File name') } const uploadedFile = () => vi.mocked(saveMediaRecording).mock.lastCall?.[0] as File describe('UploadMediaModal', () => { beforeEach(() => { vi.clearAllMocks() }) describe('panel selection', () => { it('selects the computer panel by default when both tabs are enabled', async () => { renderModal({tabs: {record: true, upload: true}}) expect(await waitForFileDrop()).toBeInTheDocument() expect(screen.getByRole('tab', {name: 'Computer'})).toHaveAttribute('aria-selected', 'true') expect(screen.queryByText('Save recording')).not.toBeInTheDocument() }) it('renders only the computer panel when the record tab is disabled', async () => { renderModal({tabs: {record: false, upload: true}}) expect(await waitForFileDrop()).toBeInTheDocument() expect(screen.queryByRole('tab', {name: 'Record'})).not.toBeInTheDocument() expect(screen.queryByText('Save recording')).not.toBeInTheDocument() }) it('renders only the record panel when the upload tab is disabled', async () => { renderModal({tabs: {record: true, upload: false}}) expect(await screen.findByText('Save recording')).toBeInTheDocument() expect(screen.queryByRole('tab', {name: 'Computer'})).not.toBeInTheDocument() expect( screen.queryByLabelText(/Drag a file here/, {selector: 'input'}), ).not.toBeInTheDocument() }) it('renders no tabs or panels while closed', () => { renderModal({open: false, tabs: {record: true, upload: true}}) expect(screen.queryByRole('tab')).not.toBeInTheDocument() expect(screen.queryByText('Save recording')).not.toBeInTheDocument() expect( screen.queryByLabelText(/Drag a file here/, {selector: 'input'}), ).not.toBeInTheDocument() }) }) describe('submitting from the computer panel', () => { it('does not upload when no file has been chosen', async () => { renderModal() await waitForFileDrop() fireEvent.click(screen.getByText('Upload')) expect(await screen.findByText('Please choose a file')).toBeVisible() expect(saveMediaRecording).not.toHaveBeenCalled() }) it('uploads the chosen file', async () => { const aFile = new File(['foo'], 'foo.mp4', {type: 'video/mp4'}) const onStartUpload = vi.fn() renderModal({computerFile: aFile, onStartUpload}) await waitForFilePreview() fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(uploadedFile().name).toBe('foo.mp4') expect(onStartUpload).toHaveBeenCalledWith(uploadedFile()) }) it('appends the extension for the file type when the name has none', async () => { const aFile = new File(['foo'], 'recording', {type: 'video/quicktime'}) renderModal({computerFile: aFile}) await waitForFilePreview() fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(uploadedFile().name).toBe('recording.mov') }) it('falls back to webm when the file type maps to no extension', async () => { const aFile = new File(['foo'], 'recording', {type: 'video/nonsense'}) renderModal({computerFile: aFile}) await waitForFilePreview() fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(uploadedFile().name).toBe('recording.webm') }) it('appends the extension to the user-entered title as well', async () => { const aFile = new File(['foo'], 'recording', {type: 'video/mp4'}) renderModal({computerFile: aFile}) await waitForFilePreview() fireEvent.change(screen.getByPlaceholderText('File name'), { target: {value: 'My holiday video'}, }) fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(uploadedFile().userEnteredTitle).toBe('My holiday video.mp4') }) it('leaves a user-entered title that already has an extension alone', async () => { const aFile = new File(['foo'], 'foo.mp4', {type: 'video/mp4'}) renderModal({computerFile: aFile}) await waitForFilePreview() fireEvent.change(screen.getByPlaceholderText('File name'), {target: {value: 'holiday.mp4'}}) fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(uploadedFile().userEnteredTitle).toBe('holiday.mp4') }) }) describe('upload completion', () => { const mediaObject = {mediaObject: {media_object: {attachment_id: '7'}}} async function submitAndCapture(overrideProps: Partial = {}) { const aFile = new File(['foo'], 'foo.mp4', {type: 'video/mp4'}) renderModal({computerFile: aFile, ...overrideProps}) await waitForFilePreview() fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) return vi.mocked(saveMediaRecording).mock.lastCall?.[2] as ( err: unknown, data: unknown, ) => Promise } it('reports the failure and dismisses when the upload errors', async () => { const onUploadComplete = vi.fn() const onDismiss = vi.fn() const uploadFinished = await submitAndCapture({onUploadComplete, onDismiss}) const err = new Error('kaboom') await uploadFinished(err, null) expect(onUploadComplete).toHaveBeenCalledWith(err, null) expect(onDismiss).toHaveBeenCalled() }) it('reports the media object and dismisses when the upload succeeds', async () => { const onUploadComplete = vi.fn() const onDismiss = vi.fn() const uploadFinished = await submitAndCapture({onUploadComplete, onDismiss}) await uploadFinished(null, mediaObject) expect(onUploadComplete).toHaveBeenCalledWith(null, mediaObject, undefined) expect(onDismiss).toHaveBeenCalled() expect(saveClosedCaptionsForAttachment).not.toHaveBeenCalled() }) it('reports a malformed response through onUploadComplete', async () => { const onUploadComplete = vi.fn() const uploadFinished = await submitAndCapture({onUploadComplete}) await uploadFinished(null, {mediaObject: null}) expect(onUploadComplete).toHaveBeenCalledWith(expect.any(Error), null) }) }) describe('footer', () => { it('dismisses when close is clicked', async () => { const onDismiss = vi.fn() renderModal({onDismiss}) await waitForFileDrop() const [, footerClose] = screen.getAllByRole('button', {name: 'Close'}) fireEvent.click(footerClose) expect(onDismiss).toHaveBeenCalled() }) it('disables the submit button while uploading when asked to', async () => { const aFile = new File(['foo'], 'foo.mp4', {type: 'video/mp4'}) renderModal({computerFile: aFile, disableSubmitWhileUploading: true}) await waitForFilePreview() const submit = screen.getByText('Upload').closest('button') as HTMLButtonElement expect(submit).not.toBeDisabled() fireEvent.click(submit) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(screen.getByText('Upload').closest('button')).toBeDisabled() }) it('leaves the submit button enabled while uploading by default', async () => { const aFile = new File(['foo'], 'foo.mp4', {type: 'video/mp4'}) renderModal({computerFile: aFile}) await waitForFilePreview() fireEvent.click(screen.getByText('Upload').closest('button') as HTMLButtonElement) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(screen.getByText('Upload').closest('button')).not.toBeDisabled() }) it('shows upload progress', async () => { const aFile = new File(['foo'], 'foo.mp4', {type: 'video/mp4'}) renderModal({computerFile: aFile}) await waitForFilePreview() fireEvent.click(screen.getByText('Upload')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) const onProgress = vi.mocked(saveMediaRecording).mock.lastCall?.[3] as (n: number) => void onProgress(42) expect(await screen.findByText('42%')).toBeInTheDocument() }) }) describe('submitting from the record panel', () => { it('uploads the recording as soon as it is saved', async () => { renderModal({tabs: {record: true, upload: false}}) fireEvent.click(await screen.findByText('Save recording')) await waitFor(() => expect(saveMediaRecording).toHaveBeenCalled()) expect(uploadedFile().name).toBe('recording.mp4') }) }) })