Show Media Captions in New Quizzes
closes LF-804 flag=none Test Plan: - Add Video to a New Quiz - Add captions - Refresh the New Quiz - Open Video tray * Verify captions appear - Repeat with Audio Change-Id: I1a3f0df0bfcaec7b3f456d7dcfea78017ab1a549 Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/334124 Reviewed-by: Eric Saupe <eric.saupe@instructure.com> QA-Review: Eric Saupe <eric.saupe@instructure.com> Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com> Product-Review: Jacob DeWar <jacob.dewar@instructure.com>
This commit is contained in:
parent
ccbfef7d8d
commit
92353a1edf
|
@ -101,6 +101,23 @@ export default class TrayController {
|
|||
})
|
||||
}
|
||||
|
||||
requestSubtitlesFromIframe(cb) {
|
||||
if (!bridge.canvasOrigin) return
|
||||
|
||||
this._subtitleListener = new AbortController()
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event?.data?.subject === "media_tracks_response") {
|
||||
cb(event?.data?.payload)
|
||||
}
|
||||
}, {signal: this._subtitleListener.signal})
|
||||
|
||||
this._audioContainer?.contentWindow?.postMessage(
|
||||
{subject: 'media_tracks_request'},
|
||||
bridge.canvasOrigin
|
||||
)
|
||||
}
|
||||
|
||||
_renderTray(trayProps) {
|
||||
const audioOptions = asAudioElement(this._audioContainer) || {}
|
||||
|
||||
|
@ -113,6 +130,7 @@ export default class TrayController {
|
|||
onExited={() => {
|
||||
bridge.focusActiveEditor(false)
|
||||
this._isOpen = false
|
||||
this._subtitleListener?.abort()
|
||||
}}
|
||||
onSave={options => {
|
||||
this._applyAudioOptions(options)
|
||||
|
@ -121,6 +139,7 @@ export default class TrayController {
|
|||
onDismiss={() => this._dismissTray()}
|
||||
open={this._shouldOpen}
|
||||
trayProps={trayProps}
|
||||
requestSubtitlesFromIframe={(cb) => this.requestSubtitlesFromIframe(cb)}
|
||||
/>
|
||||
)
|
||||
ReactDOM.render(element, this.container)
|
||||
|
|
|
@ -34,9 +34,11 @@ describe('RCE "Audios" Plugin > AudioOptionsTray', () => {
|
|||
onRequestClose: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
open: true,
|
||||
requestSubtitlesFromIframe: jest.fn(),
|
||||
audioOptions: {
|
||||
id: 'm-audio-id',
|
||||
titleText: 'Audio player',
|
||||
tracks: [{locale: 'en', inherited: false}],
|
||||
},
|
||||
trayProps: {
|
||||
host: 'localhost:3001',
|
||||
|
@ -76,4 +78,17 @@ describe('RCE "Audios" Plugin > AudioOptionsTray', () => {
|
|||
tray.$doneButton.click()
|
||||
expect(props.onSave).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
describe('requestSubtitlesFromIframe', () => {
|
||||
it('is not called when subtitles are present', () => {
|
||||
renderComponent()
|
||||
expect(props.requestSubtitlesFromIframe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('is called when no subtitles present', () => {
|
||||
props.audioOptions.tracks = null
|
||||
renderComponent()
|
||||
expect(props.requestSubtitlesFromIframe).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -150,4 +150,54 @@ describe('RCE "Audios" Plugin > AudioOptionsTray > TrayController', () => {
|
|||
await waitFor(() => expect(getTray()).toBeNull()) // the tray is closed after a transition
|
||||
})
|
||||
})
|
||||
|
||||
describe.only('#requestSubtitlesFromIframe', () => {
|
||||
let previousOrigin = ''
|
||||
|
||||
beforeAll(() => {
|
||||
previousOrigin = bridge.canvasOrigin
|
||||
bridge.canvasOrigin = 'http://localhost'
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
bridge.canvasOrigin = previousOrigin
|
||||
})
|
||||
|
||||
it('posts message to iframe onload', () => {
|
||||
const postMessageMock = jest.fn()
|
||||
const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode())
|
||||
iframe.contentWindow.postMessage = postMessageMock;
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
expect(postMessageMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('cleans up event listener on tray close', () => {
|
||||
const postMessageMock = jest.fn()
|
||||
const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode())
|
||||
iframe.contentWindow.postMessage = postMessageMock;
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
trayController.hideTrayForEditor(editors[0])
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
expect(postMessageMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('adds an event listener with a callback', () => {
|
||||
const eventMock = jest.fn()
|
||||
trayController.requestSubtitlesFromIframe(eventMock)
|
||||
const msgEvent = new Event('message')
|
||||
msgEvent.data = {subject: 'media_tracks_response', payload: [{locale: 'en'}]}
|
||||
window.dispatchEvent(msgEvent)
|
||||
expect(eventMock).toHaveBeenCalledTimes(1)
|
||||
expect(eventMock).toHaveBeenCalledWith([{locale: 'en'}])
|
||||
})
|
||||
|
||||
it('event listener ignores events with wrong subject', () => {
|
||||
const eventMock = jest.fn()
|
||||
trayController.requestSubtitlesFromIframe(eventMock)
|
||||
const msgEvent = new Event('message')
|
||||
msgEvent.data = {subject: 'wrong_response', payload: [{locale: 'en'}]}
|
||||
window.dispatchEvent(msgEvent)
|
||||
expect(eventMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import React, {useState} from 'react'
|
||||
import React, {useState, useEffect} from 'react'
|
||||
import {arrayOf, bool, func, shape, string} from 'prop-types'
|
||||
import {Flex} from '@instructure/ui-flex'
|
||||
import {Tray} from '@instructure/ui-tray'
|
||||
|
@ -40,9 +40,14 @@ export default function AudioOptionsTray({
|
|||
onSave,
|
||||
trayProps,
|
||||
audioOptions,
|
||||
requestSubtitlesFromIframe,
|
||||
}) {
|
||||
const [subtitles, setSubtitles] = useState(audioOptions.tracks || [])
|
||||
|
||||
useEffect(() => {
|
||||
if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles)
|
||||
}, [])
|
||||
|
||||
const handleSave = (e, contentProps) => {
|
||||
onSave({
|
||||
media_object_id: audioOptions.id,
|
||||
|
@ -129,6 +134,7 @@ AudioOptionsTray.propTypes = {
|
|||
onDismiss: func,
|
||||
onSave: func,
|
||||
open: bool.isRequired,
|
||||
requestSubtitlesFromIframe: func,
|
||||
trayProps: shape({
|
||||
host: string.isRequired,
|
||||
jwt: string.isRequired,
|
||||
|
@ -149,4 +155,5 @@ AudioOptionsTray.defaultProps = {
|
|||
onExited: null,
|
||||
onDismiss: null,
|
||||
onSave: null,
|
||||
requestSubtitlesFromIframe: () => {}
|
||||
}
|
||||
|
|
|
@ -161,6 +161,22 @@ export default class TrayController {
|
|||
this._editor = null
|
||||
}
|
||||
|
||||
requestSubtitlesFromIframe(cb) {
|
||||
if (!bridge.canvasOrigin) return
|
||||
|
||||
this._subtitleListener = new AbortController()
|
||||
window.addEventListener('message', (event) => {
|
||||
if (event?.data?.subject === "media_tracks_response") {
|
||||
cb(event?.data?.payload)
|
||||
}
|
||||
}, {signal: this._subtitleListener.signal})
|
||||
|
||||
this.$videoContainer?.contentWindow?.postMessage(
|
||||
{subject: 'media_tracks_request'},
|
||||
bridge.canvasOrigin
|
||||
)
|
||||
}
|
||||
|
||||
_renderTray(trayProps) {
|
||||
let vo = {}
|
||||
|
||||
|
@ -185,6 +201,7 @@ export default class TrayController {
|
|||
onExited={() => {
|
||||
bridge.focusActiveEditor(false)
|
||||
this._isOpen = false
|
||||
this._subtitleListener?.abort()
|
||||
}}
|
||||
onSave={videoOptions => {
|
||||
this._applyVideoOptions(videoOptions)
|
||||
|
@ -197,6 +214,7 @@ export default class TrayController {
|
|||
? parseStudioOptions(this.$videoContainer)
|
||||
: null
|
||||
}
|
||||
requestSubtitlesFromIframe={(cb) => this.requestSubtitlesFromIframe(cb)}
|
||||
/>
|
||||
)
|
||||
ReactDOM.render(element, this.$container)
|
||||
|
|
|
@ -25,6 +25,7 @@ import VideoOptionsTrayDriver from './VideoOptionsTrayDriver'
|
|||
import * as contentSelection from '../../../shared/ContentSelection'
|
||||
import RCEGlobals from '../../../../RCEGlobals'
|
||||
import {createLiveRegion, removeLiveRegion} from '../../../../__tests__/liveRegionHelper'
|
||||
import bridge from '../../../../../bridge'
|
||||
|
||||
const mockVideoPlayers = [
|
||||
{
|
||||
|
@ -309,4 +310,54 @@ describe('RCE "Videos" Plugin > VideoOptionsTray > TrayController', () => {
|
|||
expect(updateMediaObject).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('#requestSubtitlesFromIframe', () => {
|
||||
let previousOrigin = ''
|
||||
|
||||
beforeAll(() => {
|
||||
previousOrigin = bridge.canvasOrigin
|
||||
bridge.canvasOrigin = 'http://localhost'
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
bridge.canvasOrigin = previousOrigin
|
||||
})
|
||||
|
||||
it('posts message to iframe onload', () => {
|
||||
const postMessageMock = jest.fn()
|
||||
const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode())
|
||||
iframe.contentWindow.postMessage = postMessageMock;
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
expect(postMessageMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('cleans up event listener on tray close', () => {
|
||||
const postMessageMock = jest.fn()
|
||||
const iframe = contentSelection.findMediaPlayerIframe(editors[0].selection.getNode())
|
||||
iframe.contentWindow.postMessage = postMessageMock;
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
trayController.hideTrayForEditor(editors[0])
|
||||
trayController.showTrayForEditor(editors[0])
|
||||
expect(postMessageMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('adds an event listener with a callback', () => {
|
||||
const eventMock = jest.fn()
|
||||
trayController.requestSubtitlesFromIframe(eventMock)
|
||||
const msgEvent = new Event('message')
|
||||
msgEvent.data = {subject: 'media_tracks_response', payload: [{locale: 'en'}]}
|
||||
window.dispatchEvent(msgEvent)
|
||||
expect(eventMock).toHaveBeenCalledTimes(1)
|
||||
expect(eventMock).toHaveBeenCalledWith([{locale: 'en'}])
|
||||
})
|
||||
|
||||
it('event listener ignores events with wrong subject', () => {
|
||||
const eventMock = jest.fn()
|
||||
trayController.requestSubtitlesFromIframe(eventMock)
|
||||
const msgEvent = new Event('message')
|
||||
msgEvent.data = {subject: 'wrong_response', payload: [{locale: 'en'}]}
|
||||
window.dispatchEvent(msgEvent)
|
||||
expect(eventMock).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -37,6 +37,7 @@ describe('RCE "Videos" Plugin > VideoOptionsTray', () => {
|
|||
onRequestClose: jest.fn(),
|
||||
onSave: jest.fn(),
|
||||
open: true,
|
||||
requestSubtitlesFromIframe: jest.fn(),
|
||||
videoOptions: {
|
||||
$element: null,
|
||||
appliedHeight: 180,
|
||||
|
@ -202,6 +203,19 @@ describe('RCE "Videos" Plugin > VideoOptionsTray', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('requestSubtitlesFromIframe', () => {
|
||||
it('is not called when subtitles are present', () => {
|
||||
renderComponent()
|
||||
expect(props.requestSubtitlesFromIframe).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('is called when no subtitles present', () => {
|
||||
props.videoOptions.tracks = null
|
||||
renderComponent()
|
||||
expect(props.requestSubtitlesFromIframe).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when clicked', () => {
|
||||
beforeEach(() => {
|
||||
renderComponent()
|
||||
|
|
|
@ -56,6 +56,7 @@ export default function VideoOptionsTray({
|
|||
onSave,
|
||||
open,
|
||||
trayProps,
|
||||
requestSubtitlesFromIframe = () => {},
|
||||
onEntered = null,
|
||||
onExited = null,
|
||||
id = 'video-options-tray',
|
||||
|
@ -96,6 +97,10 @@ export default function VideoOptionsTray({
|
|||
}
|
||||
}, [videoOptions.attachmentId])
|
||||
|
||||
useEffect(() => {
|
||||
if (subtitles.length === 0) requestSubtitlesFromIframe(setSubtitles)
|
||||
}, [])
|
||||
|
||||
function handleTitleTextChange(event) {
|
||||
setTitleText(event.target.value)
|
||||
}
|
||||
|
@ -374,4 +379,5 @@ VideoOptionsTray.propTypes = {
|
|||
}),
|
||||
id: string,
|
||||
studioOptions: parsedStudioOptionsPropType,
|
||||
requestSubtitlesFromIframe: func
|
||||
}
|
||||
|
|
|
@ -63,12 +63,27 @@ ready(() => {
|
|||
}
|
||||
}
|
||||
|
||||
const mediaTracks = media_object?.media_tracks?.map(track => {
|
||||
return {
|
||||
id: track.id,
|
||||
src: track.url,
|
||||
label: captionLanguageForLocale(track.locale),
|
||||
type: track.kind,
|
||||
language: track.locale,
|
||||
inherited: track.inherited,
|
||||
}
|
||||
})
|
||||
|
||||
window.addEventListener(
|
||||
'message',
|
||||
event => {
|
||||
if (event?.data?.subject === 'reload_media' && media_id === event?.data?.media_object_id) {
|
||||
document.getElementsByTagName('video')[0].load()
|
||||
}
|
||||
else if (event?.data?.subject === 'media_tracks_request') {
|
||||
const tracks = mediaTracks?.map(t => ({locale: t.language, language: t.label, inherited: t.inherited}))
|
||||
if (tracks) event.source.postMessage({subject: 'media_tracks_response', payload: tracks}, event.origin)
|
||||
}
|
||||
},
|
||||
false
|
||||
)
|
||||
|
@ -88,16 +103,6 @@ ready(() => {
|
|||
}
|
||||
}
|
||||
|
||||
const mediaTracks = media_object?.media_tracks?.map(track => {
|
||||
return {
|
||||
id: track.id,
|
||||
src: track.url,
|
||||
label: captionLanguageForLocale(track.locale),
|
||||
type: track.kind,
|
||||
language: track.locale,
|
||||
inherited: track.inherited,
|
||||
}
|
||||
})
|
||||
|
||||
const aria_label = !media_object.title ? undefined : media_object.title
|
||||
ReactDOM.render(
|
||||
|
|
Loading…
Reference in New Issue