diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 80ab2a071b..a66cefdce9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,6 +5,14 @@ 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.
+Blades: Video player improvements:
+ - Disable edX controls on iPhone/iPod (native controls are used).
+ - Disable unsupported controls (volume, playback rate) on iPad/Android.
+ - Controls becomes visible after click on video or play placeholder to avoid
+ issues with YouTube API on iPad/Android.
+ - Captions becomes visible just after full initialization of video player.
+ - Fix blinking of captions after initialization of video player. BLD-206.
+
LMS: Fix answer distribution download for small courses. LMS-922, LMS-811
Blades: Add template for the zooming image in studio. BLD-206.
diff --git a/cms/djangoapps/contentstore/features/video.py b/cms/djangoapps/contentstore/features/video.py
index a293b13727..2a86222584 100644
--- a/cms/djangoapps/contentstore/features/video.py
+++ b/cms/djangoapps/contentstore/features/video.py
@@ -141,12 +141,13 @@ def the_youtube_video_is_shown(_step):
@step('Make sure captions are (.+)$')
def set_captions_visibility_state(_step, captions_state):
SELECTOR = '.closed .subtitles'
+ world.wait_for_visible('.hide-subtitles')
if captions_state == 'closed':
if not world.is_css_present(SELECTOR):
- world.browser.find_by_css('.hide-subtitles').click()
+ world.css_find('.hide-subtitles').click()
else:
if world.is_css_present(SELECTOR):
- world.browser.find_by_css('.hide-subtitles').click()
+ world.css_find('.hide-subtitles').click()
@step('I hover over button "([^"]*)"$')
diff --git a/cms/static/coffee/src/main.coffee b/cms/static/coffee/src/main.coffee
index fef7ce9871..5c835b27b2 100644
--- a/cms/static/coffee/src/main.coffee
+++ b/cms/static/coffee/src/main.coffee
@@ -9,7 +9,7 @@ define ["domReady", "jquery", "underscore.string", "backbone", "gettext",
window.CMS = window.CMS or {}
CMS.URL = CMS.URL or {}
window.onTouchBasedDevice = ->
- navigator.userAgent.match /iPhone|iPod|iPad/i
+ navigator.userAgent.match /iPhone|iPod|iPad|Android/i
_.extend CMS, Backbone.Events
Backbone.emulateHTTP = true
diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss
index 36318df11b..5f4e9d1063 100644
--- a/common/lib/xmodule/xmodule/css/video/display.scss
+++ b/common/lib/xmodule/xmodule/css/video/display.scss
@@ -2,6 +2,10 @@
margin-bottom: 30px;
}
+.is-hidden {
+ display: none;
+}
+
div.video {
@include clearfix();
background: #f3f3f3;
@@ -97,12 +101,35 @@ div.video {
}
}
+ .btn-play {
+ @include transform(translate(-50%, -50%));
+ position: absolute;
+ z-index: 1;
+ background: rgba(0, 0, 0, 0.7);
+ top: 50%;
+ left: 50%;
+ padding: 30px;
+ border-radius: 25%;
+
+ &:after{
+ content: '';
+ display: block;
+ width: 0px;
+ height: 0px;
+ border-style: solid;
+ border-width: 30px 0 30px 50px;
+ border-color: transparent transparent transparent #ffffff;
+ position: relative;
+ }
+ }
section.video-player {
overflow: hidden;
min-height: 300px;
- div {
+ > div {
+ height: 100%;
+
&.hidden {
display: none;
}
@@ -674,6 +701,7 @@ div.video {
width: 275px;
padding: 0 20px;
z-index: 0;
+ display: none;
}
}
@@ -764,6 +792,17 @@ div.video {
}
}
}
+
+ &.is-touch {
+ div.tc-wrapper {
+ article.video-wrapper {
+ object, iframe, video{
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+ }
}
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html
index e80bd3a0dd..a28d10422a 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video.html
@@ -3,7 +3,7 @@
+
+
-
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
index e7a46e1bc2..2408835f14 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html
@@ -3,7 +3,7 @@
+
+
-
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
index fcb5a3c319..e23b8a163d 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html
@@ -3,7 +3,7 @@
+
+
-
+
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
index ceb24299e9..737cada6d4 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html
@@ -3,7 +3,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
index 6a53a33970..83e270c7bb 100644
--- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
+++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html
@@ -3,7 +3,7 @@
+
+
-
+
diff --git a/common/lib/xmodule/xmodule/js/spec/video/events_spec.js b/common/lib/xmodule/xmodule/js/spec/video/events_spec.js
new file mode 100644
index 0000000000..3da9dfc442
--- /dev/null
+++ b/common/lib/xmodule/xmodule/js/spec/video/events_spec.js
@@ -0,0 +1,164 @@
+(function () {
+ describe('VideoPlayer Events', function () {
+ var state, videoPlayer, player, videoControl, videoCaption,
+ videoProgressSlider, videoSpeedControl, videoVolumeControl,
+ oldOTBD;
+
+ function initialize(fixture, params) {
+ if (_.isString(fixture)) {
+ loadFixtures(fixture);
+ } else {
+ if (_.isObject(fixture)) {
+ params = fixture;
+ }
+
+ loadFixtures('video_all.html');
+ }
+
+ if (_.isObject(params)) {
+ $('#example')
+ .find('#video_id')
+ .data(params);
+ }
+
+ state = new Video('#example');
+
+ state.videoEl = $('video, iframe');
+ videoPlayer = state.videoPlayer;
+ player = videoPlayer.player;
+ videoControl = state.videoControl;
+ videoCaption = state.videoCaption;
+ videoProgressSlider = state.videoProgressSlider;
+ videoSpeedControl = state.videoSpeedControl;
+ videoVolumeControl = state.videoVolumeControl;
+
+ state.resizer = (function () {
+ var methods = [
+ 'align',
+ 'alignByWidthOnly',
+ 'alignByHeightOnly',
+ 'setParams',
+ 'setMode'
+ ],
+ obj = {};
+
+ $.each(methods, function (index, method) {
+ obj[method] = jasmine.createSpy(method).andReturn(obj);
+ });
+
+ return obj;
+ }());
+ }
+
+ function initializeYouTube() {
+ initialize('video.html');
+ }
+
+ beforeEach(function () {
+ oldOTBD = window.onTouchBasedDevice;
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
+ .andReturn(null);
+ this.oldYT = window.YT;
+
+ jasmine.stubRequests();
+ window.YT = {
+ Player: function () {
+ return {
+ getPlaybackQuality: function () {}
+ };
+ },
+ PlayerState: this.oldYT.PlayerState,
+ ready: function (callback) {
+ callback();
+ }
+ };
+ });
+
+ afterEach(function () {
+ $('source').remove();
+ window.onTouchBasedDevice = oldOTBD;
+ window.YT = this.oldYT;
+ });
+
+ it('initialize', function(){
+ runs(function () {
+ initialize();
+ });
+
+ waitsFor(function () {
+ return state.el.hasClass('is-initialized');
+ }, 'Player is not initialized.', WAIT_TIMEOUT);
+
+ runs(function () {
+ expect('initialize').not.toHaveBeenTriggeredOn('.video');
+ });
+ });
+
+ it('ready', function() {
+ runs(function () {
+ initialize();
+ });
+
+ waitsFor(function () {
+ return state.el.hasClass('is-initialized');
+ }, 'Player is not initialized.', WAIT_TIMEOUT);
+
+ runs(function () {
+ expect('ready').not.toHaveBeenTriggeredOn('.video');
+ });
+ });
+
+ it('play', function() {
+ initialize();
+ videoPlayer.play();
+ expect('play').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ it('pause', function() {
+ initialize();
+ videoPlayer.play();
+ videoPlayer.pause();
+ expect('pause').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ it('volumechange', function() {
+ initialize();
+ videoPlayer.onVolumeChange(60);
+
+ expect('volumechange').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ it('speedchange', function() {
+ initialize();
+ videoPlayer.onSpeedChange('2.0');
+
+ expect('speedchange').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ it('qualitychange', function() {
+ initializeYouTube();
+ videoPlayer.onPlaybackQualityChange();
+
+ expect('qualitychange').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ it('seek', function() {
+ initialize();
+ videoPlayer.onCaptionSeek({
+ time: 1,
+ type: 'any'
+ });
+
+ expect('seek').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ it('ended', function() {
+ initialize();
+ videoPlayer.onEnded();
+
+ expect('ended').not.toHaveBeenTriggeredOn('.video');
+ });
+
+ });
+
+}).call(this);
diff --git a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
index 72b5e4e3b2..dd575104b0 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js
@@ -60,7 +60,6 @@
beforeEach(function () {
loadFixtures('video_html5.html');
- this.stubVideoPlayer = jasmine.createSpy('VideoPlayer');
$.cookie.andReturn('0.75');
});
diff --git a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
index ae2b8a276e..6a2b0b8fad 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/html5_video_spec.js
@@ -11,9 +11,7 @@
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
- .createSpy('onTouchBasedDevice').andReturn(false);
- initialize();
- player.config.events.onReady = jasmine.createSpy('onReady');
+ .createSpy('onTouchBasedDevice').andReturn(null);
});
afterEach(function() {
@@ -24,40 +22,119 @@
window.onTouchBasedDevice = oldOTBD;
});
- describe('events:', function () {
+ describe('on non-Touch devices', function () {
beforeEach(function () {
- spyOn(player, 'callStateChangeCallback').andCallThrough();
+ initialize();
+ player.config.events.onReady = jasmine.createSpy('onReady');
});
- describe('[click]', function () {
- describe('when player is paused', function () {
+ describe('events:', function () {
+ beforeEach(function () {
+ spyOn(player, 'callStateChangeCallback').andCallThrough();
+ });
+
+ describe('[click]', function () {
+ describe('when player is paused', function () {
+ beforeEach(function () {
+ spyOn(player.video, 'play').andCallThrough();
+ player.playerState = STATUS.PAUSED;
+ $(player.videoEl).trigger('click');
+ });
+
+ it('native play event was called', function () {
+ expect(player.video.play).toHaveBeenCalled();
+ });
+
+ it('player state was changed', function () {
+ waitsFor(function () {
+ return player.getPlayerState() !== STATUS.PAUSED;
+ }, 'Player state should be changed', WAIT_TIMEOUT);
+
+ runs(function () {
+ expect(player.getPlayerState())
+ .toBe(STATUS.PLAYING);
+ });
+ });
+
+ it('callback was called', function () {
+ waitsFor(function () {
+ var stateStatus = state.videoPlayer.player
+ .getPlayerState();
+
+ return stateStatus !== STATUS.PAUSED;
+ }, 'Player state should be changed', WAIT_TIMEOUT);
+
+ runs(function () {
+ expect(player.callStateChangeCallback)
+ .toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('[player is playing]', function () {
+ beforeEach(function () {
+ spyOn(player.video, 'pause').andCallThrough();
+ player.playerState = STATUS.PLAYING;
+ $(player.videoEl).trigger('click');
+ });
+
+ it('native event was called', function () {
+ expect(player.video.pause).toHaveBeenCalled();
+ });
+
+ it('player state was changed', function () {
+ waitsFor(function () {
+ return player.getPlayerState() !== STATUS.PLAYING;
+ }, 'Player state should be changed', WAIT_TIMEOUT);
+
+ runs(function () {
+ expect(player.getPlayerState())
+ .toBe(STATUS.PAUSED);
+ });
+ });
+
+ it('callback was called', function () {
+ waitsFor(function () {
+ return player.getPlayerState() !== STATUS.PLAYING;
+ }, 'Player state should be changed', WAIT_TIMEOUT);
+
+ runs(function () {
+ expect(player.callStateChangeCallback)
+ .toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('[play]', function () {
beforeEach(function () {
spyOn(player.video, 'play').andCallThrough();
player.playerState = STATUS.PAUSED;
- $(player.videoEl).trigger('click');
+ player.playVideo();
});
- it('native play event was called', function () {
+ it('native event was called', function () {
expect(player.video.play).toHaveBeenCalled();
});
+
it('player state was changed', function () {
waitsFor(function () {
- return player.getPlayerState() !== STATUS.PAUSED;
+ var state = player.getPlayerState();
+
+ return state !== STATUS.PAUSED;
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
- expect(player.getPlayerState())
- .toBe(STATUS.PLAYING);
+ expect(player.getPlayerState()).toBe(STATUS.PLAYING);
});
});
it('callback was called', function () {
waitsFor(function () {
- var stateStatus = state.videoPlayer.player
- .getPlayerState();
+ var state = player.getPlayerState();
- return stateStatus !== STATUS.PAUSED;
+ return state !== STATUS.PAUSED;
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
@@ -67,11 +144,15 @@
});
});
- describe('[player is playing]', function () {
+ describe('[pause]', function () {
beforeEach(function () {
spyOn(player.video, 'pause').andCallThrough();
- player.playerState = STATUS.PLAYING;
- $(player.videoEl).trigger('click');
+ player.playerState = STATUS.UNSTARTED;
+ player.playVideo();
+ waitsFor(function () {
+ return player.getPlayerState() !== STATUS.UNSTARTED;
+ }, 'Video never started playing', WAIT_TIMEOUT);
+ player.pauseVideo();
});
it('native event was called', function () {
@@ -84,8 +165,7 @@
}, 'Player state should be changed', WAIT_TIMEOUT);
runs(function () {
- expect(player.getPlayerState())
- .toBe(STATUS.PAUSED);
+ expect(player.getPlayerState()).toBe(STATUS.PAUSED);
});
});
@@ -93,243 +173,189 @@
waitsFor(function () {
return player.getPlayerState() !== STATUS.PLAYING;
}, 'Player state should be changed', WAIT_TIMEOUT);
-
runs(function () {
expect(player.callStateChangeCallback)
.toHaveBeenCalled();
});
});
});
- });
- describe('[play]', function () {
- beforeEach(function () {
- spyOn(player.video, 'play').andCallThrough();
- player.playerState = STATUS.PAUSED;
- player.playVideo();
- });
+ describe('[loadedmetadata]', function () {
+ it(
+ 'player state was changed, start/end was defined, ' +
+ 'onReady called', function ()
+ {
+ waitsFor(function () {
+ return player.getPlayerState() !== STATUS.UNSTARTED;
+ }, 'Video cannot be played', WAIT_TIMEOUT);
- it('native event was called', function () {
- expect(player.video.play).toHaveBeenCalled();
- });
-
- it('player state was changed', function () {
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.PAUSED;
- }, 'Player state should be changed', WAIT_TIMEOUT);
-
- runs(function () {
- expect(player.getPlayerState()).toBe(STATUS.PLAYING);
+ runs(function () {
+ expect(player.getPlayerState()).toBe(STATUS.PAUSED);
+ expect(player.video.currentTime).toBe(0);
+ expect(player.config.events.onReady)
+ .toHaveBeenCalled();
+ });
});
});
- it('callback was called', function () {
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.PAUSED;
- }, 'Player state should be changed', WAIT_TIMEOUT);
-
- runs(function () {
- expect(player.callStateChangeCallback)
- .toHaveBeenCalled();
+ describe('[ended]', function () {
+ beforeEach(function () {
+ waitsFor(function () {
+ return player.getPlayerState() !== STATUS.UNSTARTED;
+ }, 'Video cannot be played', WAIT_TIMEOUT);
});
- });
- });
- describe('[pause]', function () {
- beforeEach(function () {
- spyOn(player.video, 'pause').andCallThrough();
- player.playerState = STATUS.UNSTARTED;
- player.playVideo();
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.UNSTARTED;
- }, 'Video never started playing', WAIT_TIMEOUT);
- player.pauseVideo();
- });
-
- it('native event was called', function () {
- expect(player.video.pause).toHaveBeenCalled();
- });
-
- it('player state was changed', function () {
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.PLAYING;
- }, 'Player state should be changed', WAIT_TIMEOUT);
-
- runs(function () {
- expect(player.getPlayerState()).toBe(STATUS.PAUSED);
+ it('player state was changed', function () {
+ runs(function () {
+ jasmine.fireEvent(player.video, 'ended');
+ expect(player.getPlayerState()).toBe(STATUS.ENDED);
+ });
});
- });
- it('callback was called', function () {
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.PLAYING;
- }, 'Player state should be changed', WAIT_TIMEOUT);
- runs(function () {
- expect(player.callStateChangeCallback)
- .toHaveBeenCalled();
- });
- });
- });
-
- describe('[canplay]', function () {
- it(
- 'player state was changed, start/end was defined, ' +
- 'onReady called', function ()
- {
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.UNSTARTED;
- }, 'Video cannot be played', WAIT_TIMEOUT);
-
- runs(function () {
- expect(player.getPlayerState()).toBe(STATUS.PAUSED);
- expect(player.video.currentTime).toBe(0);
- expect(player.config.events.onReady)
- .toHaveBeenCalled();
- });
- });
- });
-
- describe('[ended]', function () {
- beforeEach(function () {
- waitsFor(function () {
- return player.getPlayerState() !== STATUS.UNSTARTED;
- }, 'Video cannot be played', WAIT_TIMEOUT);
- });
-
- it('player state was changed', function () {
- runs(function () {
+ it('callback was called', function () {
jasmine.fireEvent(player.video, 'ended');
+ expect(player.callStateChangeCallback).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('methods', function () {
+ var volume, seek, duration, playbackRate;
+
+ beforeEach(function () {
+ waitsFor(function () {
+ volume = player.video.volume;
+ seek = player.video.currentTime;
+ return player.playerState === STATUS.PAUSED;
+ }, 'Video cannot be played', WAIT_TIMEOUT);
+ });
+
+ it('pauseVideo', function () {
+ runs(function () {
+ spyOn(player.video, 'pause').andCallThrough();
+ player.pauseVideo();
+ expect(player.video.pause).toHaveBeenCalled();
+ });
+ });
+
+ describe('seekTo', function () {
+ it('set new correct value', function () {
+ runs(function () {
+ player.seekTo(2);
+ expect(player.getCurrentTime()).toBe(2);
+ });
+ });
+
+ it('set new inccorrect values', function () {
+ runs(function () {
+ player.seekTo(-50);
+ expect(player.getCurrentTime()).toBe(seek);
+ player.seekTo('5');
+ expect(player.getCurrentTime()).toBe(seek);
+ player.seekTo(500000);
+ expect(player.getCurrentTime()).toBe(seek);
+ });
+ });
+ });
+
+ describe('setVolume', function () {
+ it('set new correct value', function () {
+ runs(function () {
+ player.setVolume(50);
+ expect(player.getVolume()).toBe(50 * 0.01);
+ });
+ });
+
+ it('set new incorrect values', function () {
+ runs(function () {
+ player.setVolume(-50);
+ expect(player.getVolume()).toBe(volume);
+ player.setVolume('5');
+ expect(player.getVolume()).toBe(volume);
+ player.setVolume(500000);
+ expect(player.getVolume()).toBe(volume);
+ });
+ });
+ });
+
+ it('getCurrentTime', function () {
+ runs(function () {
+ player.video.currentTime = 3;
+ expect(player.getCurrentTime())
+ .toBe(player.video.currentTime);
+ });
+ });
+
+ it('playVideo', function () {
+ runs(function () {
+ spyOn(player.video, 'play').andCallThrough();
+ player.playVideo();
+ expect(player.video.play).toHaveBeenCalled();
+ });
+ });
+
+ it('getPlayerState', function () {
+ runs(function () {
+ player.playerState = STATUS.PLAYING;
+ expect(player.getPlayerState()).toBe(STATUS.PLAYING);
+ player.playerState = STATUS.ENDED;
expect(player.getPlayerState()).toBe(STATUS.ENDED);
});
});
- it('callback was called', function () {
- jasmine.fireEvent(player.video, 'ended');
- expect(player.callStateChangeCallback).toHaveBeenCalled();
+ it('getVolume', function () {
+ runs(function () {
+ volume = player.video.volume = 0.5;
+ expect(player.getVolume()).toBe(volume);
+ });
+ });
+
+ it('getDuration', function () {
+ runs(function () {
+ duration = player.video.duration;
+ expect(player.getDuration()).toBe(duration);
+ });
+ });
+
+ describe('setPlaybackRate', function () {
+ it('set a correct value', function () {
+ playbackRate = 1.5;
+ player.setPlaybackRate(playbackRate);
+ expect(player.video.playbackRate).toBe(playbackRate);
+ });
+
+ it('set NaN value', function () {
+ var oldPlaybackRate = player.video.playbackRate;
+
+ // When we try setting the playback rate to some
+ // non-numerical value, nothing should happen.
+ playbackRate = NaN;
+ player.setPlaybackRate(playbackRate);
+ expect(player.video.playbackRate).toBe(oldPlaybackRate);
+ });
+ });
+
+ it('getAvailablePlaybackRates', function () {
+ expect(player.getAvailablePlaybackRates())
+ .toEqual(playbackRates);
+ });
+
+ it('_getLogs', function () {
+ runs(function () {
+ var logs = player._getLogs();
+ expect(logs).toEqual(jasmine.any(Array));
+ expect(logs.length).toBeGreaterThan(0);
+ });
});
});
});
- describe('methods', function () {
- var volume, seek, duration, playbackRate;
+ it('native controls are used on iPhone', function () {
+ window.onTouchBasedDevice.andReturn(['iPhone']);
+ initialize();
+ player.config.events.onReady = jasmine.createSpy('onReady');
- beforeEach(function () {
- waitsFor(function () {
- volume = player.video.volume;
- seek = player.video.currentTime;
- return player.playerState === STATUS.PAUSED;
- }, 'Video cannot be played', WAIT_TIMEOUT);
- });
-
- it('pauseVideo', function () {
- runs(function () {
- spyOn(player.video, 'pause').andCallThrough();
- player.pauseVideo();
- expect(player.video.pause).toHaveBeenCalled();
- });
- });
-
- describe('seekTo', function () {
- it('set new correct value', function () {
- runs(function () {
- player.seekTo(2);
- expect(player.getCurrentTime()).toBe(2);
- });
- });
-
- it('set new inccorrect values', function () {
- runs(function () {
- player.seekTo(-50);
- expect(player.getCurrentTime()).toBe(seek);
- player.seekTo('5');
- expect(player.getCurrentTime()).toBe(seek);
- player.seekTo(500000);
- expect(player.getCurrentTime()).toBe(seek);
- });
- });
- });
-
- describe('setVolume', function () {
- it('set new correct value', function () {
- runs(function () {
- player.setVolume(50);
- expect(player.getVolume()).toBe(50 * 0.01);
- });
- });
-
- it('set new incorrect values', function () {
- runs(function () {
- player.setVolume(-50);
- expect(player.getVolume()).toBe(volume);
- player.setVolume('5');
- expect(player.getVolume()).toBe(volume);
- player.setVolume(500000);
- expect(player.getVolume()).toBe(volume);
- });
- });
- });
-
- it('getCurrentTime', function () {
- runs(function () {
- player.video.currentTime = 3;
- expect(player.getCurrentTime())
- .toBe(player.video.currentTime);
- });
- });
-
- it('playVideo', function () {
- runs(function () {
- spyOn(player.video, 'play').andCallThrough();
- player.playVideo();
- expect(player.video.play).toHaveBeenCalled();
- });
- });
-
- it('getPlayerState', function () {
- runs(function () {
- player.playerState = STATUS.PLAYING;
- expect(player.getPlayerState()).toBe(STATUS.PLAYING);
- player.playerState = STATUS.ENDED;
- expect(player.getPlayerState()).toBe(STATUS.ENDED);
- });
- });
-
- it('getVolume', function () {
- runs(function () {
- volume = player.video.volume = 0.5;
- expect(player.getVolume()).toBe(volume);
- });
- });
-
- it('getDuration', function () {
- runs(function () {
- duration = player.video.duration;
- expect(player.getDuration()).toBe(duration);
- });
- });
-
- describe('setPlaybackRate', function () {
- it('set a correct value', function () {
- playbackRate = 1.5;
- player.setPlaybackRate(playbackRate);
- expect(player.video.playbackRate).toBe(playbackRate);
- });
-
- it('set NaN value', function () {
- var oldPlaybackRate = player.video.playbackRate;
-
- // When we try setting the playback rate to some
- // non-numerical value, nothing should happen.
- playbackRate = NaN;
- player.setPlaybackRate(playbackRate);
- expect(player.video.playbackRate).toBe(oldPlaybackRate);
- });
- });
-
- it('getAvailablePlaybackRates', function () {
- expect(player.getAvailablePlaybackRates())
- .toEqual(playbackRates);
- });
+ expect($('video')).toHaveAttr('controls');
});
});
}).call(this);
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
index 4e45d32838..3be31f458c 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_caption_spec.js
@@ -15,7 +15,7 @@
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
- .andReturn(false);
+ .andReturn(null);
initialize();
});
@@ -175,7 +175,7 @@
describe('when on a touch-based device', function () {
beforeEach(function () {
- window.onTouchBasedDevice.andReturn(true);
+ window.onTouchBasedDevice.andReturn(['iPad']);
initialize();
});
@@ -209,34 +209,15 @@
});
describe('mouse movement', function () {
- // We will store default window.setTimeout() function here.
- var oldSetTimeout = null;
-
beforeEach(function () {
- // Store original window.setTimeout() function. If we do not do
- // this, then all other tests that rely on code which uses
- // window.setTimeout() function might (and probably will) fail.
- oldSetTimeout = window.setTimeout;
- // Redefine window.setTimeout() function as a spy.
- window.setTimeout = jasmine.createSpy().andCallFake(
- function (callback, timeout) {
- return 5;
- }
- );
- window.setTimeout.andReturn(100);
+ jasmine.Clock.useMock();
spyOn(window, 'clearTimeout');
});
- afterEach(function () {
- // Reset the default window.setTimeout() function. If we do not
- // do this, then all other tests that rely on code which uses
- // window.setTimeout() function might (and probably will) fail.
- window.setTimeout = oldSetTimeout;
- });
-
describe('when cursor is outside of the caption box', function () {
beforeEach(function () {
$(window).trigger(jQuery.Event('mousemove'));
+ jasmine.Clock.tick(state.config.captionsFreezeTime);
});
it('does not set freezing timeout', function () {
@@ -246,11 +227,14 @@
describe('when cursor is in the caption box', function () {
beforeEach(function () {
+ spyOn(videoCaption, 'onMouseLeave');
$('.subtitles').trigger(jQuery.Event('mouseenter'));
+ jasmine.Clock.tick(state.config.captionsFreezeTime);
});
it('set the freezing timeout', function () {
- expect(videoCaption.frozen).toEqual(100);
+ expect(videoCaption.frozen).not.toBeFalsy();
+ expect(videoCaption.onMouseLeave).toHaveBeenCalled();
});
describe('when the cursor is moving', function () {
@@ -259,7 +243,7 @@
});
it('reset the freezing timeout', function () {
- expect(window.clearTimeout).toHaveBeenCalledWith(100);
+ expect(window.clearTimeout).toHaveBeenCalled();
});
});
@@ -269,7 +253,7 @@
});
it('reset the freezing timeout', function () {
- expect(window.clearTimeout).toHaveBeenCalledWith(100);
+ expect(window.clearTimeout).toHaveBeenCalled();
});
});
});
@@ -337,7 +321,7 @@
describe('play', function () {
describe('when the caption was not rendered', function () {
beforeEach(function () {
- window.onTouchBasedDevice.andReturn(true);
+ window.onTouchBasedDevice.andReturn(['iPad']);
initialize();
videoCaption.play();
});
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js
index 1c6912cb79..49f52d208d 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_control_spec.js
@@ -2,15 +2,23 @@
describe('VideoControl', function() {
var state, videoControl, oldOTBD;
- function initialize() {
- loadFixtures('video_all.html');
+ function initialize(fixture) {
+ if (fixture) {
+ loadFixtures(fixture);
+ } else {
+ loadFixtures('video_all.html');
+ }
state = new Video('#example');
videoControl = state.videoControl;
}
+ function initializeYouTube() {
+ initialize('video.html');
+ }
+
beforeEach(function(){
oldOTBD = window.onTouchBasedDevice;
- window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
});
afterEach(function() {
@@ -75,13 +83,13 @@
describe('when on a touch based device', function() {
beforeEach(function() {
- window.onTouchBasedDevice.andReturn(true);
+ window.onTouchBasedDevice.andReturn(['iPad']);
initialize();
});
it('does not add the play class to video control', function() {
- expect($('.video_control')).not.toHaveClass('play');
- expect($('.video_control')).not.toHaveAttr('title', 'Play');
+ expect($('.video_control')).toHaveClass('play');
+ expect($('.video_control')).toHaveAttr('title', 'Play');
});
});
});
@@ -147,6 +155,136 @@
});
});
});
+
+ describe('Play placeholder', function () {
+
+ beforeEach(function () {
+ this.oldYT = window.YT;
+
+ jasmine.stubRequests();
+ window.YT = {
+ Player: function () { },
+ PlayerState: this.oldYT.PlayerState,
+ ready: function (callback) {
+ callback();
+ }
+ };
+
+ spyOn(window.YT, 'Player');
+ });
+
+ afterEach(function () {
+ window.YT = this.oldYT;
+ });
+
+
+ it ('works correctly on calling proper methods', function () {
+ initialize();
+ var btnPlay = state.el.find('.btn-play');
+
+ videoControl.showPlayPlaceholder();
+
+ expect(btnPlay).not.toHaveClass('is-hidden');
+ expect(btnPlay).toHaveAttrs({
+ 'aria-hidden': 'false',
+ 'tabindex': 0
+ });
+
+ videoControl.hidePlayPlaceholder();
+
+ expect(btnPlay).toHaveClass('is-hidden');
+ expect(btnPlay).toHaveAttrs({
+ 'aria-hidden': 'true',
+ 'tabindex': -1
+ });
+ });
+
+ var cases = [
+ {
+ name: 'PC',
+ isShown: false,
+ isTouch: null
+ },
+ {
+ name: 'iPad',
+ isShown: true,
+ isTouch: ['iPad']
+ },
+ {
+ name: 'Android',
+ isShown: true,
+ isTouch: ['Android']
+ },
+ {
+ name: 'iPhone',
+ isShown: false,
+ isTouch: ['iPhone']
+ }
+ ];
+
+ $.each(cases, function(index, data) {
+ var message = [
+ (data.isShown) ? 'is' : 'is not',
+ ' shown on',
+ data.name
+ ].join('');
+
+ it(message, function () {
+ window.onTouchBasedDevice.andReturn(data.isTouch);
+ initialize();
+ var btnPlay = state.el.find('.btn-play');
+
+ if (data.isShown) {
+ expect(btnPlay).not.toHaveClass('is-hidden');
+ } else {
+ expect(btnPlay).toHaveClass('is-hidden');
+ }
+ });
+ });
+
+ $.each(['iPad', 'Android'], function(index, device) {
+ it('is shown on paused video on '+ device +' in HTML5 player', function () {
+ window.onTouchBasedDevice.andReturn([device]);
+ initialize();
+ var btnPlay = state.el.find('.btn-play');
+
+ videoControl.play();
+ videoControl.pause();
+
+ expect(btnPlay).not.toHaveClass('is-hidden');
+ });
+
+ it('is hidden on playing video on '+ device +' in HTML5 player', function () {
+ window.onTouchBasedDevice.andReturn([device]);
+ initialize();
+ var btnPlay = state.el.find('.btn-play');
+
+ videoControl.play();
+
+ expect(btnPlay).toHaveClass('is-hidden');
+ });
+
+ it('is hidden on paused video on '+ device +' in YouTube player', function () {
+ window.onTouchBasedDevice.andReturn([device]);
+ initializeYouTube();
+ var btnPlay = state.el.find('.btn-play');
+
+ videoControl.play();
+ videoControl.pause();
+
+ expect(btnPlay).toHaveClass('is-hidden');
+ });
+ });
+ });
+
+ it('show', function () {
+ initialize();
+ var controls = state.el.find('.video-controls');
+ controls.addClass('is-hidden');
+
+ videoControl.show();
+ expect(controls).not.toHaveClass('is-hidden');
+ });
});
}).call(this);
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
index 62e1475356..cadba4c995 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_player_spec.js
@@ -57,7 +57,7 @@
beforeEach(function () {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
- .andReturn(false);
+ .andReturn(null);
});
afterEach(function () {
@@ -119,8 +119,8 @@
window.YT = {
Player: function () { },
PlayerState: oldYT.PlayerState,
- ready: function (f) {
- f();
+ ready: function (callback) {
+ callback();
}
};
@@ -156,19 +156,18 @@
// available globally. It is defined within the scope of Require
// JS.
- describe('when not on a touch based device', function () {
- beforeEach(function () {
- window.onTouchBasedDevice.andReturn(true);
- initialize();
- });
-
- it('create video volume control', function () {
- expect(videoVolumeControl).toBeDefined();
- expect(videoVolumeControl.el).toHaveClass('volume');
+ describe('when on a touch based device', function () {
+ $.each(['iPad', 'Android'], function(index, device) {
+ it('create video volume control on' + device, function() {
+ window.onTouchBasedDevice.andReturn([device]);
+ initialize();
+ expect(videoVolumeControl).toBeUndefined();
+ expect(state.el.find('div.volume')).not.toExist();
+ });
});
});
- describe('when on a touch based device', function () {
+ describe('when not on a touch based device', function () {
var oldOTBD;
beforeEach(function () {
@@ -343,16 +342,8 @@
state.videoPlayer.play();
waitsFor(function () {
- var duration = videoPlayer.duration(),
- currentTime = videoPlayer.currentTime;
-
- return (
- isFinite(currentTime) &&
- currentTime > 0 &&
- isFinite(duration) &&
- duration > 0
- );
- }, 'video begins playing', 10000);
+ return videoPlayer.isPlaying();
+ }, 'video begins playing', WAIT_TIMEOUT);
});
it('Slider event causes log update', function () {
@@ -555,34 +546,24 @@
});
it('video is paused on first endTime, start & end time are reset', function () {
- var checkForStartEndTimeSet = true;
+ var duration;
videoProgressSlider.notifyThroughHandleEnd.reset();
videoPlayer.pause.reset();
videoPlayer.play();
waitsFor(function () {
- if (
- !isFinite(videoPlayer.currentTime) ||
- videoPlayer.currentTime <= 0
- ) {
- return false;
- }
+ duration = Math.round(videoPlayer.currentTime);
- if (checkForStartEndTimeSet) {
- checkForStartEndTimeSet = false;
-
- expect(videoPlayer.startTime).toBe(START_TIME);
- expect(videoPlayer.endTime).toBe(END_TIME);
- }
-
- return videoPlayer.pause.calls.length === 1
- }, 5000, 'pause() has been called');
+ return videoPlayer.pause.calls.length === 1;
+ }, 'pause() has been called', WAIT_TIMEOUT);
runs(function () {
expect(videoPlayer.startTime).toBe(0);
expect(videoPlayer.endTime).toBe(null);
+ expect(duration).toBe(END_TIME);
+
expect(videoProgressSlider.notifyThroughHandleEnd)
.toHaveBeenCalledWith({end: true});
});
@@ -608,7 +589,7 @@
}
return false;
- }, 'Video is fully loaded.', 1000);
+ }, 'Video is fully loaded.', WAIT_TIMEOUT);
runs(function () {
var htmlStr;
@@ -637,7 +618,7 @@
it('update the playback time on caption', function () {
waitsFor(function () {
return videoPlayer.duration() > 0;
- }, 'Video is fully loaded.', 1000);
+ }, 'Video is fully loaded.', WAIT_TIMEOUT);
runs(function () {
videoPlayer.updatePlayTime(60);
@@ -654,7 +635,7 @@
duration = videoPlayer.duration();
return duration > 0;
- }, 'Video is fully loaded.', 1000);
+ }, 'Video is fully loaded.', WAIT_TIMEOUT);
runs(function () {
videoPlayer.updatePlayTime(60);
@@ -692,9 +673,9 @@
waitsFor(function () {
duration = videoPlayer.duration();
- return duration > 0 &&
+ return videoPlayer.isPlaying() &&
videoPlayer.initialSeekToStartTime === false;
- }, 'duration becomes available', 1000);
+ }, 'duration becomes available', WAIT_TIMEOUT);
runs(function () {
expect(videoPlayer.startTime).toBe(START_TIME);
@@ -724,11 +705,9 @@
videoPlayer.play();
waitsFor(function () {
- duration = videoPlayer.duration();
-
- return duration > 0 &&
+ return videoPlayer.isPlaying() &&
videoPlayer.initialSeekToStartTime === false;
- }, 'updatePlayTime was invoked and duration is set', 5000);
+ }, 'updatePlayTime was invoked and duration is set', WAIT_TIMEOUT);
runs(function () {
expect(videoPlayer.endTime).toBe(null);
@@ -896,6 +875,62 @@
expect(realValue).toEqual(expectedValue);
});
});
+
+ describe('on Touch devices', function () {
+ it('`is-touch` class name is added to container', function () {
+ $.each(['iPad', 'Android', 'iPhone'], function(index, device) {
+ window.onTouchBasedDevice.andReturn([device]);
+ initialize();
+
+ expect(state.el).toHaveClass('is-touch');
+ });
+ });
+
+ it('modules are not initialized on iPhone', function () {
+ window.onTouchBasedDevice.andReturn(['iPhone']);
+ initialize();
+
+ var modules = [
+ videoControl, videoCaption, videoProgressSlider,
+ videoSpeedControl, videoVolumeControl
+ ];
+
+ $.each(modules, function (index, module) {
+ expect(module).toBeUndefined();
+ });
+ });
+
+ $.each(['iPad', 'Android'], function(index, device) {
+ var message = 'controls become visible after playing starts on ' +
+ device;
+ it(message, function() {
+ var controls;
+ window.onTouchBasedDevice.andReturn([device]);
+
+ runs(function () {
+ initialize();
+ controls = state.el.find('.video-controls');
+ });
+
+ waitsFor(function () {
+ return state.el.hasClass('is-initialized');
+ },'Video is not initialized.' , WAIT_TIMEOUT);
+
+ runs(function () {
+ expect(controls).toHaveClass('is-hidden');
+ videoPlayer.play();
+ });
+
+ waitsFor(function () {
+ return videoPlayer.isPlaying();
+ },'Video does not play.' , WAIT_TIMEOUT);
+
+ runs(function () {
+ expect(controls).not.toHaveClass('is-hidden');
+ });
+ });
+ });
+ });
});
}).call(this);
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js
index a8450c39cd..5535f2543e 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_progress_slider_spec.js
@@ -12,7 +12,7 @@
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice')
- .andReturn(false);
+ .andReturn(null);
});
afterEach(function() {
@@ -44,18 +44,23 @@
});
describe('on a touch-based device', function() {
- beforeEach(function() {
- window.onTouchBasedDevice.andReturn(true);
- spyOn($.fn, 'slider').andCallThrough();
- initialize();
- });
+ it('does not build the slider on iPhone', function() {
- it('does not build the slider', function() {
- expect(videoProgressSlider.slider).toBeUndefined();
+ window.onTouchBasedDevice.andReturn(['iPhone']);
+ initialize();
+
+ expect(videoProgressSlider).toBeUndefined();
// We can't expect $.fn.slider not to have been called,
// because sliders are used in other parts of Video.
});
+ $.each(['iPad', 'Android'], function(index, device) {
+ it('build the slider on ' + device, function() {
+ window.onTouchBasedDevice.andReturn([device]);
+ initialize();
+ expect(videoProgressSlider.slider).toBeDefined();
+ });
+ });
});
});
@@ -127,125 +132,58 @@
initialize();
spyOn($.fn, 'slider').andCallThrough();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
-
- state.videoPlayer.play();
-
- waitsFor(function () {
- var duration = videoPlayer.duration(),
- currentTime = videoPlayer.currentTime;
-
- return (
- isFinite(currentTime) &&
- currentTime > 0 &&
- isFinite(duration) &&
- duration > 0
- );
- }, 'video begins playing', 10000);
});
it('freeze the slider', function() {
- runs(function () {
- videoProgressSlider.onSlide(
- jQuery.Event('slide'), { value: 20 }
- );
+ videoProgressSlider.onSlide(
+ jQuery.Event('slide'), { value: 20 }
+ );
- expect(videoProgressSlider.frozen).toBeTruthy();
- });
+ expect(videoProgressSlider.frozen).toBeTruthy();
});
- // Turned off test due to flakiness (11/25/13)
- xit('trigger seek event', function() {
- runs(function () {
- videoProgressSlider.onSlide(
- jQuery.Event('slide'), { value: 20 }
- );
+ it('trigger seek event', function() {
+ videoProgressSlider.onSlide(
+ jQuery.Event('slide'), { value: 20 }
+ );
- expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
-
- waitsFor(function () {
- return Math.round(videoPlayer.currentTime) === 20;
- }, 'currentTime got updated', 10000);
- });
+ expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
});
});
describe('onStop', function() {
- // We will store default window.setTimeout() function here.
- var oldSetTimeout = null;
beforeEach(function() {
- // Store original window.setTimeout() function. If we do not do
- // this, then all other tests that rely on code which uses
- // window.setTimeout() function might (and probably will) fail.
- oldSetTimeout = window.setTimeout;
- // Redefine window.setTimeout() function as a spy.
- window.setTimeout = jasmine.createSpy()
- .andCallFake(function (callback, timeout) {
- return 5;
- });
- window.setTimeout.andReturn(100);
+ jasmine.Clock.useMock();
initialize();
spyOn(videoPlayer, 'onSlideSeek').andCallThrough();
- videoPlayer.play();
-
- waitsFor(function () {
- var duration = videoPlayer.duration(),
- currentTime = videoPlayer.currentTime;
-
- return (
- isFinite(currentTime) &&
- currentTime > 0 &&
- isFinite(duration) &&
- duration > 0
- );
- }, 'video begins playing', 10000);
- });
-
- afterEach(function () {
- // Reset the default window.setTimeout() function. If we do not
- // do this, then all other tests that rely on code which uses
- // window.setTimeout() function might (and probably will) fail.
- window.setTimeout = oldSetTimeout;
});
it('freeze the slider', function() {
- runs(function () {
- videoProgressSlider.onStop(
- jQuery.Event('stop'), { value: 20 }
- );
+ videoProgressSlider.onStop(
+ jQuery.Event('stop'), { value: 20 }
+ );
- expect(videoProgressSlider.frozen).toBeTruthy();
- });
+ expect(videoProgressSlider.frozen).toBeTruthy();
});
- // Turned off test due to flakiness (11/25/13)
- xit('trigger seek event', function() {
- runs(function () {
- videoProgressSlider.onStop(
- jQuery.Event('stop'), { value: 20 }
- );
+ it('trigger seek event', function() {
+ videoProgressSlider.onStop(
+ jQuery.Event('stop'), { value: 20 }
+ );
- expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
-
- waitsFor(function () {
- return Math.round(videoPlayer.currentTime) === 20;
- }, 'currentTime got updated', 10000);
- });
+ expect(videoPlayer.onSlideSeek).toHaveBeenCalled();
});
it('set timeout to unfreeze the slider', function() {
- runs(function () {
- videoProgressSlider.onStop(
- jQuery.Event('stop'), { value: 20 }
- );
+ videoProgressSlider.onStop(
+ jQuery.Event('stop'), { value: 20 }
+ );
- expect(window.setTimeout).toHaveBeenCalledWith(
- jasmine.any(Function), 200
- );
- window.setTimeout.mostRecentCall.args[0]();
- expect(videoProgressSlider.frozen).toBeFalsy();
- });
+ jasmine.Clock.tick(200);
+
+ expect(videoProgressSlider.frozen).toBeFalsy();
});
});
@@ -317,15 +255,7 @@
videoPlayer.play();
waitsFor(function () {
- var duration = videoPlayer.duration(),
- currentTime = videoPlayer.currentTime;
-
- return (
- isFinite(duration) &&
- duration > 0 &&
- isFinite(currentTime) &&
- currentTime > 0
- );
+ return videoPlayer.isPlaying();
}, 'duration is set, video is playing', 5000);
runs(function () {
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js
index d1749b48f1..d8bd234684 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_quality_control_spec.js
@@ -13,7 +13,7 @@
oldOTBD = window.onTouchBasedDevice;
window.onTouchBasedDevice = jasmine
.createSpy('onTouchBasedDevice')
- .andReturn(false);
+ .andReturn(null);
});
afterEach(function() {
@@ -49,7 +49,7 @@
'role': 'button',
'title': 'HD off',
'aria-disabled': 'false'
- });
+ });
});
it('bind the quality control', function() {
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
index 6836b2fcf6..f012dc21e3 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_speed_control_spec.js
@@ -12,7 +12,7 @@
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
- window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
});
@@ -48,7 +48,7 @@
'role': 'button',
'title': 'Speeds',
'aria-disabled': 'false'
- });
+ });
});
it('bind to change video speed link', function() {
@@ -57,16 +57,12 @@
});
describe('when running on touch based device', function() {
- beforeEach(function() {
- window.onTouchBasedDevice.andReturn(true);
- initialize();
- });
-
- it('open the speed toggle on click', function() {
- $('.speeds').click();
- expect($('.speeds')).toHaveClass('open');
- $('.speeds').click();
- expect($('.speeds')).not.toHaveClass('open');
+ $.each(['iPad', 'Android'], function(index, device) {
+ it('is not rendered on' + device, function() {
+ window.onTouchBasedDevice.andReturn([device]);
+ initialize();
+ expect(state.el.find('div.speeds')).not.toExist();
+ });
});
});
@@ -96,7 +92,7 @@
// 2. Speed anchor
// 3. A number of speed entry anchors
// 4. Volume anchor
- // If an other focusable element is inserted or if the order is changed, things will
+ // If another focusable element is inserted or if the order is changed, things will
// malfunction as a flag, state.previousFocus, is set in the 1,3,4 elements and is
// used to determine the behavior of foucus() and blur() for the speed anchor.
it('checks for a certain order in focusable elements in video controls', function() {
diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
index 9e64a63b4d..c8e2db97f7 100644
--- a/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
+++ b/common/lib/xmodule/xmodule/js/spec/video/video_volume_control_spec.js
@@ -11,7 +11,7 @@
beforeEach(function() {
oldOTBD = window.onTouchBasedDevice;
- window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(false);
+ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice').andReturn(null);
});
afterEach(function() {
@@ -58,9 +58,9 @@
});
expect(sliderHandle.attr('aria-valuenow')).toBeInRange(0, 100);
expect(sliderHandle.attr('aria-valuetext')).toBeInArray(arr);
-
+
});
-
+
it('add ARIA attributes to volume control', function () {
var volumeControl = $('div.volume>a');
expect(volumeControl).toHaveAttrs({
@@ -121,38 +121,38 @@
{
range: 'muted',
value: 0,
- expectation: 'muted'
+ expectation: 'muted'
},
{
range: 'in ]0,20]',
value: 10,
- expectation: 'very low'
+ expectation: 'very low'
},
{
range: 'in ]20,40]',
value: 30,
- expectation: 'low'
+ expectation: 'low'
},
{
range: 'in ]40,60]',
value: 50,
- expectation: 'average'
+ expectation: 'average'
},
{
range: 'in ]60,80]',
value: 70,
- expectation: 'loud'
+ expectation: 'loud'
},
{
range: 'in ]80,100[',
value: 90,
- expectation: 'very loud'
+ expectation: 'very loud'
},
{
range: 'maximum',
value: 100,
- expectation: 'maximum'
- }
+ expectation: 'maximum'
+ }
];
$.each(initialData, function(index, data) {
@@ -162,7 +162,7 @@
value: data.value
});
});
-
+
it('changes ARIA attributes', function () {
var sliderHandle = $('div.volume-slider>a.ui-slider-handle');
expect(sliderHandle).toHaveAttrs({
diff --git a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
index 4ced648483..77fef5ebc7 100644
--- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
+++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js
@@ -44,15 +44,29 @@ function (VideoPlayer) {
state.initialize(element)
.done(function () {
+ // On iPhones and iPods native controls are used.
+ if (/iP(hone|od)/i.test(state.isTouch[0])) {
+ _hideWaitPlaceholder(state);
+ state.el.trigger('initialize', arguments);
+
+ return false;
+ }
+
_initializeModules(state)
.done(function () {
- state.el
- .addClass('is-initialized')
- .find('.spinner')
- .attr({
- 'aria-hidden': 'true',
- 'tabindex': -1
- });
+ // On iPad ready state occurs just after start playing.
+ // We hide controls before video starts playing.
+ if (/iPad|Android/i.test(state.isTouch[0])) {
+ state.el.on('play', _.once(function() {
+ state.trigger('videoControl.show', null);
+ }));
+ } else {
+ // On PC show controls immediately.
+ state.trigger('videoControl.show', null);
+ }
+
+ _hideWaitPlaceholder(state);
+ state.el.trigger('initialize', arguments);
});
});
};
@@ -235,6 +249,16 @@ function (VideoPlayer) {
return true;
}
+ function _hideWaitPlaceholder(state) {
+ state.el
+ .addClass('is-initialized')
+ .find('.spinner')
+ .attr({
+ 'aria-hidden': 'true',
+ 'tabindex': -1
+ });
+ }
+
function _setConfigurations(state) {
_configureCaptions(state);
_setPlayerMode(state);
@@ -242,7 +266,7 @@ function (VideoPlayer) {
// Possible value are: 'visible', 'hiding', and 'invisible'.
state.controlState = 'visible';
state.controlHideTimeout = null;
- state.captionState = 'visible';
+ state.captionState = 'invisible';
state.captionHideTimeout = null;
}
@@ -299,12 +323,17 @@ function (VideoPlayer) {
// element has a CSS class 'fullscreen'.
this.__dfd__ = $.Deferred();
this.isFullScreen = false;
+ this.isTouch = onTouchBasedDevice() || '';
// The parent element of the video, and the ID.
this.el = $(element).find('.video');
this.elVideoWrapper = this.el.find('.video-wrapper');
this.id = this.el.attr('id').replace(/video_/, '');
+ if (this.isTouch) {
+ this.el.addClass('is-touch');
+ }
+
// jQuery .data() return object with keys in lower camelCase format.
data = this.el.data();
diff --git a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
index 2635398937..85b16ea210 100644
--- a/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
+++ b/common/lib/xmodule/xmodule/js/src/video/02_html5_video.js
@@ -90,6 +90,10 @@ function () {
return [0.75, 1.0, 1.25, 1.5];
};
+ Player.prototype._getLogs = function () {
+ return this.logs;
+ };
+
return Player;
/*
@@ -129,8 +133,10 @@ function () {
* }
*/
function Player(el, config) {
- var sourceStr, _this, errorMessage;
+ var isTouch = onTouchBasedDevice() || '',
+ sourceStr, _this, errorMessage;
+ this.logs = [];
// Initially we assume that el is a DOM element. If jQuery selector
// fails to select something, we assume that el is an ID of a DOM
// element. We try to select by ID. If jQuery fails this time, we
@@ -214,40 +220,51 @@ function () {
// determine what the video is currently doing.
this.videoEl = $(this.video);
+ if (/iP(hone|od)/i.test(isTouch[0])) {
+ this.videoEl.prop('controls', true);
+ }
+
this.playerState = HTML5Video.PlayerState.UNSTARTED;
// Attach a 'click' event on the