diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5eb50a06f2..9b4d828e8c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,7 +5,7 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. -LMS: Forums. Added handling for case where discussion module can get `None` as +LMS: Forums. Added handling for case where discussion module can get `None` as value of lms.start in `lms/djangoapps/django_comment_client/utils.py` Studio, LMS: Make ModelTypes more strict about their expected content (for @@ -16,6 +16,8 @@ an Integer can contain 3 or '3'. This changed an update to the xblock library. LMS: Courses whose id matches a regex in the COURSES_WITH_UNSAFE_CODE Django setting now run entirely outside the Python sandbox. +Blades: Added tests for Video Alpha player. + Blades: Video Alpha bug fix for speed changing to 1.0 in Firefox. Blades: Additional event tracking added to Video Alpha: fullscreen switch, show/hide diff --git a/common/lib/xmodule/xmodule/js/fixtures/videoalpha.html b/common/lib/xmodule/xmodule/js/fixtures/videoalpha.html new file mode 100644 index 0000000000..bccf5df2cc --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/videoalpha.html @@ -0,0 +1,23 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html new file mode 100644 index 0000000000..6088d07f2b --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/videoalpha_html5.html @@ -0,0 +1,27 @@ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/common/lib/xmodule/xmodule/js/spec/helper.coffee b/common/lib/xmodule/xmodule/js/spec/helper.coffee index 5cf75366d8..5f7fc27be0 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.coffee +++ b/common/lib/xmodule/xmodule/js/spec/helper.coffee @@ -20,10 +20,25 @@ jasmine.stubbedMetadata = bogus: duration: 100 +jasmine.fireEvent = (el, eventName) -> + if document.createEvent + event = document.createEvent "HTMLEvents" + event.initEvent eventName, true, true + else + event = document.createEventObject() + event.eventType = eventName + event.eventName = eventName + if document.createEvent + el.dispatchEvent(event) + else + el.fireEvent("on" + event.eventType, event) + jasmine.stubbedCaption = start: [0, 10000, 20000, 30000] text: ['Caption at 0', 'Caption at 10000', 'Caption at 20000', 'Caption at 30000'] +jasmine.stubbedHtml5Speeds = ['0.75', '1.0', '1.25', '1.50'] + jasmine.stubRequests = -> spyOn($, 'ajax').andCallFake (settings) -> if match = settings.url.match /youtube\.com\/.+\/videos\/(.+)\?v=2&alt=jsonc/ @@ -41,9 +56,12 @@ jasmine.stubRequests = -> throw "External request attempted for #{settings.url}, which is not defined." jasmine.stubYoutubePlayer = -> - YT.Player = -> jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode', + YT.Player = -> + obj = jasmine.createSpyObj 'YT.Player', ['cueVideoById', 'getVideoEmbedCode', 'getCurrentTime', 'getPlayerState', 'getVolume', 'setVolume', 'loadVideoById', - 'playVideo', 'pauseVideo', 'seekTo'] + 'playVideo', 'pauseVideo', 'seekTo', 'getDuration', 'getAvailablePlaybackRates', 'setPlaybackRate'] + obj['getAvailablePlaybackRates'] = jasmine.createSpy('getAvailablePlaybackRates').andReturn [0.75, 1.0, 1.25, 1.5] + obj jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> enableParts = [enableParts] unless $.isArray(enableParts) @@ -60,6 +78,21 @@ jasmine.stubVideoPlayer = (context, enableParts, createPlayer=true) -> if createPlayer return new VideoPlayer(video: context.video) +jasmine.stubVideoPlayerAlpha = (context, enableParts, createPlayer=true, html5=false) -> + suite = context.suite + currentPartName = suite.description while suite = suite.parentSuite + if html5 == false + loadFixtures 'videoalpha.html' + else + loadFixtures 'videoalpha_html5.html' + jasmine.stubRequests() + YT.Player = undefined + window.OldVideoPlayerAlpha = undefined + context.video = new VideoAlpha '#example', '.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' + jasmine.stubYoutubePlayer() + if createPlayer + return new VideoPlayerAlpha(video: context.video) + # Stub jQuery.cookie $.cookie = jasmine.createSpy('jQuery.cookie').andReturn '1.0' diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/html5_video.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/html5_video.coffee new file mode 100644 index 0000000000..176ceb7827 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/html5_video.coffee @@ -0,0 +1,311 @@ +describe 'VideoAlpha HTML5Video', -> + playbackRates = [0.75, 1.0, 1.25, 1.5] + STATUS = window.YT.PlayerState + playerVars = + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + modestbranding: 1 + html5: 1 + file = window.location.href.replace(/\/common(.*)$/, '') + '/test_root/data/videoalpha/gizmo' + html5Sources = + mp4: "#{file}.mp4" + webm: "#{file}.webm" + ogg: "#{file}.ogv" + onReady = jasmine.createSpy 'onReady' + onStateChange = jasmine.createSpy 'onStateChange' + + beforeEach -> + loadFixtures 'videoalpha_html5.html' + @el = $('#example').find('.video') + @player = new window.HTML5Video.Player @el, + playerVars: playerVars, + videoSources: html5Sources, + events: + onReady: onReady + onStateChange: onStateChange + + @videoEl = @el.find('.video-player video').get(0) + + it 'PlayerState', -> + expect(HTML5Video.PlayerState).toEqual STATUS + + describe 'constructor', -> + it 'create an html5 video element', -> + expect(@el.find('.video-player div')).toContain 'video' + + it 'check if sources are created in correct way', -> + sources = $(@videoEl).find('source') + videoTypes = [] + videoSources = [] + $.each html5Sources, (index, source) -> + videoTypes.push index + videoSources.push source + $.each sources, (index, source) -> + s = $(source) + expect($.inArray(s.attr('src'), videoSources)).not.toEqual -1 + expect($.inArray(s.attr('type').replace('video/', ''), videoTypes)) + .not.toEqual -1 + + it 'check if click event is handled on the player', -> + expect(@videoEl).toHandle 'click' + + # NOTE: According to + # + # https://github.com/ariya/phantomjs/wiki/Supported-Web-Standards#unsupported-features + # + # Video and Audio (due to the nature of PhantomJS) are not supported. After discussion + # with William Daly, some tests are disabled (Jenkins uses phantomjs for running tests + # and those tests fail). + # + # During code review, please enable the test below (change "xdescribe" to "describe" + # to enable the test). + xdescribe 'events:', -> + + beforeEach -> + spyOn(@player, 'callStateChangeCallback').andCallThrough() + + describe 'click', -> + describe 'when player is paused', -> + beforeEach -> + spyOn(@videoEl, 'play').andCallThrough() + @player.playerState = STATUS.PAUSED + $(@videoEl).trigger('click') + + it 'native play event was called', -> + expect(@videoEl.play).toHaveBeenCalled() + + it 'player state was changed', -> + expect(@player.playerState).toBe STATUS.PLAYING + + it 'callback was called', -> + expect(@player.callStateChangeCallback).toHaveBeenCalled() + + describe 'when player is played', -> + + beforeEach -> + spyOn(@videoEl, 'pause').andCallThrough() + @player.playerState = STATUS.PLAYING + $(@videoEl).trigger('click') + + it 'native pause event was called', -> + expect(@videoEl.pause).toHaveBeenCalled() + + it 'player state was changed', -> + expect(@player.playerState).toBe STATUS.PAUSED + + it 'callback was called', -> + expect(@player.callStateChangeCallback).toHaveBeenCalled() + + describe 'play', -> + + beforeEach -> + spyOn(@videoEl, 'play').andCallThrough() + @player.playerState = STATUS.PAUSED + @videoEl.play() + + it 'native event was called', -> + expect(@videoEl.play).toHaveBeenCalled() + + it 'player state was changed', -> + waitsFor ( -> + @player.playerState != HTML5Video.PlayerState.PAUSED + ), 'Player state should be changed', 1000 + + runs -> + expect(@player.playerState).toBe STATUS.PLAYING + + it 'callback was called', -> + waitsFor ( -> + @player.playerState != STATUS.PAUSED + ), 'Player state should be changed', 1000 + + runs -> + expect(@player.callStateChangeCallback).toHaveBeenCalled() + + describe 'pause', -> + + beforeEach -> + spyOn(@videoEl, 'pause').andCallThrough() + @videoEl.play() + @videoEl.pause() + + it 'native event was called', -> + expect(@videoEl.pause).toHaveBeenCalled() + + it 'player state was changed', -> + waitsFor ( -> + @player.playerState != STATUS.UNSTARTED + ), 'Player state should be changed', 1000 + + runs -> + expect(@player.playerState).toBe STATUS.PAUSED + + it 'callback was called', -> + waitsFor ( -> + @player.playerState != HTML5Video.PlayerState.UNSTARTED + ), 'Player state should be changed', 1000 + + runs -> + expect(@player.callStateChangeCallback).toHaveBeenCalled() + + describe 'canplay', -> + + beforeEach -> + waitsFor ( -> + @player.playerState != STATUS.UNSTARTED + ), 'Video cannot be played', 1000 + + it 'player state was changed', -> + runs -> + expect(@player.playerState).toBe STATUS.PAUSED + + it 'end property was defined', -> + runs -> + expect(@player.end).not.toBeNull() + + it 'start position was defined', -> + runs -> + expect(@videoEl.currentTime).toBe(@player.start) + + it 'callback was called', -> + runs -> + expect(@player.config.events.onReady).toHaveBeenCalled() + + describe 'ended', -> + beforeEach -> + waitsFor ( -> + @player.playerState != STATUS.UNSTARTED + ), 'Video cannot be played', 1000 + + it 'player state was changed', -> + runs -> + jasmine.fireEvent @videoEl, "ended" + expect(@player.playerState).toBe STATUS.ENDED + + it 'callback was called', -> + jasmine.fireEvent @videoEl, "ended" + expect(@player.callStateChangeCallback).toHaveBeenCalled() + + describe 'timeupdate', -> + + beforeEach -> + spyOn(@videoEl, 'pause').andCallThrough() + waitsFor ( -> + @player.playerState != STATUS.UNSTARTED + ), 'Video cannot be played', 1000 + + it 'player should be paused', -> + runs -> + @player.end = 3 + @videoEl.currentTime = 5 + jasmine.fireEvent @videoEl, "timeupdate" + expect(@videoEl.pause).toHaveBeenCalled() + + it 'end param should be re-defined', -> + runs -> + @player.end = 3 + @videoEl.currentTime = 5 + jasmine.fireEvent @videoEl, "timeupdate" + expect(@player.end).toBe @videoEl.duration + + # NOTE: According to + # + # https://github.com/ariya/phantomjs/wiki/Supported-Web-Standards#unsupported-features + # + # Video and Audio (due to the nature of PhantomJS) are not supported. After discussion + # with William Daly, some tests are disabled (Jenkins uses phantomjs for running tests + # and those tests fail). + # + # During code review, please enable the test below (change "xdescribe" to "describe" + # to enable the test). + xdescribe 'methods:', -> + + beforeEach -> + waitsFor ( -> + @volume = @videoEl.volume + @seek = @videoEl.currentTime + @player.playerState == STATUS.PAUSED + ), 'Video cannot be played', 1000 + + + it 'pauseVideo', -> + spyOn(@videoEl, 'pause').andCallThrough() + @player.pauseVideo() + expect(@videoEl.pause).toHaveBeenCalled() + + describe 'seekTo', -> + + it 'set new correct value', -> + runs -> + @player.seekTo(2) + expect(@videoEl.currentTime).toBe 2 + + it 'set new inccorrect values', -> + runs -> + @player.seekTo(-50) + expect(@videoEl.currentTime).toBe @seek + @player.seekTo('5') + expect(@videoEl.currentTime).toBe @seek + @player.seekTo(500000) + expect(@videoEl.currentTime).toBe @seek + + describe 'setVolume', -> + + it 'set new correct value', -> + runs -> + @player.setVolume(50) + expect(@videoEl.volume).toBe 50*0.01 + + it 'set new inccorrect values', -> + runs -> + @player.setVolume(-50) + expect(@videoEl.volume).toBe @volume + @player.setVolume('5') + expect(@videoEl.volume).toBe @volume + @player.setVolume(500000) + expect(@videoEl.volume).toBe @volume + + it 'getCurrentTime', -> + runs -> + @videoEl.currentTime = 3 + expect(@player.getCurrentTime()).toBe @videoEl.currentTime + + it 'playVideo', -> + runs -> + spyOn(@videoEl, 'play').andCallThrough() + @player.playVideo() + expect(@videoEl.play).toHaveBeenCalled() + + it 'getPlayerState', -> + runs -> + @player.playerState = STATUS.PLAYING + expect(@player.getPlayerState()).toBe STATUS.PLAYING + @player.playerState = STATUS.ENDED + expect(@player.getPlayerState()).toBe STATUS.ENDED + + it 'getVolume', -> + runs -> + @volume = @videoEl.volume = 0.5 + expect(@player.getVolume()).toBe @volume + + it 'getDuration', -> + runs -> + @duration = @videoEl.duration + expect(@player.getDuration()).toBe @duration + + describe 'setPlaybackRate', -> + it 'set a correct value', -> + @playbackRate = 1.5 + @player.setPlaybackRate @playbackRate + expect(@videoEl.playbackRate).toBe @playbackRate + + it 'set NaN value', -> + @playbackRate = NaN + @player.setPlaybackRate @playbackRate + expect(@videoEl.playbackRate).toBe 1.0 + + it 'getAvailablePlaybackRates', -> + expect(@player.getAvailablePlaybackRates()).toEqual playbackRates diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_caption_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_caption_spec.coffee new file mode 100644 index 0000000000..4bd237b81d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_caption_spec.coffee @@ -0,0 +1,373 @@ +describe 'VideoCaptionAlpha', -> + + beforeEach -> + spyOn(VideoCaptionAlpha.prototype, 'fetchCaption').andCallThrough() + spyOn($, 'ajaxWithPrefix').andCallThrough() + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + + afterEach -> + YT.Player = undefined + $.fn.scrollTo.reset() + $('.subtitles').remove() + + describe 'constructor', -> + + describe 'always', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + + it 'set the youtube id', -> + expect(@caption.youtubeId).toEqual 'normalSpeedYoutubeId' + + it 'create the caption element', -> + expect($('.video')).toContain 'ol.subtitles' + + it 'add caption control to video player', -> + expect($('.video')).toContain 'a.hide-subtitles' + + it 'fetch the caption', -> + expect(@caption.loaded).toBeTruthy() + expect(@caption.fetchCaption).toHaveBeenCalled() + expect($.ajaxWithPrefix).toHaveBeenCalledWith + url: @caption.captionURL() + notifyOnError: false + success: jasmine.any(Function) + + it 'bind window resize event', -> + expect($(window)).toHandleWith 'resize', @caption.resize + + it 'bind the hide caption button', -> + expect($('.hide-subtitles')).toHandleWith 'click', @caption.toggle + + it 'bind the mouse movement', -> + expect($('.subtitles')).toHandleWith 'mouseover', @caption.onMouseEnter + expect($('.subtitles')).toHandleWith 'mouseout', @caption.onMouseLeave + expect($('.subtitles')).toHandleWith 'mousemove', @caption.onMovement + expect($('.subtitles')).toHandleWith 'mousewheel', @caption.onMovement + expect($('.subtitles')).toHandleWith 'DOMMouseScroll', @caption.onMovement + + describe 'when on a non touch-based device', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + + it 'render the caption', -> + captionsData = jasmine.stubbedCaption + $('.subtitles li[data-index]').each (index, link) => + expect($(link)).toHaveData 'index', index + expect($(link)).toHaveData 'start', captionsData.start[index] + expect($(link)).toHaveText captionsData.text[index] + + it 'add a padding element to caption', -> + expect($('.subtitles li:first')).toBe '.spacing' + expect($('.subtitles li:last')).toBe '.spacing' + + it 'bind all the caption link', -> + $('.subtitles li[data-index]').each (index, link) => + expect($(link)).toHandleWith 'click', @caption.seekPlayer + + it 'set rendered to true', -> + expect(@caption.rendered).toBeTruthy() + + describe 'when on a touch-based device', -> + + beforeEach -> + window.onTouchBasedDevice.andReturn true + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + + it 'show explaination message', -> + expect($('.subtitles li')).toHaveHtml "Caption will be displayed when you start playing the video." + + it 'does not set rendered to true', -> + expect(@caption.rendered).toBeFalsy() + + describe 'mouse movement', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + window.setTimeout.andReturn(100) + spyOn window, 'clearTimeout' + + describe 'when cursor is outside of the caption box', -> + + beforeEach -> + $(window).trigger jQuery.Event 'mousemove' + + it 'does not set freezing timeout', -> + expect(@caption.frozen).toBeFalsy() + + describe 'when cursor is in the caption box', -> + + beforeEach -> + $('.subtitles').trigger jQuery.Event 'mouseenter' + + it 'set the freezing timeout', -> + expect(@caption.frozen).toEqual 100 + + describe 'when the cursor is moving', -> + beforeEach -> + $('.subtitles').trigger jQuery.Event 'mousemove' + + it 'reset the freezing timeout', -> + expect(window.clearTimeout).toHaveBeenCalledWith 100 + + describe 'when the mouse is scrolling', -> + beforeEach -> + $('.subtitles').trigger jQuery.Event 'mousewheel' + + it 'reset the freezing timeout', -> + expect(window.clearTimeout).toHaveBeenCalledWith 100 + + describe 'when cursor is moving out of the caption box', -> + beforeEach -> + @caption.frozen = 100 + $.fn.scrollTo.reset() + + describe 'always', -> + beforeEach -> + $('.subtitles').trigger jQuery.Event 'mouseout' + + it 'reset the freezing timeout', -> + expect(window.clearTimeout).toHaveBeenCalledWith 100 + + it 'unfreeze the caption', -> + expect(@caption.frozen).toBeNull() + + describe 'when the player is playing', -> + beforeEach -> + @caption.playing = true + $('.subtitles li[data-index]:first').addClass 'current' + $('.subtitles').trigger jQuery.Event 'mouseout' + + it 'scroll the caption', -> + expect($.fn.scrollTo).toHaveBeenCalled() + + describe 'when the player is not playing', -> + beforeEach -> + @caption.playing = false + $('.subtitles').trigger jQuery.Event 'mouseout' + + it 'does not scroll the caption', -> + expect($.fn.scrollTo).not.toHaveBeenCalled() + + describe 'search', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + + it 'return a correct caption index', -> + expect(@caption.search(0)).toEqual 0 + expect(@caption.search(9999)).toEqual 0 + expect(@caption.search(10000)).toEqual 1 + expect(@caption.search(15000)).toEqual 1 + expect(@caption.search(30000)).toEqual 3 + expect(@caption.search(30001)).toEqual 3 + + describe 'play', -> + describe 'when the caption was not rendered', -> + beforeEach -> + window.onTouchBasedDevice.andReturn true + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + @caption.play() + + it 'render the caption', -> + captionsData = jasmine.stubbedCaption + $('.subtitles li[data-index]').each (index, link) => + expect($(link)).toHaveData 'index', index + expect($(link)).toHaveData 'start', captionsData.start[index] + expect($(link)).toHaveText captionsData.text[index] + + it 'add a padding element to caption', -> + expect($('.subtitles li:first')).toBe '.spacing' + expect($('.subtitles li:last')).toBe '.spacing' + + it 'bind all the caption link', -> + $('.subtitles li[data-index]').each (index, link) => + expect($(link)).toHandleWith 'click', @caption.seekPlayer + + it 'set rendered to true', -> + expect(@caption.rendered).toBeTruthy() + + it 'set playing to true', -> + expect(@caption.playing).toBeTruthy() + + describe 'pause', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + @caption.playing = true + @caption.pause() + + it 'set playing to false', -> + expect(@caption.playing).toBeFalsy() + + describe 'updatePlayTime', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + + describe 'when the video speed is 1.0x', -> + beforeEach -> + @caption.currentSpeed = '1.0' + @caption.updatePlayTime 25.000 + + it 'search the caption based on time', -> + expect(@caption.currentIndex).toEqual 2 + + describe 'when the video speed is not 1.0x', -> + beforeEach -> + @caption.currentSpeed = '0.75' + @caption.updatePlayTime 25.000 + + it 'search the caption based on 1.0x speed', -> + expect(@caption.currentIndex).toEqual 1 + + describe 'when the index is not the same', -> + beforeEach -> + @caption.currentIndex = 1 + $('.subtitles li[data-index=1]').addClass 'current' + @caption.updatePlayTime 25.000 + + it 'deactivate the previous caption', -> + expect($('.subtitles li[data-index=1]')).not.toHaveClass 'current' + + it 'activate new caption', -> + expect($('.subtitles li[data-index=2]')).toHaveClass 'current' + + it 'save new index', -> + expect(@caption.currentIndex).toEqual 2 + + it 'scroll caption to new position', -> + expect($.fn.scrollTo).toHaveBeenCalled() + + describe 'when the index is the same', -> + beforeEach -> + @caption.currentIndex = 1 + $('.subtitles li[data-index=1]').addClass 'current' + @caption.updatePlayTime 15.000 + + it 'does not change current subtitle', -> + expect($('.subtitles li[data-index=1]')).toHaveClass 'current' + + describe 'resize', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + $('.subtitles li[data-index=1]').addClass 'current' + @caption.resize() + + it 'set the height of caption container', -> + expect(parseInt($('.subtitles').css('maxHeight'))).toBeCloseTo $('.video-wrapper').height(), 2 + + it 'set the height of caption spacing', -> + firstSpacing = Math.abs(parseInt($('.subtitles .spacing:first').css('height'))) + lastSpacing = Math.abs(parseInt($('.subtitles .spacing:last').css('height'))) + + expect(firstSpacing - @caption.topSpacingHeight()).toBeLessThan 1 + expect(lastSpacing - @caption.bottomSpacingHeight()).toBeLessThan 1 + + it 'scroll caption to new position', -> + expect($.fn.scrollTo).toHaveBeenCalled() + + describe 'scrollCaption', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + + describe 'when frozen', -> + beforeEach -> + @caption.frozen = true + $('.subtitles li[data-index=1]').addClass 'current' + @caption.scrollCaption() + + it 'does not scroll the caption', -> + expect($.fn.scrollTo).not.toHaveBeenCalled() + + describe 'when not frozen', -> + beforeEach -> + @caption.frozen = false + + describe 'when there is no current caption', -> + beforeEach -> + @caption.scrollCaption() + + it 'does not scroll the caption', -> + expect($.fn.scrollTo).not.toHaveBeenCalled() + + describe 'when there is a current caption', -> + beforeEach -> + $('.subtitles li[data-index=1]').addClass 'current' + @caption.scrollCaption() + + it 'scroll to current caption', -> + offset = -0.5 * ($('.video-wrapper').height() - $('.subtitles .current:first').height()) + + expect($.fn.scrollTo).toHaveBeenCalledWith $('.subtitles .current:first', @caption.el), + offset: offset + + describe 'seekPlayer', -> + + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @caption = @player.caption + $(@caption).bind 'seek', (event, time) => @time = time + + describe 'when the video speed is 1.0x', -> + beforeEach -> + @caption.currentSpeed = '1.0' + $('.subtitles li[data-start="30000"]').trigger('click') + + it 'trigger seek event with the correct time', -> + expect(@player.currentTime).toEqual 30.000 + + describe 'when the video speed is not 1.0x', -> + beforeEach -> + @caption.currentSpeed = '0.75' + $('.subtitles li[data-start="30000"]').trigger('click') + + it 'trigger seek event with the correct time', -> + expect(@player.currentTime).toEqual 40.000 + + describe 'toggle', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + spyOn @video, 'log' + @caption = @player.caption + $('.subtitles li[data-index=1]').addClass 'current' + + describe 'when the caption is visible', -> + beforeEach -> + @caption.el.removeClass 'closed' + @caption.toggle jQuery.Event('click') + + it 'log the hide_transcript event', -> + expect(@video.log).toHaveBeenCalledWith 'hide_transcript', + currentTime: @player.currentTime + + it 'hide the caption', -> + expect(@caption.el).toHaveClass 'closed' + + describe 'when the caption is hidden', -> + beforeEach -> + @caption.el.addClass 'closed' + @caption.toggle jQuery.Event('click') + + it 'log the show_transcript event', -> + expect(@video.log).toHaveBeenCalledWith 'show_transcript', + currentTime: @player.currentTime + + it 'show the caption', -> + expect(@caption.el).not.toHaveClass 'closed' + + it 'scroll the caption', -> + expect($.fn.scrollTo).toHaveBeenCalled() diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_control_spec.coffee new file mode 100644 index 0000000000..a4dc8739d8 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_control_spec.coffee @@ -0,0 +1,103 @@ +describe 'VideoControlAlpha', -> + beforeEach -> + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + loadFixtures 'videoalpha.html' + $('.video-controls').html '' + + describe 'constructor', -> + + it 'render the video controls', -> + @control = new window.VideoControlAlpha(el: $('.video-controls')) + expect($('.video-controls')).toContain + ['.slider', 'ul.vcr', 'a.play', '.vidtime', '.add-fullscreen'].join(',') + expect($('.video-controls').find('.vidtime')).toHaveText '0:00 / 0:00' + + it 'bind the playback button', -> + @control = new window.VideoControlAlpha(el: $('.video-controls')) + expect($('.video_control')).toHandleWith 'click', @control.togglePlayback + + describe 'when on a touch based device', -> + beforeEach -> + window.onTouchBasedDevice.andReturn true + @control = new window.VideoControlAlpha(el: $('.video-controls')) + + it 'does not add the play class to video control', -> + expect($('.video_control')).not.toHaveClass 'play' + expect($('.video_control')).not.toHaveHtml 'Play' + + + describe 'when on a non-touch based device', -> + + beforeEach -> + @control = new window.VideoControlAlpha(el: $('.video-controls')) + + it 'add the play class to video control', -> + expect($('.video_control')).toHaveClass 'play' + expect($('.video_control')).toHaveHtml 'Play' + + describe 'play', -> + + beforeEach -> + @control = new window.VideoControlAlpha(el: $('.video-controls')) + @control.play() + + it 'switch playback button to play state', -> + expect($('.video_control')).not.toHaveClass 'play' + expect($('.video_control')).toHaveClass 'pause' + expect($('.video_control')).toHaveHtml 'Pause' + + describe 'pause', -> + + beforeEach -> + @control = new window.VideoControlAlpha(el: $('.video-controls')) + @control.pause() + + it 'switch playback button to pause state', -> + expect($('.video_control')).not.toHaveClass 'pause' + expect($('.video_control')).toHaveClass 'play' + expect($('.video_control')).toHaveHtml 'Play' + + describe 'togglePlayback', -> + + beforeEach -> + @control = new window.VideoControlAlpha(el: $('.video-controls')) + + describe 'when the control does not have play or pause class', -> + beforeEach -> + $('.video_control').removeClass('play').removeClass('pause') + + describe 'when the video is playing', -> + beforeEach -> + $('.video_control').addClass('play') + spyOnEvent @control, 'pause' + @control.togglePlayback jQuery.Event('click') + + it 'does not trigger the pause event', -> + expect('pause').not.toHaveBeenTriggeredOn @control + + describe 'when the video is paused', -> + beforeEach -> + $('.video_control').addClass('pause') + spyOnEvent @control, 'play' + @control.togglePlayback jQuery.Event('click') + + it 'does not trigger the play event', -> + expect('play').not.toHaveBeenTriggeredOn @control + + describe 'when the video is playing', -> + beforeEach -> + spyOnEvent @control, 'pause' + $('.video_control').addClass 'pause' + @control.togglePlayback jQuery.Event('click') + + it 'trigger the pause event', -> + expect('pause').toHaveBeenTriggeredOn @control + + describe 'when the video is paused', -> + beforeEach -> + spyOnEvent @control, 'play' + $('.video_control').addClass 'play' + @control.togglePlayback jQuery.Event('click') + + it 'trigger the play event', -> + expect('play').toHaveBeenTriggeredOn @control diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_player_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_player_spec.coffee new file mode 100644 index 0000000000..e9a5ca30b4 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_player_spec.coffee @@ -0,0 +1,561 @@ +describe 'VideoPlayerAlpha', -> + playerVars = + controls: 0 + wmode: 'transparent' + rel: 0 + showinfo: 0 + enablejsapi: 1 + modestbranding: 1 + html5: 1 + + beforeEach -> + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + # It tries to call methods of VideoProgressSlider on Spy + for part in ['VideoCaptionAlpha', 'VideoSpeedControlAlpha', 'VideoVolumeControlAlpha', 'VideoProgressSliderAlpha', 'VideoControlAlpha'] + spyOn(window[part].prototype, 'initialize').andCallThrough() + + + afterEach -> + YT.Player = undefined + + describe 'constructor', -> + beforeEach -> + $.fn.qtip.andCallFake -> + $(this).data('qtip', true) + + describe 'always', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + + it 'instanticate current time to zero', -> + expect(@player.currentTime).toEqual 0 + + it 'set the element', -> + expect(@player.el).toHaveId 'video_id' + + it 'create video control', -> + expect(window.VideoControlAlpha.prototype.initialize).toHaveBeenCalled() + expect(@player.control).toBeDefined() + expect(@player.control.el).toBe $('.video-controls', @player.el) + + it 'create video caption', -> + expect(window.VideoCaptionAlpha.prototype.initialize).toHaveBeenCalled() + expect(@player.caption).toBeDefined() + expect(@player.caption.el).toBe @player.el + expect(@player.caption.youtubeId).toEqual 'normalSpeedYoutubeId' + expect(@player.caption.currentSpeed).toEqual '1.0' + expect(@player.caption.captionAssetPath).toEqual '/static/subs/' + + it 'create video speed control', -> + expect(window.VideoSpeedControlAlpha.prototype.initialize).toHaveBeenCalled() + expect(@player.speedControl).toBeDefined() + expect(@player.speedControl.el).toBe $('.secondary-controls', @player.el) + expect(@player.speedControl.speeds).toEqual ['0.75', '1.0'] + expect(@player.speedControl.currentSpeed).toEqual '1.0' + + it 'create video progress slider', -> + expect(window.VideoSpeedControlAlpha.prototype.initialize).toHaveBeenCalled() + expect(@player.progressSlider).toBeDefined() + expect(@player.progressSlider.el).toBe $('.slider', @player.el) + + it 'bind to video control play event', -> + expect($(@player.control)).toHandleWith 'play', @player.play + + it 'bind to video control pause event', -> + expect($(@player.control)).toHandleWith 'pause', @player.pause + + it 'bind to video caption seek event', -> + expect($(@player.caption)).toHandleWith 'caption_seek', @player.onSeek + + it 'bind to video speed control speedChange event', -> + expect($(@player.speedControl)).toHandleWith 'speedChange', @player.onSpeedChange + + it 'bind to video progress slider seek event', -> + expect($(@player.progressSlider)).toHandleWith 'slide_seek', @player.onSeek + + it 'bind to video volume control volumeChange event', -> + expect($(@player.volumeControl)).toHandleWith 'volumeChange', @player.onVolumeChange + + it 'bind to key press', -> + expect($(document.documentElement)).toHandleWith 'keyup', @player.bindExitFullScreen + + it 'bind to fullscreen switching button', -> + expect($('.add-fullscreen')).toHandleWith 'click', @player.toggleFullScreen + + it 'create Youtube player', -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + spyOn YT, 'Player' + @player = new VideoPlayerAlpha video: @video + expect(YT.Player).toHaveBeenCalledWith('id', { + playerVars: playerVars + videoId: 'normalSpeedYoutubeId' + events: + onReady: @player.onReady + onStateChange: @player.onStateChange + onPlaybackQualityChange: @player.onPlaybackQualityChange + }) + + it 'create HTML5 player', -> + jasmine.stubVideoPlayerAlpha @, [], false, true + spyOn HTML5Video, 'Player' + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + expect(HTML5Video.Player).toHaveBeenCalledWith @video.el, + playerVars: playerVars + videoSources: @video.html5Sources + events: + onReady: @player.onReady + onStateChange: @player.onStateChange + + describe 'when not on a touch based device', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + $('.add-fullscreen, .hide-subtitles').removeData 'qtip' + @player = new VideoPlayerAlpha video: @video + + it 'add the tooltip to fullscreen and subtitle button', -> + expect($('.add-fullscreen')).toHaveData 'qtip' + expect($('.hide-subtitles')).toHaveData 'qtip' + + it 'create video volume control', -> + expect(window.VideoVolumeControlAlpha.prototype.initialize).toHaveBeenCalled() + expect(@player.volumeControl).toBeDefined() + expect(@player.volumeControl.el).toBe $('.secondary-controls', @player.el) + + describe 'when on a touch based device', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + window.onTouchBasedDevice.andReturn true + $('.add-fullscreen, .hide-subtitles').removeData 'qtip' + @player = new VideoPlayerAlpha video: @video + + it 'does not add the tooltip to fullscreen and subtitle button', -> + expect($('.add-fullscreen')).not.toHaveData 'qtip' + expect($('.hide-subtitles')).not.toHaveData 'qtip' + + it 'does not create video volume control', -> + expect(window.VideoVolumeControlAlpha.prototype.initialize).not.toHaveBeenCalled() + expect(@player.volumeControl).not.toBeDefined() + + describe 'onReady', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + spyOn @video, 'log' + $('.video').append $('
') + @video.embed() + @player = @video.player + spyOnEvent @player, 'ready' + spyOnEvent @player, 'updatePlayTime' + @player.onReady() + + it 'log the load_video event', -> + expect(@video.log).toHaveBeenCalledWith 'load_video' + + describe 'when not on a touch based device', -> + beforeEach -> + spyOn @player, 'play' + @player.onReady() + + it 'autoplay the first video', -> + expect(@player.play).toHaveBeenCalled() + + describe 'when on a touch based device', -> + beforeEach -> + window.onTouchBasedDevice.andReturn true + spyOn @player, 'play' + @player.onReady() + + it 'does not autoplay the first video', -> + expect(@player.play).not.toHaveBeenCalled() + + describe 'onStateChange', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + + describe 'when the video is unstarted', -> + beforeEach -> + @player = new VideoPlayerAlpha video: @video + spyOn @player.control, 'pause' + @player.caption.pause = jasmine.createSpy('VideoCaptionAlpha.pause') + @player.onStateChange data: YT.PlayerState.UNSTARTED + + it 'pause the video control', -> + expect(@player.control.pause).toHaveBeenCalled() + + it 'pause the video caption', -> + expect(@player.caption.pause).toHaveBeenCalled() + + describe 'when the video is playing', -> + beforeEach -> + @anotherPlayer = jasmine.createSpyObj 'AnotherPlayer', ['onPause'] + window.OldVideoPlayerAlpha = @anotherPlayer + @player = new VideoPlayerAlpha video: @video + spyOn @video, 'log' + spyOn(window, 'setInterval').andReturn 100 + spyOn @player.control, 'play' + @player.caption.play = jasmine.createSpy('VideoCaptionAlpha.play') + @player.progressSlider.play = jasmine.createSpy('VideoProgressSliderAlpha.play') + @player.player.getVideoEmbedCode.andReturn 'embedCode' + @player.onStateChange data: YT.PlayerState.PLAYING + + it 'log the play_video event', -> + expect(@video.log).toHaveBeenCalledWith 'play_video', {currentTime: 0} + + it 'pause other video player', -> + expect(@anotherPlayer.onPause).toHaveBeenCalled() + + it 'set current video player as active player', -> + expect(window.OldVideoPlayerAlpha).toEqual @player + + it 'set update interval', -> + expect(window.setInterval).toHaveBeenCalledWith @player.update, 200 + expect(@player.player.interval).toEqual 100 + + it 'play the video control', -> + expect(@player.control.play).toHaveBeenCalled() + + it 'play the video caption', -> + expect(@player.caption.play).toHaveBeenCalled() + + it 'play the video progress slider', -> + expect(@player.progressSlider.play).toHaveBeenCalled() + + describe 'when the video is paused', -> + beforeEach -> + @player = new VideoPlayerAlpha video: @video + spyOn @video, 'log' + spyOn window, 'clearInterval' + spyOn @player.control, 'pause' + @player.caption.pause = jasmine.createSpy('VideoCaptionAlpha.pause') + @player.player.interval = 100 + @player.player.getVideoEmbedCode.andReturn 'embedCode' + @player.onStateChange data: YT.PlayerState.PAUSED + + it 'log the pause_video event', -> + expect(@video.log).toHaveBeenCalledWith 'pause_video', {currentTime: 0} + + it 'clear update interval', -> + expect(window.clearInterval).toHaveBeenCalledWith 100 + expect(@player.player.interval).toBeNull() + + it 'pause the video control', -> + expect(@player.control.pause).toHaveBeenCalled() + + it 'pause the video caption', -> + expect(@player.caption.pause).toHaveBeenCalled() + + describe 'when the video is ended', -> + beforeEach -> + @player = new VideoPlayerAlpha video: @video + spyOn @player.control, 'pause' + @player.caption.pause = jasmine.createSpy('VideoCaptionAlpha.pause') + @player.onStateChange data: YT.PlayerState.ENDED + + it 'pause the video control', -> + expect(@player.control.pause).toHaveBeenCalled() + + it 'pause the video caption', -> + expect(@player.caption.pause).toHaveBeenCalled() + + describe 'onSeek', -> + conf = [{ + desc : 'check if seek_video is logged with slide_seek type', + type: 'slide_seek', + obj: 'progressSlider' + },{ + desc : 'check if seek_video is logged with caption_seek type', + type: 'caption_seek', + obj: 'caption' + }] + + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + spyOn window, 'clearInterval' + @player.player.interval = 100 + spyOn @player, 'updatePlayTime' + spyOn @video, 'log' + + $.each conf, (key, value) -> + it value.desc, -> + type = value.type + old_time = 0 + new_time = 60 + $(@player[value.obj]).trigger value.type, new_time + expect(@video.log).toHaveBeenCalledWith 'seek_video', + old_time: old_time + new_time: new_time + type: value.type + + it 'seek the player', -> + $(@player.progressSlider).trigger 'slide_seek', 60 + expect(@player.player.seekTo).toHaveBeenCalledWith 60, true + + it 'call updatePlayTime on player', -> + $(@player.progressSlider).trigger 'slide_seek', 60 + expect(@player.updatePlayTime).toHaveBeenCalledWith 60 + + describe 'when the player is playing', -> + beforeEach -> + $(@player.progressSlider).trigger 'slide_seek', 60 + @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING + @player.onSeek {}, 60 + + it 'reset the update interval', -> + expect(window.clearInterval).toHaveBeenCalledWith 100 + + describe 'when the player is not playing', -> + beforeEach -> + $(@player.progressSlider).trigger 'slide_seek', 60 + @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED + @player.onSeek {}, 60 + + it 'set the current time', -> + expect(@player.currentTime).toEqual 60 + + describe 'onSpeedChange', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + @player.currentTime = 60 + spyOn @player, 'updatePlayTime' + spyOn(@video, 'setSpeed').andCallThrough() + spyOn(@video, 'log') + + describe 'always', -> + beforeEach -> + @player.onSpeedChange {}, '0.75', false + + it 'check if speed_change_video is logged', -> + expect(@video.log).toHaveBeenCalledWith 'speed_change_video', + currentTime: @player.currentTime + old_speed: '1.0' + new_speed: '0.75' + + it 'convert the current time to the new speed', -> + expect(@player.currentTime).toEqual '80.000' + + it 'set video speed to the new speed', -> + expect(@video.setSpeed).toHaveBeenCalledWith '0.75', false + + it 'tell video caption that the speed has changed', -> + expect(@player.caption.currentSpeed).toEqual '0.75' + + describe 'when the video is playing', -> + beforeEach -> + @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING + @player.onSpeedChange {}, '0.75' + + it 'load the video', -> + expect(@player.player.loadVideoById).toHaveBeenCalledWith 'slowerSpeedYoutubeId', '80.000' + + it 'trigger updatePlayTime event', -> + expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000' + + describe 'when the video is not playing', -> + beforeEach -> + @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED + @player.onSpeedChange {}, '0.75' + + it 'cue the video', -> + expect(@player.player.cueVideoById).toHaveBeenCalledWith 'slowerSpeedYoutubeId', '80.000' + + it 'trigger updatePlayTime event', -> + expect(@player.updatePlayTime).toHaveBeenCalledWith '80.000' + + describe 'onVolumeChange', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + @player.onVolumeChange undefined, 60 + + it 'set the volume on player', -> + expect(@player.player.setVolume).toHaveBeenCalledWith 60 + + describe 'update', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + spyOn @player, 'updatePlayTime' + + describe 'when the current time is unavailable from the player', -> + beforeEach -> + @player.player.getCurrentTime.andReturn undefined + @player.update() + + it 'does not trigger updatePlayTime event', -> + expect(@player.updatePlayTime).not.toHaveBeenCalled() + + describe 'when the current time is available from the player', -> + beforeEach -> + @player.player.getCurrentTime.andReturn 60 + @player.update() + + it 'trigger updatePlayTime event', -> + expect(@player.updatePlayTime).toHaveBeenCalledWith(60) + + describe 'updatePlayTime', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + spyOn(@video, 'getDuration').andReturn 1800 + @player.caption.updatePlayTime = jasmine.createSpy('VideoCaptionAlpha.updatePlayTime') + @player.progressSlider.updatePlayTime = jasmine.createSpy('VideoProgressSliderAlpha.updatePlayTime') + @player.updatePlayTime 60 + + it 'update the video playback time', -> + expect($('.vidtime')).toHaveHtml '1:00 / 30:00' + + it 'update the playback time on caption', -> + expect(@player.caption.updatePlayTime).toHaveBeenCalledWith 60 + + it 'update the playback time on progress slider', -> + expect(@player.progressSlider.updatePlayTime).toHaveBeenCalledWith 60, 1800 + + describe 'toggleFullScreen', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + @player.caption.resize = jasmine.createSpy('VideoCaptionAlpha.resize') + + describe 'when the video player is not full screen', -> + beforeEach -> + spyOn @video, 'log' + @player.el.removeClass 'fullscreen' + @player.toggleFullScreen(jQuery.Event("click")) + + it 'log the fullscreen event', -> + expect(@video.log).toHaveBeenCalledWith 'fullscreen', + currentTime: @player.currentTime + + it 'replace the full screen button tooltip', -> + expect($('.add-fullscreen')).toHaveAttr 'title', 'Exit fill browser' + + it 'add the fullscreen class', -> + expect(@player.el).toHaveClass 'fullscreen' + + it 'tell VideoCaption to resize', -> + expect(@player.caption.resize).toHaveBeenCalled() + + describe 'when the video player already full screen', -> + beforeEach -> + spyOn @video, 'log' + @player.el.addClass 'fullscreen' + @player.toggleFullScreen(jQuery.Event("click")) + + it 'log the not_fullscreen event', -> + expect(@video.log).toHaveBeenCalledWith 'not_fullscreen', + currentTime: @player.currentTime + + it 'replace the full screen button tooltip', -> + expect($('.add-fullscreen')).toHaveAttr 'title', 'Fill browser' + + it 'remove exit full screen button', -> + expect(@player.el).not.toContain 'a.exit' + + it 'remove the fullscreen class', -> + expect(@player.el).not.toHaveClass 'fullscreen' + + it 'tell VideoCaption to resize', -> + expect(@player.caption.resize).toHaveBeenCalled() + + describe 'play', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + + describe 'when the player is not ready', -> + beforeEach -> + @player.player.playVideo = undefined + @player.play() + + it 'does nothing', -> + expect(@player.player.playVideo).toBeUndefined() + + describe 'when the player is ready', -> + beforeEach -> + @player.player.playVideo.andReturn true + @player.play() + + it 'delegate to the Youtube player', -> + expect(@player.player.playVideo).toHaveBeenCalled() + + describe 'isPlaying', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + + describe 'when the video is playing', -> + beforeEach -> + @player.player.getPlayerState.andReturn YT.PlayerState.PLAYING + + it 'return true', -> + expect(@player.isPlaying()).toBeTruthy() + + describe 'when the video is not playing', -> + beforeEach -> + @player.player.getPlayerState.andReturn YT.PlayerState.PAUSED + + it 'return false', -> + expect(@player.isPlaying()).toBeFalsy() + + describe 'pause', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + @player.pause() + + it 'delegate to the Youtube player', -> + expect(@player.player.pauseVideo).toHaveBeenCalled() + + describe 'duration', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + spyOn @video, 'getDuration' + @player.duration() + + it 'delegate to the video', -> + expect(@video.getDuration).toHaveBeenCalled() + + describe 'currentSpeed', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + @video.speed = '3.0' + + it 'delegate to the video', -> + expect(@player.currentSpeed()).toEqual '3.0' + + describe 'volume', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @, [], false + $('.video').append $('
') + @player = new VideoPlayerAlpha video: @video + @player.player.getVolume.andReturn 42 + + describe 'without value', -> + it 'return current volume', -> + expect(@player.volume()).toEqual 42 + + describe 'with value', -> + it 'set player volume', -> + @player.volume(60) + expect(@player.player.setVolume).toHaveBeenCalledWith(60) diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_progress_slider_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_progress_slider_spec.coffee new file mode 100644 index 0000000000..dd787aefbb --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_progress_slider_spec.coffee @@ -0,0 +1,165 @@ +describe 'VideoProgressSliderAlpha', -> + beforeEach -> + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + + describe 'constructor', -> + describe 'on a non-touch based device', -> + beforeEach -> + spyOn($.fn, 'slider').andCallThrough() + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + + it 'build the slider', -> + expect(@progressSlider.slider).toBe '.slider' + expect($.fn.slider).toHaveBeenCalledWith + range: 'min' + change: @progressSlider.onChange + slide: @progressSlider.onSlide + stop: @progressSlider.onStop + + it 'build the seek handle', -> + expect(@progressSlider.handle).toBe '.slider .ui-slider-handle' + expect($.fn.qtip).toHaveBeenCalledWith + content: "0:00" + position: + my: 'bottom center' + at: 'top center' + container: @progressSlider.handle + hide: + delay: 700 + style: + classes: 'ui-tooltip-slider' + widget: true + + describe 'on a touch-based device', -> + beforeEach -> + window.onTouchBasedDevice.andReturn true + spyOn($.fn, 'slider').andCallThrough() + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + + it 'does not build the slider', -> + expect(@progressSlider.slider).toBeUndefined + expect($.fn.slider).not.toHaveBeenCalled() + + describe 'play', -> + beforeEach -> + spyOn(VideoProgressSliderAlpha.prototype, 'buildSlider').andCallThrough() + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + + describe 'when the slider was already built', -> + + beforeEach -> + @progressSlider.play() + + it 'does not build the slider', -> + expect(@progressSlider.buildSlider.calls.length).toEqual 1 + + describe 'when the slider was not already built', -> + beforeEach -> + spyOn($.fn, 'slider').andCallThrough() + @progressSlider.slider = null + @progressSlider.play() + + it 'build the slider', -> + expect(@progressSlider.slider).toBe '.slider' + expect($.fn.slider).toHaveBeenCalledWith + range: 'min' + change: @progressSlider.onChange + slide: @progressSlider.onSlide + stop: @progressSlider.onStop + + it 'build the seek handle', -> + expect(@progressSlider.handle).toBe '.ui-slider-handle' + expect($.fn.qtip).toHaveBeenCalledWith + content: "0:00" + position: + my: 'bottom center' + at: 'top center' + container: @progressSlider.handle + hide: + delay: 700 + style: + classes: 'ui-tooltip-slider' + widget: true + + describe 'updatePlayTime', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + + describe 'when frozen', -> + beforeEach -> + spyOn($.fn, 'slider').andCallThrough() + @progressSlider.frozen = true + @progressSlider.updatePlayTime 20, 120 + + it 'does not update the slider', -> + expect($.fn.slider).not.toHaveBeenCalled() + + describe 'when not frozen', -> + beforeEach -> + spyOn($.fn, 'slider').andCallThrough() + @progressSlider.frozen = false + @progressSlider.updatePlayTime 20, 120 + + it 'update the max value of the slider', -> + expect($.fn.slider).toHaveBeenCalledWith 'option', 'max', 120 + + it 'update current value of the slider', -> + expect($.fn.slider).toHaveBeenCalledWith 'value', 20 + + describe 'onSlide', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + spyOnEvent @progressSlider, 'slide_seek' + @progressSlider.onSlide {}, value: 20 + + it 'freeze the slider', -> + expect(@progressSlider.frozen).toBeTruthy() + + it 'update the tooltip', -> + expect($.fn.qtip).toHaveBeenCalled() + + it 'trigger seek event', -> + expect('slide_seek').toHaveBeenTriggeredOn @progressSlider + expect(@player.currentTime).toEqual 20 + + describe 'onChange', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + @progressSlider.onChange {}, value: 20 + + it 'update the tooltip', -> + expect($.fn.qtip).toHaveBeenCalled() + + describe 'onStop', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + spyOnEvent @progressSlider, 'slide_seek' + @progressSlider.onStop {}, value: 20 + + it 'freeze the slider', -> + expect(@progressSlider.frozen).toBeTruthy() + + it 'trigger seek event', -> + expect('slide_seek').toHaveBeenTriggeredOn @progressSlider + expect(@player.currentTime).toEqual 20 + + it 'set timeout to unfreeze the slider', -> + expect(window.setTimeout).toHaveBeenCalledWith jasmine.any(Function), 200 + window.setTimeout.mostRecentCall.args[0]() + expect(@progressSlider.frozen).toBeFalsy() + + describe 'updateTooltip', -> + beforeEach -> + @player = jasmine.stubVideoPlayerAlpha @ + @progressSlider = @player.progressSlider + @progressSlider.updateTooltip 90 + + it 'set the tooltip value', -> + expect($.fn.qtip).toHaveBeenCalledWith 'option', 'content.text', '1:30' diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_speed_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_speed_control_spec.coffee new file mode 100644 index 0000000000..ca4bfe815a --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_speed_control_spec.coffee @@ -0,0 +1,91 @@ +describe 'VideoSpeedControlAlpha', -> + beforeEach -> + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + jasmine.stubVideoPlayerAlpha @ + $('.speeds').remove() + + describe 'constructor', -> + describe 'always', -> + beforeEach -> + @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' + + it 'add the video speed control to player', -> + secondaryControls = $('.secondary-controls') + li = secondaryControls.find('.video_speeds li') + expect(secondaryControls).toContain '.speeds' + expect(secondaryControls).toContain '.video_speeds' + expect(secondaryControls.find('p.active').text()).toBe '1.0x' + expect(li.filter('.active')).toHaveData 'speed', @speedControl.currentSpeed + expect(li.length).toBe @speedControl.speeds.length + $.each li.toArray().reverse(), (index, link) => + expect($(link)).toHaveData 'speed', @speedControl.speeds[index] + expect($(link).find('a').text()).toBe @speedControl.speeds[index] + 'x' + + it 'bind to change video speed link', -> + expect($('.video_speeds a')).toHandleWith 'click', @speedControl.changeVideoSpeed + + describe 'when running on touch based device', -> + beforeEach -> + window.onTouchBasedDevice.andReturn true + $('.speeds').removeClass 'open' + @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' + + it 'open the speed toggle on click', -> + $('.speeds').click() + expect($('.speeds')).toHaveClass 'open' + $('.speeds').click() + expect($('.speeds')).not.toHaveClass 'open' + + describe 'when running on non-touch based device', -> + beforeEach -> + $('.speeds').removeClass 'open' + @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' + + it 'open the speed toggle on hover', -> + $('.speeds').mouseenter() + expect($('.speeds')).toHaveClass 'open' + $('.speeds').mouseleave() + expect($('.speeds')).not.toHaveClass 'open' + + it 'close the speed toggle on mouse out', -> + $('.speeds').mouseenter().mouseleave() + expect($('.speeds')).not.toHaveClass 'open' + + it 'close the speed toggle on click', -> + $('.speeds').mouseenter().click() + expect($('.speeds')).not.toHaveClass 'open' + + describe 'changeVideoSpeed', -> + beforeEach -> + @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' + @video.setSpeed '1.0' + + describe 'when new speed is the same', -> + beforeEach -> + spyOnEvent @speedControl, 'speedChange' + $('li[data-speed="1.0"] a').click() + + it 'does not trigger speedChange event', -> + expect('speedChange').not.toHaveBeenTriggeredOn @speedControl + + describe 'when new speed is not the same', -> + beforeEach -> + @newSpeed = null + $(@speedControl).bind 'speedChange', (event, newSpeed) => @newSpeed = newSpeed + spyOnEvent @speedControl, 'speedChange' + $('li[data-speed="0.75"] a').click() + + it 'trigger speedChange event', -> + expect('speedChange').toHaveBeenTriggeredOn @speedControl + expect(@newSpeed).toEqual 0.75 + + describe 'onSpeedChange', -> + beforeEach -> + @speedControl = new VideoSpeedControlAlpha el: $('.secondary-controls'), speeds: @video.speeds, currentSpeed: '1.0' + $('li[data-speed="1.0"] a').addClass 'active' + @speedControl.setSpeed '0.75' + + it 'set the new speed as active', -> + expect($('.video_speeds li[data-speed="1.0"]')).not.toHaveClass 'active' + expect($('.video_speeds li[data-speed="0.75"]')).toHaveClass 'active' + expect($('.speeds p.active')).toHaveHtml '0.75x' diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_volume_control_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_volume_control_spec.coffee new file mode 100644 index 0000000000..4bb9f1cbf8 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display/video_volume_control_spec.coffee @@ -0,0 +1,94 @@ +describe 'VideoVolumeControlAlpha', -> + beforeEach -> + jasmine.stubVideoPlayerAlpha @ + $('.volume').remove() + + describe 'constructor', -> + beforeEach -> + spyOn($.fn, 'slider') + @volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls') + + it 'initialize currentVolume to 100', -> + expect(@volumeControl.currentVolume).toEqual 100 + + it 'render the volume control', -> + expect($('.secondary-controls').html()).toContain """ +
+ +
+
+
+
+ """ + + it 'create the slider', -> + expect($.fn.slider).toHaveBeenCalledWith + orientation: "vertical" + range: "min" + min: 0 + max: 100 + value: 100 + change: @volumeControl.onChange + slide: @volumeControl.onChange + + it 'bind the volume control', -> + expect($('.volume>a')).toHandleWith 'click', @volumeControl.toggleMute + + expect($('.volume')).not.toHaveClass 'open' + $('.volume').mouseenter() + expect($('.volume')).toHaveClass 'open' + $('.volume').mouseleave() + expect($('.volume')).not.toHaveClass 'open' + + describe 'onChange', -> + beforeEach -> + spyOnEvent @volumeControl, 'volumeChange' + @newVolume = undefined + @volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls') + $(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume + + describe 'when the new volume is more than 0', -> + beforeEach -> + @volumeControl.onChange undefined, value: 60 + + it 'set the player volume', -> + expect(@newVolume).toEqual 60 + + it 'remote muted class', -> + expect($('.volume')).not.toHaveClass 'muted' + + describe 'when the new volume is 0', -> + beforeEach -> + @volumeControl.onChange undefined, value: 0 + + it 'set the player volume', -> + expect(@newVolume).toEqual 0 + + it 'add muted class', -> + expect($('.volume')).toHaveClass 'muted' + + describe 'toggleMute', -> + beforeEach -> + @newVolume = undefined + @volumeControl = new VideoVolumeControlAlpha el: $('.secondary-controls') + $(@volumeControl).bind 'volumeChange', (event, volume) => @newVolume = volume + + describe 'when the current volume is more than 0', -> + beforeEach -> + @volumeControl.currentVolume = 60 + @volumeControl.toggleMute() + + it 'save the previous volume', -> + expect(@volumeControl.previousVolume).toEqual 60 + + it 'set the player volume', -> + expect(@newVolume).toEqual 0 + + describe 'when the current volume is 0', -> + beforeEach -> + @volumeControl.currentVolume = 0 + @volumeControl.previousVolume = 60 + @volumeControl.toggleMute() + + it 'set the player volume to previous volume', -> + expect(@newVolume).toEqual 60 diff --git a/common/lib/xmodule/xmodule/js/spec/videoalpha/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/videoalpha/display_spec.coffee new file mode 100644 index 0000000000..3715c3d813 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/videoalpha/display_spec.coffee @@ -0,0 +1,286 @@ +describe 'VideoAlpha', -> + metadata = + slowerSpeedYoutubeId: + id: @slowerSpeedYoutubeId + duration: 300 + normalSpeedYoutubeId: + id: @normalSpeedYoutubeId + duration: 200 + + beforeEach -> + jasmine.stubRequests() + window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn false + @videosDefinition = '0.75:slowerSpeedYoutubeId,1.0:normalSpeedYoutubeId' + @slowerSpeedYoutubeId = 'slowerSpeedYoutubeId' + @normalSpeedYoutubeId = 'normalSpeedYoutubeId' + + afterEach -> + window.OldVideoPlayerAlpha = undefined + window.onYouTubePlayerAPIReady = undefined + window.onHTML5PlayerAPIReady = undefined + + describe 'constructor', -> + describe 'YT', -> + beforeEach -> + loadFixtures 'videoalpha.html' + @stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha') + $.cookie.andReturn '0.75' + + describe 'by default', -> + beforeEach -> + spyOn(window.VideoAlpha.prototype, 'fetchMetadata').andCallFake -> + @metadata = metadata + @video = new VideoAlpha '#example', @videosDefinition + + it 'check videoType', -> + expect(@video.videoType).toEqual('youtube') + + it 'reset the current video player', -> + expect(window.OldVideoPlayerAlpha).toBeUndefined() + + it 'set the elements', -> + expect(@video.el).toBe '#video_id' + + it 'parse the videos', -> + expect(@video.videos).toEqual + '0.75': @slowerSpeedYoutubeId + '1.0': @normalSpeedYoutubeId + + it 'fetch the video metadata', -> + expect(@video.fetchMetadata).toHaveBeenCalled + expect(@video.metadata).toEqual metadata + + it 'parse available video speeds', -> + expect(@video.speeds).toEqual ['0.75', '1.0'] + + it 'set current video speed via cookie', -> + expect(@video.speed).toEqual '0.75' + + it 'store a reference for this video player in the element', -> + expect($('.video').data('video')).toEqual @video + + describe 'when the Youtube API is already available', -> + beforeEach -> + @originalYT = window.YT + window.YT = { Player: true } + spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha) + @video = new VideoAlpha '#example', @videosDefinition + + afterEach -> + window.YT = @originalYT + + it 'create the Video Player', -> + expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video) + expect(@video.player).toEqual @stubVideoPlayerAlpha + + describe 'when the Youtube API is not ready', -> + beforeEach -> + @originalYT = window.YT + window.YT = {} + @video = new VideoAlpha '#example', @videosDefinition + + afterEach -> + window.YT = @originalYT + + it 'set the callback on the window object', -> + expect(window.onYouTubePlayerAPIReady).toEqual jasmine.any(Function) + + describe 'when the Youtube API becoming ready', -> + beforeEach -> + @originalYT = window.YT + window.YT = {} + spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha) + @video = new VideoAlpha '#example', @videosDefinition + window.onYouTubePlayerAPIReady() + + afterEach -> + window.YT = @originalYT + + it 'create the Video Player for all video elements', -> + expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video) + expect(@video.player).toEqual @stubVideoPlayerAlpha + + describe 'HTML5', -> + beforeEach -> + loadFixtures 'videoalpha_html5.html' + @stubVideoPlayerAlpha = jasmine.createSpy('VideoPlayerAlpha') + $.cookie.andReturn '0.75' + + describe 'by default', -> + beforeEach -> + @originalHTML5 = window.HTML5Video.Player + window.HTML5Video.Player = undefined + @video = new VideoAlpha '#example', @videosDefinition + + afterEach -> + window.HTML5Video.Player = @originalHTML5 + + it 'check videoType', -> + expect(@video.videoType).toEqual('html5') + + it 'reset the current video player', -> + expect(window.OldVideoPlayerAlpha).toBeUndefined() + + it 'set the elements', -> + expect(@video.el).toBe '#video_id' + + it 'parse the videos if subtitles exist', -> + sub = 'test_name_of_the_subtitles' + expect(@video.videos).toEqual + '0.75': sub + '1.0': sub + '1.25': sub + '1.5': sub + + it 'parse the videos if subtitles doesn\'t exist', -> + $('#example').find('.video').data('sub', '') + @video = new VideoAlpha '#example', @videosDefinition + sub = '' + expect(@video.videos).toEqual + '0.75': sub + '1.0': sub + '1.25': sub + '1.5': sub + + it 'parse Html5 sources', -> + html5Sources = + mp4: 'test.mp4' + webm: 'test.webm' + ogg: 'test.ogv' + expect(@video.html5Sources).toEqual html5Sources + + it 'parse available video speeds', -> + speeds = jasmine.stubbedHtml5Speeds + expect(@video.speeds).toEqual speeds + + it 'set current video speed via cookie', -> + expect(@video.speed).toEqual '0.75' + + it 'store a reference for this video player in the element', -> + expect($('.video').data('video')).toEqual @video + + describe 'when the HTML5 API is already available', -> + beforeEach -> + @originalHTML5Video = window.HTML5Video + window.HTML5Video = { Player: true } + spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha) + @video = new VideoAlpha '#example', @videosDefinition + + afterEach -> + window.HTML5Video = @originalHTML5Video + + it 'create the Video Player', -> + expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video) + expect(@video.player).toEqual @stubVideoPlayerAlpha + + describe 'when the HTML5 API is not ready', -> + beforeEach -> + @originalHTML5Video = window.HTML5Video + window.HTML5Video = {} + @video = new VideoAlpha '#example', @videosDefinition + + afterEach -> + window.HTML5Video = @originalHTML5Video + + it 'set the callback on the window object', -> + expect(window.onHTML5PlayerAPIReady).toEqual jasmine.any(Function) + + describe 'when the HTML5 API becoming ready', -> + beforeEach -> + @originalHTML5Video = window.HTML5Video + window.HTML5Video = {} + spyOn(window, 'VideoPlayerAlpha').andReturn(@stubVideoPlayerAlpha) + @video = new VideoAlpha '#example', @videosDefinition + window.onHTML5PlayerAPIReady() + + afterEach -> + window.HTML5Video = @originalHTML5Video + + it 'create the Video Player for all video elements', -> + expect(window.VideoPlayerAlpha).toHaveBeenCalledWith(video: @video) + expect(@video.player).toEqual @stubVideoPlayerAlpha + + describe 'youtubeId', -> + beforeEach -> + loadFixtures 'videoalpha.html' + $.cookie.andReturn '1.0' + @video = new VideoAlpha '#example', @videosDefinition + + describe 'with speed', -> + it 'return the video id for given speed', -> + expect(@video.youtubeId('0.75')).toEqual @slowerSpeedYoutubeId + expect(@video.youtubeId('1.0')).toEqual @normalSpeedYoutubeId + + describe 'without speed', -> + it 'return the video id for current speed', -> + expect(@video.youtubeId()).toEqual @normalSpeedYoutubeId + + describe 'setSpeed', -> + describe 'YT', -> + beforeEach -> + loadFixtures 'videoalpha.html' + @video = new VideoAlpha '#example', @videosDefinition + + describe 'when new speed is available', -> + beforeEach -> + @video.setSpeed '0.75' + + it 'set new speed', -> + expect(@video.speed).toEqual '0.75' + + it 'save setting for new speed', -> + expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/' + + describe 'when new speed is not available', -> + beforeEach -> + @video.setSpeed '1.75' + + it 'set speed to 1.0x', -> + expect(@video.speed).toEqual '1.0' + + describe 'HTML5', -> + beforeEach -> + loadFixtures 'videoalpha_html5.html' + @video = new VideoAlpha '#example', @videosDefinition + + describe 'when new speed is available', -> + beforeEach -> + @video.setSpeed '0.75' + + it 'set new speed', -> + expect(@video.speed).toEqual '0.75' + + it 'save setting for new speed', -> + expect($.cookie).toHaveBeenCalledWith 'video_speed', '0.75', expires: 3650, path: '/' + + describe 'when new speed is not available', -> + beforeEach -> + @video.setSpeed '1.75' + + it 'set speed to 1.0x', -> + expect(@video.speed).toEqual '1.0' + + describe 'getDuration', -> + beforeEach -> + loadFixtures 'videoalpha.html' + @video = new VideoAlpha '#example', @videosDefinition + + it 'return duration for current video', -> + expect(@video.getDuration()).toEqual 200 + + describe 'log', -> + beforeEach -> + loadFixtures 'videoalpha.html' + @video = new VideoAlpha '#example', @videosDefinition + spyOn Logger, 'log' + @video.log 'someEvent', { + currentTime: 25, + speed: '1.0' + } + + it 'call the logger with valid extra parameters', -> + expect(Logger.log).toHaveBeenCalledWith 'someEvent', + id: 'id' + code: @normalSpeedYoutubeId + currentTime: 25 + speed: '1.0' diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee index ff61a9a459..317979bb4d 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_caption.coffee @@ -37,7 +37,7 @@ class @VideoCaptionAlpha extends SubviewAlpha @loaded = true if onTouchBasedDevice() - $('.subtitles li').html "Caption will be displayed when you start playing the video." + $('.subtitles').html "
  • Caption will be displayed when you start playing the video.
  • " else @renderCaption() diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee index 31dd115fa6..7019386e04 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_player.coffee @@ -6,7 +6,7 @@ class @VideoPlayerAlpha extends SubviewAlpha # we must pause the player (stop setInterval() method). if (window.OldVideoPlayerAlpha) and (window.OldVideoPlayerAlpha.onPause) window.OldVideoPlayerAlpha.onPause() - window.OldVideoPlayerAlpha = this + window.OldVideoPlayerAlpha = @ if @video.videoType is 'youtube' @PlayerState = YT.PlayerState @@ -29,7 +29,7 @@ class @VideoPlayerAlpha extends SubviewAlpha $(@progressSlider).bind('slide_seek', @onSeek) if @volumeControl $(@volumeControl).bind('volumeChange', @onVolumeChange) - $(document).keyup @bindExitFullScreen + $(document.documentElement).keyup @bindExitFullScreen @$('.add-fullscreen').click @toggleFullScreen @addToolTip() unless onTouchBasedDevice() @@ -114,7 +114,7 @@ class @VideoPlayerAlpha extends SubviewAlpha @video.log 'load_video' if @video.videoType is 'html5' @player.setPlaybackRate @video.speed - if not onTouchBasedDevice() and $('.video:first').data('autoplay') is 'True' + if not onTouchBasedDevice() and $('.video:first').data('autoplay') isnt 'False' $('.video-load-complete:first').data('video').player.play() onStateChange: (event) => @@ -308,7 +308,7 @@ class @VideoPlayerAlpha extends SubviewAlpha @player.pauseVideo() if @player.pauseVideo duration: -> - duration = @player.getDuration() + duration = @player.getDuration() if @player.getDuration if isFinite(duration) is false duration = @video.getDuration() duration diff --git a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee index e9ed9923b0..5197c4938f 100644 --- a/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee +++ b/common/lib/xmodule/xmodule/js/src/videoalpha/display/video_progress_slider.coffee @@ -12,7 +12,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha @buildHandle() buildHandle: -> - @handle = @$('.slider .ui-slider-handle') + @handle = @$('.ui-slider-handle') @handle.qtip content: "#{Time.format(@slider.slider('value'))}" position: @@ -43,7 +43,7 @@ class @VideoProgressSliderAlpha extends SubviewAlpha onStop: (event, ui) => @frozen = true - $(@).trigger('seek', ui.value) + $(@).trigger('slide_seek', ui.value) setTimeout (=> @frozen = false), 200 updateTooltip: (value)-> diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index dba8bbd0b4..1c25b272a3 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,6 +1,6 @@ --- metadata: - display_name: Video Alpha 1 + display_name: Video Alpha version: 1 data: | diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py index ff17f88dfc..6fb331b3cf 100644 --- a/common/lib/xmodule/xmodule/tests/test_logic.py +++ b/common/lib/xmodule/xmodule/tests/test_logic.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- +# pylint: disable=W0232 """Test for Xmodule functional logic.""" import json import unittest -from lxml import etree - from xmodule.poll_module import PollDescriptor from xmodule.conditional_module import ConditionalDescriptor from xmodule.word_cloud_module import WordCloudDescriptor -from xmodule.videoalpha_module import VideoAlphaDescriptor +from xmodule.tests import test_system class PostData: """Class which emulate postdata.""" @@ -17,6 +16,7 @@ class PostData: self.dict_data = dict_data def getlist(self, key): + """Get data by key from `self.dict_data`.""" return self.dict_data.get(key) @@ -27,9 +27,10 @@ class LogicTest(unittest.TestCase): def setUp(self): class EmptyClass: + """Empty object.""" pass - self.system = None + self.system = test_system() self.descriptor = EmptyClass() self.xmodule_class = self.descriptor_class.module_class @@ -40,10 +41,12 @@ class LogicTest(unittest.TestCase): ) def ajax_request(self, dispatch, get): + """Call Xmodule.handle_ajax.""" return json.loads(self.xmodule.handle_ajax(dispatch, get)) class PollModuleTest(LogicTest): + """Logic tests for Poll Xmodule.""" descriptor_class = PollDescriptor raw_model_data = { 'poll_answers': {'Yes': 1, 'Dont_know': 0, 'No': 0}, @@ -69,6 +72,7 @@ class PollModuleTest(LogicTest): class ConditionalModuleTest(LogicTest): + """Logic tests for Conditional Xmodule.""" descriptor_class = ConditionalDescriptor def test_ajax_request(self): @@ -83,6 +87,7 @@ class ConditionalModuleTest(LogicTest): class WordCloudModuleTest(LogicTest): + """Logic tests for Word Cloud Xmodule.""" descriptor_class = WordCloudDescriptor raw_model_data = { 'all_words': {'cat': 10, 'dog': 5, 'mom': 1, 'dad': 2}, @@ -91,8 +96,6 @@ class WordCloudModuleTest(LogicTest): } def test_bad_ajax_request(self): - - # TODO: move top global test. Formalize all our Xmodule errors. response = self.ajax_request('bad_dispatch', {}) self.assertDictEqual(response, { 'status': 'fail', @@ -118,34 +121,6 @@ class WordCloudModuleTest(LogicTest): {'text': 'cat', 'size': 12, 'percent': 54.0}] ) - self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) ) - - -class VideoAlphaModuleTest(LogicTest): - descriptor_class = VideoAlphaDescriptor - - raw_model_data = { - 'data': '' - } - - def test_get_timeframe_no_parameters(self): - xmltree = etree.fromstring('test') - output = self.xmodule._get_timeframe(xmltree) - self.assertEqual(output, ('', '')) - - def test_get_timeframe_with_one_parameter(self): - xmltree = etree.fromstring( - 'test' - ) - output = self.xmodule._get_timeframe(xmltree) - self.assertEqual(output, (247, '')) - - def test_get_timeframe_with_two_parameters(self): - xmltree = etree.fromstring( - '''test''' - ) - output = self.xmodule._get_timeframe(xmltree) - self.assertEqual(output, (247, 47079)) + self.assertEqual( + 100.0, + sum(i['percent'] for i in response['top_words'])) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 43de021799..a64e094a58 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -1,3 +1,15 @@ +# pylint: disable=W0223 +"""VideoAlpha is ungraded Xmodule for support video content. +It's new improved video module, which support additional feature: + +- Can play non-YouTube video sources via in-browser HTML5 video player. +- YouTube defaults to HTML5 mode from the start. +- Speed changes in both YouTube and non-YouTube videos happen via +in-browser HTML5 video method (when in HTML5 mode). +- Navigational subtitles can be disabled altogether via an attribute +in XML. +""" + import json import logging @@ -21,6 +33,7 @@ log = logging.getLogger(__name__) class VideoAlphaFields(object): + """Fields for `VideoAlphaModule` and `VideoAlphaDescriptor`.""" data = String(help="XML data for the problem", scope=Scope.content) position = Integer(help="Current position in the video", scope=Scope.user_state, default=0) display_name = String(help="Display name for this module", scope=Scope.settings) @@ -68,7 +81,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): 'ogv': self._get_source(xmltree, ['ogv']), } self.track = self._get_track(xmltree) - self.start_time, self.end_time = self._get_timeframe(xmltree) + self.start_time, self.end_time = self.get_timeframe(xmltree) def _get_source(self, xmltree, exts=None): """Find the first valid source, which ends with one of `exts`.""" @@ -77,7 +90,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): return self._get_first_external(xmltree, 'source', condition) def _get_track(self, xmltree): - # find the first valid track + """Find the first valid track.""" return self._get_first_external(xmltree, 'track') def _get_first_external(self, xmltree, tag, condition=bool): @@ -93,39 +106,33 @@ class VideoAlphaModule(VideoAlphaFields, XModule): break return result - def _get_timeframe(self, xmltree): + def get_timeframe(self, xmltree): """ Converts 'start_time' and 'end_time' parameters in video tag to seconds. If there are no parameters, returns empty string. """ - def parse_time(s): + def parse_time(str_time): """Converts s in '12:34:45' format to seconds. If s is None, returns empty string""" - if s is None: + if str_time is None: return '' else: - x = time.strptime(s, '%H:%M:%S') + obj_time = time.strptime(str_time, '%H:%M:%S') return datetime.timedelta( - hours=x.tm_hour, - minutes=x.tm_min, - seconds=x.tm_sec + hours=obj_time.tm_hour, + minutes=obj_time.tm_min, + seconds=obj_time.tm_sec ).total_seconds() return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time')) def handle_ajax(self, dispatch, get): - """Handle ajax calls to this video. - TODO (vshnayder): This is not being called right now, so the - position is not being saved. - """ + """This is not being called right now and we raise 404 error.""" log.debug(u"GET {0}".format(get)) log.debug(u"DISPATCH {0}".format(dispatch)) - if dispatch == 'goto_position': - self.position = int(float(get['position'])) - log.info(u"NEW POSITION {0}".format(self.position)) - return json.dumps({'success': True}) raise Http404() def get_instance_state(self): + """Return information about state (position).""" return json.dumps({'position': self.position}) def get_html(self): @@ -143,7 +150,8 @@ class VideoAlphaModule(VideoAlphaFields, XModule): 'sources': self.sources, 'track': self.track, 'display_name': self.display_name_with_default, - # TODO (cpennington): This won't work when we move to data that isn't on the filesystem + # This won't work when we move to data that + # isn't on the filesystem 'data_dir': getattr(self, 'data_dir', None), 'caption_asset_path': caption_asset_path, 'show_captions': self.show_captions, @@ -154,5 +162,6 @@ class VideoAlphaModule(VideoAlphaFields, XModule): class VideoAlphaDescriptor(VideoAlphaFields, RawDescriptor): + """Descriptor for `VideoAlphaModule`.""" module_class = VideoAlphaModule template_dir_name = "videoalpha" diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py index cc53bf735a..1cb403018c 100644 --- a/lms/djangoapps/courseware/tests/__init__.py +++ b/lms/djangoapps/courseware/tests/__init__.py @@ -25,8 +25,8 @@ class BaseTestXmodule(ModuleStoreTestCase): """Base class for testing Xmodules with mongo store. This class prepares course and users for tests: - 1. create test course - 2. create, enrol and login users for this course + 1. create test course; + 2. create, enrol and login users for this course; Any xmodule should overwrite only next parameters for test: 1. TEMPLATE_NAME diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py new file mode 100644 index 0000000000..a6bff60acf --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_videoalpha_mongo.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +"""Video xmodule tests in mongo.""" + +from . import BaseTestXmodule +from .test_videoalpha_xml import SOURCE_XML +from django.conf import settings + + +class TestVideo(BaseTestXmodule): + """Integration tests: web client + mongo.""" + + TEMPLATE_NAME = "i4x://edx/templates/videoalpha/Video_Alpha" + DATA = SOURCE_XML + MODEL_DATA = { + 'data': DATA + } + + def test_handle_ajax_dispatch(self): + responses = { + user.username: self.clients[user.username].post( + self.get_url('whatever'), + {}, + HTTP_X_REQUESTED_WITH='XMLHttpRequest') + for user in self.users + } + + self.assertEqual( + set([ + response.status_code + for _, response in responses.items() + ]).pop(), + 404) + + def test_videoalpha_constructor(self): + """Make sure that all parameters extracted correclty from xml""" + + # `get_html` return only context, cause we + # overwrite `system.render_template` + context = self.item_module.get_html() + expected_context = { + 'data_dir': getattr(self, 'data_dir', None), + 'caption_asset_path': '/c4x/MITx/999/asset/subs_', + 'show_captions': self.item_module.show_captions, + 'display_name': self.item_module.display_name_with_default, + 'end': self.item_module.end_time, + 'id': self.item_module.location.html_id(), + 'sources': self.item_module.sources, + 'start': self.item_module.start_time, + 'sub': self.item_module.sub, + 'track': self.item_module.track, + 'youtube_streams': self.item_module.youtube_streams, + 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) + } + self.assertDictEqual(context, expected_context) diff --git a/lms/djangoapps/courseware/tests/test_videoalpha_xml.py b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py new file mode 100644 index 0000000000..44e0a7811a --- /dev/null +++ b/lms/djangoapps/courseware/tests/test_videoalpha_xml.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +"""Test for VideoAlpha Xmodule functional logic. +These tests data readed from xml, not from mongo. + +We have a ModuleStoreTestCase class defined in +common/lib/xmodule/xmodule/modulestore/tests/django_utils.py. +You can search for usages of this in the cms and lms tests for examples. +You use this so that it will do things like point the modulestore +setting to mongo, flush the contentstore before and after, load the +templates, etc. +You can then use the CourseFactory and XModuleItemFactory as defined in +common/lib/xmodule/xmodule/modulestore/tests/factories.py to create the +course, section, subsection, unit, etc. +""" + +import json +import unittest +from mock import Mock +from lxml import etree + +from django.conf import settings + +from xmodule.videoalpha_module import VideoAlphaDescriptor, VideoAlphaModule +from xmodule.modulestore import Location +from xmodule.tests import test_system +from xmodule.tests.test_logic import LogicTest + + +SOURCE_XML = """ + + + + + +""" + + +class VideoAlphaFactory(object): + """A helper class to create videoalpha modules with various parameters + for testing. + """ + + # tag that uses youtube videos + sample_problem_xml_youtube = SOURCE_XML + + @staticmethod + def create(): + """Method return VideoAlpha Xmodule instance.""" + location = Location(["i4x", "edX", "videoalpha", "default", + "SampleProblem1"]) + model_data = {'data': VideoAlphaFactory.sample_problem_xml_youtube} + + descriptor = Mock(weight="1") + + system = test_system() + system.render_template = lambda template, context: context + VideoAlphaModule.location = location + module = VideoAlphaModule(system, descriptor, model_data) + + return module + + +class VideoAlphaModuleTest(LogicTest): + """Tests for logic of VideoAlpha Xmodule.""" + + descriptor_class = VideoAlphaDescriptor + + raw_model_data = { + 'data': '' + } + + def test_get_timeframe_no_parameters(self): + xmltree = etree.fromstring('test') + output = self.xmodule.get_timeframe(xmltree) + self.assertEqual(output, ('', '')) + + def test_get_timeframe_with_one_parameter(self): + xmltree = etree.fromstring( + 'test' + ) + output = self.xmodule.get_timeframe(xmltree) + self.assertEqual(output, (247, '')) + + def test_get_timeframe_with_two_parameters(self): + xmltree = etree.fromstring( + '''test''' + ) + output = self.xmodule.get_timeframe(xmltree) + self.assertEqual(output, (247, 47079)) + + +class VideoAlphaModuleUnitTest(unittest.TestCase): + """Unit tests for VideoAlpha Xmodule.""" + + def test_videoalpha_constructor(self): + """Make sure that all parameters extracted correclty from xml""" + module = VideoAlphaFactory.create() + + # `get_html` return only context, cause we + # overwrite `system.render_template` + context = module.get_html() + expected_context = { + 'caption_asset_path': '/static/subs/', + 'sub': module.sub, + 'data_dir': getattr(self, 'data_dir', None), + 'display_name': module.display_name_with_default, + 'end': module.end_time, + 'start': module.start_time, + 'id': module.location.html_id(), + 'show_captions': module.show_captions, + 'sources': module.sources, + 'youtube_streams': module.youtube_streams, + 'track': module.track, + 'autoplay': settings.MITX_FEATURES.get('AUTOPLAY_VIDEOS', True) + } + self.assertDictEqual(context, expected_context) + + self.assertDictEqual( + json.loads(module.get_instance_state()), + {'position': 0}) diff --git a/test_root/data/videoalpha/gizmo.mp4 b/test_root/data/videoalpha/gizmo.mp4 new file mode 100644 index 0000000000..1fc478842f Binary files /dev/null and b/test_root/data/videoalpha/gizmo.mp4 differ diff --git a/test_root/data/videoalpha/gizmo.ogv b/test_root/data/videoalpha/gizmo.ogv new file mode 100644 index 0000000000..2c4a447f1f Binary files /dev/null and b/test_root/data/videoalpha/gizmo.ogv differ diff --git a/test_root/data/videoalpha/gizmo.webm b/test_root/data/videoalpha/gizmo.webm new file mode 100644 index 0000000000..95d5031a86 Binary files /dev/null and b/test_root/data/videoalpha/gizmo.webm differ