/* * 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() }) }) })