/*
* 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 {StudioPlayer} from '@instructure/studio-player'
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react'
import React from 'react'
import {vi} from 'vitest'
import {ACCEPTED_FILE_TYPES} from '../acceptedMediaFileTypes'
import ComputerPanel from '../ComputerPanel'
vi.mock('@instructure/studio-player', () => ({
StudioPlayer: vi.fn(() =>
),
}))
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...',
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',
},
SelectStrings: {
USE_ARROWS: 'Use arrow keys to navigate options.',
LIST_COLLAPSED: 'List collapsed.',
LIST_EXPANDED: 'List expanded.',
OPTION_SELECTED: '{option} selected.',
},
}
const LIVE_REGION_ID = 'flash_screenreader_holder'
interface ComputerPanelProps {
theFile: File | null
setFile: (file: File | null) => void
hasUploadedFile: boolean
setHasUploadedFile: (uploaded: boolean) => void
label: string
uploadMediaTranslations: typeof uploadMediaTranslations
accept: string[]
userLocale: string
liveRegion: () => HTMLElement | null
updateSubtitles: () => boolean
}
function createPanel(overrideProps: Partial, ref?: React.Ref) {
return (
{}}
hasUploadedFile={false}
setHasUploadedFile={() => {}}
label="Upload File"
uploadMediaTranslations={uploadMediaTranslations}
accept={ACCEPTED_FILE_TYPES}
userLocale="en"
liveRegion={() => document.getElementById(LIVE_REGION_ID)}
updateSubtitles={() => false}
{...overrideProps}
/>
)
}
function renderPanel(overrideProps = {}) {
return render(createPanel(overrideProps))
}
function selectFile(theFile: File, overrideProps: Partial = {}) {
const result = render(createPanel(overrideProps))
const dropZone = result.getByLabelText(/Upload File/, {selector: 'input'})
fireEvent.change(dropZone, {target: {files: [theFile]}})
result.rerender(createPanel({...overrideProps, theFile, hasUploadedFile: true}))
return result
}
describe('UploadMedia: ComputerPanel', () => {
beforeEach(() => {
const liveRegion = document.createElement('div')
liveRegion.id = LIVE_REGION_ID
liveRegion.setAttribute('role', 'alert')
document.body.appendChild(liveRegion)
window.matchMedia =
window.matchMedia ||
(() => ({
matches: false,
addListener: () => {},
removeListener: () => {},
}))
})
afterEach(() => {
const liveRegion = document.getElementById(LIVE_REGION_ID)
if (liveRegion) {
document.body.removeChild(liveRegion)
}
})
it('shows an editable text input for title if a valid file is added', async () => {
const aFile = new File(['foo'], 'foo.mov', {
type: 'video/quicktime',
})
const setFile = (file: File | null) =>
rerender(
createPanel({
setFile,
theFile: file,
hasUploadedFile: true,
}),
)
const {rerender, getByLabelText, getByPlaceholderText} = renderPanel({setFile})
const dropZone = getByLabelText(/Upload File/, {selector: 'input'})
fireEvent.change(dropZone, {
target: {
files: [aFile],
},
})
const titleInput = getByPlaceholderText('File name')
if (!(titleInput instanceof HTMLInputElement)) {
throw new Error('Expected titleInput to be an HTMLInputElement')
}
expect(titleInput.value).toEqual('foo.mov')
fireEvent.change(titleInput, {target: {value: 'Awesome video'}})
expect(titleInput.value).toEqual('Awesome video')
})
describe('validation', () => {
describe('file', () => {
it('shows an error if not defined', () => {
const ref = React.createRef<{updateValidationMessages: () => void}>()
const {getByText} = render(createPanel({}, ref))
act(() => ref.current?.updateValidationMessages())
expect(getByText('Please choose a file')).toBeVisible()
})
it('shows an error if rejected', () => {
const notAMediaFile = new File(['foo'], 'foo.txt', {type: 'text/plain'})
const {getByLabelText, getByText} = renderPanel()
const dropZone = getByLabelText(/Upload File/, {selector: 'input'})
fireEvent.change(dropZone, {
target: {
files: [notAMediaFile],
},
})
expect(getByText('Please select a file of a supported type')).toBeVisible()
})
})
describe('file name', () => {
it('shows an error if blank', () => {
const ref = React.createRef<{updateValidationMessages: () => void}>()
const aFile = new File(['foo'], 'foo.mov', {
type: 'video/quicktime',
})
const {getByPlaceholderText, getByText} = render(
createPanel(
{
theFile: aFile,
hasUploadedFile: true,
},
ref,
),
)
const titleInput = getByPlaceholderText('File name')
fireEvent.change(titleInput, {target: {value: ''}})
act(() => ref.current?.updateValidationMessages())
expect(getByText('Please enter a file name')).toBeVisible()
})
})
})
describe('file preview', () => {
afterEach(() => {
vi.clearAllMocks()
})
it('Renders a video player preview if afile type is a video', async () => {
const aFile = new File(['foo'], 'foo.mp4', {
type: 'video/mp4',
})
const {getByTestId} = selectFile(aFile)
await waitFor(() => getByTestId('studio-player'))
expect(vi.mocked(StudioPlayer).mock.lastCall?.[0]).toMatchObject({
src: {src: aFile, type: 'video/object'},
})
})
it('Renders the StudioPlayer once a video is selected', async () => {
const aFile = new File(['foo'], 'foo.mp4', {
type: 'video/mp4',
})
const setFile = vi.fn()
const setHasUploadedFile = vi.fn()
const {getByLabelText, getByTestId, queryByTestId, rerender} = renderPanel({
setFile,
setHasUploadedFile,
})
// Verify StudioPlayer is not rendered before file upload
expect(queryByTestId('studio-player')).not.toBeInTheDocument()
// Trigger the file upload
const dropZone = getByLabelText(/Upload File/, {selector: 'input'})
fireEvent.change(dropZone, {
target: {
files: [aFile],
},
})
rerender(
createPanel({
setFile,
setHasUploadedFile,
theFile: aFile,
hasUploadedFile: true,
}),
)
await waitFor(() => expect(queryByTestId('studio-player')).toBeInTheDocument())
expect(setFile).toHaveBeenCalledWith(aFile)
expect(setHasUploadedFile).toHaveBeenCalledWith(true)
})
// NON_PREVIEWABLE_TYPES won't load in the player via a blob url.
it('Renders a video icon instead of a player for video/avi', async () => {
const aFile = new File(['foo'], 'foo.avi', {type: 'video/avi'})
const {getByTestId, getByText, queryByTestId} = selectFile(aFile)
const icon = await waitFor(() => getByTestId('preview-video-icon'))
expect(icon).toBeInTheDocument()
expect(getByText('No preview is available for this file.')).toBeInTheDocument()
expect(queryByTestId('studio-player')).not.toBeInTheDocument()
})
it('Renders a video icon instead of a player for video/x-ms-wma', async () => {
const aFile = new File(['foo'], 'foo.wma', {type: 'video/x-ms-wma'})
const {getByTestId, getByText, queryByTestId} = selectFile(aFile)
const icon = await waitFor(() => getByTestId('preview-video-icon'))
expect(icon).toBeInTheDocument()
expect(getByText('No preview is available for this file.')).toBeInTheDocument()
expect(queryByTestId('studio-player')).not.toBeInTheDocument()
})
it('Renders a video icon instead of a player for video/x-ms-wmv', async () => {
const aFile = new File(['foo'], 'foo.wmv', {type: 'video/x-ms-wmv'})
const {getByTestId, getByText, queryByTestId} = selectFile(aFile)
const icon = await waitFor(() => getByTestId('preview-video-icon'))
expect(icon).toBeInTheDocument()
expect(getByText('No preview is available for this file.')).toBeInTheDocument()
expect(queryByTestId('studio-player')).not.toBeInTheDocument()
})
it('clicking the trash button removes the file preview', async () => {
const aFile = new File(['foo'], 'foo.mov', {
type: 'video/quicktime',
})
const setFile = vi.fn()
const setHasUploadedFile = vi.fn()
const {getByText} = renderPanel({
theFile: aFile,
setFile,
setHasUploadedFile,
hasUploadedFile: true,
})
const clearButton = await waitFor(() => getByText('Remove'))
expect(clearButton).toBeInTheDocument()
act(() => {
fireEvent.click(clearButton)
})
expect(setHasUploadedFile).toHaveBeenCalledWith(false)
expect(setFile).toHaveBeenCalledWith(null)
})
})
describe('shows closed captions panel', () => {
beforeEach(() => {
window.matchMedia =
window.matchMedia ||
(() => ({
matches: false,
addListener: () => {},
removeListener: () => {},
}))
})
it('shows closed captions panel when uploading videos', async () => {
renderPanel({
theFile: new File(['bits'], 'dummy-video.mp4', {
lastModified: 1568991600840,
type: 'video/mp4',
}),
hasUploadedFile: true,
})
const ccCheckbox = await screen.findByRole('checkbox', {name: 'Add CC/Subtitles'})
fireEvent.click(ccCheckbox)
expect(screen.getByText('Add CC/Subtitles')).toBeInTheDocument()
})
it('shows closed captions panel when uploading audios', async () => {
renderPanel({
theFile: new File(['bits'], 'dummy-audio.mp3', {
lastModified: 1568991600840,
type: 'audio/mp3',
}),
hasUploadedFile: true,
})
const ccCheckbox = await screen.findByRole('checkbox', {name: 'Add CC/Subtitles'})
fireEvent.click(ccCheckbox)
expect(screen.getByText('Add CC/Subtitles')).toBeInTheDocument()
})
})
})