From 4c7bfb44dd7fe5b5ab3f32abd0cf9ca711fafd47 Mon Sep 17 00:00:00 2001 From: Alexander Kryklia Date: Mon, 4 May 2015 13:09:42 +0300 Subject: [PATCH] Add Video Bumper. Fix n-click behaviour on poster. Fix unit tests. Fix handler for non_en lang for bumper. Add more tests. Fix docstrings. Fix pep8. Fix static redirection with bumper. Fix button in IE11. Add video_bumper field in bok_choy. Fix pylink violations. Update docstrings and some clean up. Rename edx_video_id in bumper tests. Fix too long lines in help text. Address ui comments. Fix bumper events. Refactor bumper-transcripts code, fix bugs, address comments. Squashed commits: Fix download transcript button. [74e0c8c] Fix quality [a759f33] Fix error, when sub contains extension. [b30755c] Revert "Add video files to host for transcripts." This reverts commit cf8a96bf84346e17b6ad57ad4cc6a27d7a9118cd. [36f038a] Add video files to host for transcripts. [23f1655] Fix pep8 and pyling issues. [0f1f9d2] Update acceptance test. [765a27d] Wait for ajax in captions. [8ae72a3] Fix logic. [063450f] Fix unit tests. [d1075fc] Fix handlers tests. [25d31ad] Update bumper_utils. [cb5f9df] Remove maxDiff. [8738b1a] Code cleanup. [87dbcb7] Fix issues with transcripts. [ec899de] Fix transcripts in serializers. [444b1fc] Fix transcripts typo. [d524cb5] Fix bumper. [f62cf22] Fix video mongo tests. [8f1b55a] Fix dispatches. [53bc308] Add more fixes. [d5e3723] Fix test_video_handlers and rename the method. [93efc23] Fix mobile tests. [740e2ae] Fix pep8 and pylint. [47cfb66] Address comments, add fixes. [4e499d9] Add fixes. [8353553] Add improvements. Updated dispatch values) . Use ddt in bumper handler tests. Move common metadata to single place. Fix style. Update docstring. Fix poster button. Improve bumper events. Fix test after rebase. Address comments. Download transcript: use def video lang, not bump. Renamed date_last_view_bumper to bumper_last_view_date. Rename do_not_show_again_bumper to bumper_... Address comments. Fix tests for download for en lang. Fix bumper logic. Update strings. Update resizer. Remove resizer. Fix unit tests. Add tests. Fix bumper events. Clean up tests. Fix pylint violations. Fix pep8 and pylint violations. Update docs and method names. Update events. Make /static/ prefix a must. Fix wrong code. --- .../models/settings/course_metadata.py | 3 + cms/envs/bok_choy.py | 3 + cms/envs/common.py | 9 + .../xmodule/css/video/accessible_menu.scss | 2 +- .../xmodule/xmodule/css/video/display.scss | 136 +++-- .../xmodule/xmodule/js/fixtures/poster.jpg | Bin 0 -> 13764 bytes .../xmodule/xmodule/js/fixtures/video.html | 45 +- .../xmodule/js/fixtures/video_all.html | 46 +- .../xmodule/js/fixtures/video_html5.html | 20 +- .../js/fixtures/video_no_captions.html | 17 +- .../js/fixtures/video_with_bumper.html | 36 ++ .../js/fixtures/video_yt_multiple.html | 101 +--- common/lib/xmodule/xmodule/js/spec/helper.js | 10 +- .../xmodule/js/spec/video/general_spec.js | 72 +-- .../xmodule/js/spec/video/html5_video_spec.js | 2 +- .../xmodule/js/spec/video/initialize_spec.js | 164 +----- .../spec/video/video_accessible_menu_spec.js | 19 +- .../js/spec/video/video_bumper_spec.js | 109 ++++ .../js/spec/video/video_caption_spec.js | 58 +- .../js/spec/video/video_context_menu_spec.js | 6 +- .../js/spec/video/video_control_spec.js | 319 ++-------- .../video/video_events_bumper_plugin_spec.js | 157 +++++ .../js/spec/video/video_events_plugin_spec.js | 166 ++++++ .../js/spec/video/video_focus_grabber_spec.js | 1 + .../js/spec/video/video_full_screen_spec.js | 102 ++++ .../video/video_play_pause_control_spec.js | 68 +++ .../spec/video/video_play_placeholder_spec.js | 151 +++++ .../video/video_play_skip_control_spec.js | 64 ++ .../js/spec/video/video_player_spec.js | 220 +------ .../js/spec/video/video_poster_spec.js | 42 ++ .../spec/video/video_progress_slider_spec.js | 20 + .../spec/video/video_quality_control_spec.js | 8 +- .../video/video_save_state_plugin_spec.js | 230 ++++++++ .../js/spec/video/video_skip_control_spec.js | 55 ++ .../js/spec/video/video_speed_control_spec.js | 9 + .../spec/video/video_volume_control_spec.js | 8 +- .../xmodule/js/src/video/00_resizer.js | 18 +- .../xmodule/js/src/video/01_initialize.js | 156 ++--- .../xmodule/js/src/video/02_html5_video.js | 85 ++- .../js/src/video/035_video_accessible_menu.js | 491 +++++++--------- .../xmodule/js/src/video/03_video_player.js | 163 ++--- .../xmodule/js/src/video/04_video_control.js | 198 +------ .../js/src/video/04_video_full_screen.js | 175 ++++++ .../js/src/video/05_video_quality_control.js | 29 +- .../js/src/video/06_video_progress_slider.js | 20 +- .../js/src/video/07_video_volume_control.js | 72 ++- .../js/src/video/08_video_speed_control.js | 88 ++- .../js/src/video/095_video_context_menu.js | 6 + .../xmodule/xmodule/js/src/video/09_bumper.js | 109 ++++ .../js/src/video/09_events_bumper_plugin.js | 112 ++++ .../xmodule/js/src/video/09_events_plugin.js | 129 ++++ .../js/src/video/09_play_pause_control.js | 87 +++ .../js/src/video/09_play_placeholder.js | 87 +++ .../js/src/video/09_play_skip_control.js | 84 +++ .../xmodule/xmodule/js/src/video/09_poster.js | 66 +++ .../js/src/video/09_save_state_plugin.js | 118 ++++ .../xmodule/js/src/video/09_skip_control.js | 74 +++ .../xmodule/js/src/video/09_video_caption.js | 211 ++++--- .../xmodule/js/src/video/10_commands.js | 27 +- .../xmodule/xmodule/js/src/video/10_main.js | 133 +++-- .../xmodule/modulestore/inheritance.py | 12 + .../xmodule/xmodule/video_module/__init__.py | 1 + .../xmodule/video_module/bumper_utils.py | 142 +++++ .../xmodule/video_module/transcripts_utils.py | 80 ++- .../xmodule/video_module/video_handlers.py | 56 +- .../xmodule/video_module/video_module.py | 138 +++-- .../xmodule/video_module/video_utils.py | 42 ++ .../xmodule/video_module/video_xfields.py | 13 +- .../test/acceptance/pages/lms/video/video.py | 58 +- .../pages/studio/settings_advanced.py | 1 + .../tests/video/test_video_events.py | 280 +++++++-- .../tests/video/test_video_module.py | 79 +++ common/test/db_cache/bok_choy_data.json | 2 +- .../courseware/tests/test_video_handlers.py | 287 ++++++--- .../courseware/tests/test_video_mongo.py | 556 +++++++++++------- .../mobile_api/video_outlines/serializers.py | 5 +- .../mobile_api/video_outlines/views.py | 3 +- lms/envs/bok_choy.py | 5 + lms/envs/common.py | 9 + lms/templates/video.html | 82 +-- 80 files changed, 4649 insertions(+), 2418 deletions(-) create mode 100644 common/lib/xmodule/xmodule/js/fixtures/poster.jpg create mode 100644 common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js create mode 100644 common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/04_video_full_screen.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_bumper.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_events_bumper_plugin.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_events_plugin.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_play_pause_control.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_play_placeholder.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_play_skip_control.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_poster.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_save_state_plugin.js create mode 100644 common/lib/xmodule/xmodule/js/src/video/09_skip_control.js create mode 100644 common/lib/xmodule/xmodule/video_module/bumper_utils.py diff --git a/cms/djangoapps/models/settings/course_metadata.py b/cms/djangoapps/models/settings/course_metadata.py index 2f22ba400f..7d37c193a5 100644 --- a/cms/djangoapps/models/settings/course_metadata.py +++ b/cms/djangoapps/models/settings/course_metadata.py @@ -78,6 +78,9 @@ class CourseMetadata(object): if not settings.FEATURES.get('ENABLE_TEAMS'): filtered_list.append('teams_configuration') + if not settings.FEATURES.get('ENABLE_VIDEO_BUMPER'): + filtered_list.append('video_bumper') + return filtered_list @classmethod diff --git a/cms/envs/bok_choy.py b/cms/envs/bok_choy.py index df8d0b73b1..dcd120abf1 100644 --- a/cms/envs/bok_choy.py +++ b/cms/envs/bok_choy.py @@ -74,6 +74,9 @@ FEATURES['ENABLE_TEAMS'] = True # Enable custom content licensing FEATURES['LICENSING'] = True +FEATURES['ENABLE_MOBILE_REST_API'] = True # Enable video bumper in Studio +FEATURES['ENABLE_VIDEO_BUMPER'] = True # Enable video bumper in Studio settings + ########################### Entrance Exams ################################# FEATURES['ENTRANCE_EXAMS'] = True diff --git a/cms/envs/common.py b/cms/envs/common.py index 39a3008c30..f8d9e27354 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -163,6 +163,13 @@ FEATURES = { # Teams feature 'ENABLE_TEAMS': False, + + # Show video bumper in Studio + 'ENABLE_VIDEO_BUMPER': False, + + # How many seconds to show the bumper again, default is 7 days: + 'SHOW_BUMPER_PERIODICITY': 7 * 24 * 3600, + } ENABLE_JASMINE = False @@ -645,6 +652,8 @@ YOUTUBE = { 'v': 'set_youtube_id_of_11_symbols_here', }, }, + + 'IMAGE_API': 'http://img.youtube.com/vi/{youtube_id}/0.jpg', # /maxresdefault.jpg for 1920*1080 } ############################# VIDEO UPLOAD PIPELINE ############################# diff --git a/common/lib/xmodule/xmodule/css/video/accessible_menu.scss b/common/lib/xmodule/xmodule/css/video/accessible_menu.scss index 3c283df770..05739ef2ad 100644 --- a/common/lib/xmodule/xmodule/css/video/accessible_menu.scss +++ b/common/lib/xmodule/xmodule/css/video/accessible_menu.scss @@ -22,7 +22,7 @@ $a11y--blue-s1: saturate($blue,15%); } .a11y-menu-list { - @extend %ui-depth1; + @extend %ui-depth3; top: 100%; margin: 0; padding: 0; diff --git a/common/lib/xmodule/xmodule/css/video/display.scss b/common/lib/xmodule/xmodule/css/video/display.scss index 8bbfdd1b25..eb7ae84149 100644 --- a/common/lib/xmodule/xmodule/css/video/display.scss +++ b/common/lib/xmodule/xmodule/css/video/display.scss @@ -27,6 +27,23 @@ div.video { } } + // CASE: video pre-roll state + &.is-pre-roll { + .slider { + visibility: hidden; + } + + .video-player { + position: relative; + &:before { + display: block; + content: ""; + width: 100%; + padding-top: 55%; + } + } + } + div.tc-wrapper { @include clearfix(); position: relative; @@ -169,6 +186,7 @@ div.video { } object, iframe, video { + display: block; border: none; width: 100%; } @@ -282,7 +300,7 @@ div.video { } } - ul.vcr { + .vcr { float: left; list-style: none; margin: 0 lh() 0 0; @@ -293,49 +311,52 @@ div.video { font-size: em(14); } - li { + .video_control { + @extend %video-button; float: left; - margin-bottom: 0; + background-image: url('../images/vcr.png'); + background-position: 15px 15px ; + background-repeat: no-repeat; + border-left: none; + padding: 0 lh(.75); + width: 14px; - a { - @extend %video-button; - background-image: url('../images/vcr.png'); - background-position: 15px 15px ; - background-repeat: no-repeat; - border-left: none; - box-shadow: 1px 0 0 #555; - padding: 0 lh(.75); - width: 14px; - - &:focus { - @extend %ui-depth4; - position: relative; - outline: $white dotted thin; - outline-offset: -2px; - } - - &:empty { - height: 46px; - background-position: 15px 15px; - } - - &.play { - background-position: 17px -114px; - } - - &.pause { - background-position: 16px -50px; - } + &:focus { + @extend %ui-depth4; + position: relative; + outline: $white dotted thin; + outline-offset: -2px; } - div.vidtime { - font-weight: bold; - line-height: 46px; //height of play pause buttons - -webkit-font-smoothing: antialiased; - padding-left: lh(.75); - @media (max-width: 1120px) { - padding-left: lh(0.5); - } + &:empty { + height: 46px; + background-position: 15px 15px; + } + + &.play { + background-position: 17px -114px; + } + + &.pause { + background-position: 16px -50px; + } + + &.skip { + background-image: none; + text-indent: 0; + width: initial; + white-space: nowrap; + } + } + + div.vidtime { + @extend %t-strong; + float: left; + line-height: 46px; //height of play pause buttons + -webkit-font-smoothing: antialiased; + padding-left: lh(.75); + @media (max-width: 1120px) { + padding-left: lh(0.5); } } } @@ -504,11 +525,14 @@ div.video { background-image: url('../images/volume.png'); background-position: 10px center; background-repeat: no-repeat; - border-left: none; width: 30px; height: 46px; } + &:not(:first-child) > a { + border-left: none; + } + .volume-slider-container { @include transition(none); @extend %ui-depth1; @@ -686,8 +710,7 @@ div.video { } ol.subtitles { - width: 0; - height: 0; + @extend .is-hidden; } ol.subtitles.html5 { @@ -792,13 +815,38 @@ div.video { &.is-touch { div.tc-wrapper { article.video-wrapper { - object, iframe, video{ + object, iframe, video { width: 100%; height: 100%; } } } } + + .video-pre-roll { + @extend %ui-depth3; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-position: 50% 50%; + background-repeat: no-repeat; + background-size: 100%; + background-color: $black; + + &.is-html5 { + background-size: 15%; + } + + .btn-play { + text-indent: -999px; + overflow: hidden; + border: none; + box-shadow: none; + line-height: 0; + } + } } diff --git a/common/lib/xmodule/xmodule/js/fixtures/poster.jpg b/common/lib/xmodule/xmodule/js/fixtures/poster.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7f987f944e2dc670f2687b216cdead3579effc8d GIT binary patch literal 13764 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 T0rw04@4R&Le?}#LJN^FvWh@Sj literal 0 HcmV?d00001 diff --git a/common/lib/xmodule/xmodule/js/fixtures/video.html b/common/lib/xmodule/xmodule/js/fixtures/video.html index e1fe11ae47..dabb3801b9 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video.html @@ -4,22 +4,7 @@
@@ -35,35 +20,11 @@ - -
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_all.html b/common/lib/xmodule/xmodule/js/fixtures/video_all.html index 1b0727d3d7..617d958357 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_all.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_all.html @@ -4,23 +4,7 @@
@@ -36,35 +20,11 @@ - -
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html index 678803a90d..47be4f04fc 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_html5.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_html5.html @@ -4,23 +4,7 @@
@@ -33,8 +17,6 @@ - -
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 a34df976bf..77017d403d 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_no_captions.html @@ -4,22 +4,7 @@
diff --git a/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html new file mode 100644 index 0000000000..22bd206268 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/fixtures/video_with_bumper.html @@ -0,0 +1,36 @@ +
+
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+
+
+
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 8086c2b269..8842b1e592 100644 --- a/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html +++ b/common/lib/xmodule/xmodule/js/fixtures/video_yt_multiple.html @@ -4,22 +4,7 @@
@@ -35,35 +20,11 @@ - -
@@ -77,20 +38,7 @@
@@ -142,20 +68,7 @@
diff --git a/common/lib/xmodule/xmodule/js/spec/helper.js b/common/lib/xmodule/xmodule/js/spec/helper.js index f188c9c639..97d422d5d8 100644 --- a/common/lib/xmodule/xmodule/js/spec/helper.js +++ b/common/lib/xmodule/xmodule/js/spec/helper.js @@ -206,6 +206,9 @@ }, toBeInArray: function (array) { return $.inArray(this.actual, array) > -1; + }, + toBeFocused: function () { + return $(this.actual)[0] === $(this.actual)[0].ownerDocument.activeElement; } }); @@ -239,12 +242,11 @@ loadFixtures('video_all.html'); } - // If `params` is an object, assign it's properties as data attributes + // If `params` is an object, assign its properties as data attributes // to the main video DIV element. if (_.isObject(params)) { - $('#example') - .find('#video_id') - .data(params); + var metadata = _.extend($('#video_id').data('metadata'), params); + $('#video_id').data('metadata', metadata); } jasmine.stubRequests(); 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 72fc269fd8..7cefab5e49 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -1,6 +1,6 @@ (function (undefined) { describe('Video', function () { - var oldOTBD; + var oldOTBD, state; beforeEach(function () { jasmine.stubRequests(); @@ -17,11 +17,12 @@ beforeEach(function () { loadFixtures('video.html'); $.cookie.andReturn('0.50'); + this.state = jasmine.initializePlayerYouTube('video_html5.html'); }); describe('by default', function () { - beforeEach(function () { - this.state = new window.Video('#example'); + afterEach(function () { + this.state.videoPlayer.destroy(); }); it('check videoType', function () { @@ -54,19 +55,16 @@ var state; beforeEach(function () { - loadFixtures('video_html5.html'); $.cookie.andReturn('0.75'); + state = jasmine.initializePlayer('video_html5.html'); + }); + + afterEach(function () { + state.videoPlayer.destroy(); + state = undefined; }); describe('by default', function () { - beforeEach(function () { - state = new window.Video('#example'); - }); - - afterEach(function () { - state = undefined; - }); - it('check videoType', function () { expect(state.videoType).toEqual('html5'); }); @@ -95,14 +93,6 @@ // the stand alone HTML5 player object is already loaded, so no // further testing in that case is required. describe('HTML5 API is available', function () { - beforeEach(function () { - state = new Video('#example'); - }); - - afterEach(function () { - state = null; - }); - it('create the Video Player', function () { expect(state.videoPlayer.player).not.toBeUndefined(); }); @@ -113,8 +103,11 @@ describe('YouTube API is not loaded', function () { beforeEach(function () { window.YT = undefined; + state = jasmine.initializePlayerYouTube(); + }) - state = jasmine.initializePlayerYouTube('video.html'); + afterEach(function () { + state.videoPlayer.destroy(); }); it('callback, to be called after YouTube API loads, exists and is called', function () { @@ -159,9 +152,8 @@ } ]; - beforeEach(function () { - loadFixtures('video.html'); - + afterEach(function () { + state.videoPlayer.destroy(); }); $.each(miniTestSuite, function (index, test) { @@ -172,13 +164,10 @@ function itFabrique(itDescription, data, expectData) { it(itDescription, function () { - $('#example').find('.video') - .data({ - 'start': data.start, - 'end': data.end - }); - - state = new Video('#example'); + state = jasmine.initializePlayer('video.html', { + 'start': data.start, + 'end': data.end + }); expect(state.config.startTime).toBe(expectData.start); expect(state.config.endTime).toBe(expectData.end); @@ -238,26 +227,5 @@ expect(numAjaxCalls).toBe(1); }); }); - - describe('log', function () { - beforeEach(function () { - loadFixtures('video_html5.html'); - state = new Video('#example'); - spyOn(Logger, 'log'); - state.videoPlayer.log('someEvent', { - currentTime: 25, - speed: '1.0' - }); - }); - - it('call the logger with valid extra parameters', function () { - expect(Logger.log).toHaveBeenCalledWith('someEvent', { - id: 'id', - code: 'html5', - currentTime: 25, - speed: '1.0' - }); - }); - }); }); }).call(this); 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 baefb4dea1..b22cdd375e 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 @@ -10,8 +10,8 @@ afterEach(function () { state.storage.clear(); + state.videoPlayer.destroy(); $.fn.scrollTo.reset(); - $('.subtitles').remove(); $('source').remove(); window.onTouchBasedDevice = oldOTBD; }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js index e08ba56a75..f3194b8bce 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/initialize_spec.js @@ -12,158 +12,6 @@ function (Initialize) { state = {}; }); - describe('saveState function', function () { - var videoPlayerCurrentTime, newCurrentTime, speed; - - // We make sure that `currentTime` is a float. We need to test - // that Math.round() is called. - videoPlayerCurrentTime = 3.1242; - - // We have two times, because one is stored in - // `videoPlayer.currentTime`, and the other is passed directly to - // `saveState` in `data` object. In each case, there is different - // code that handles these times. They have to be different for - // test completeness sake. Also, make sure it is float, as is the - // time above. - newCurrentTime = 5.4; - - speed = '0.75'; - - beforeEach(function () { - state = { - videoPlayer: { - currentTime: videoPlayerCurrentTime - }, - storage: { - setItem: jasmine.createSpy() - }, - config: { - saveStateUrl: 'http://example.com/save_user_state' - } - }; - - spyOn($, 'ajax'); - spyOn(Time, 'formatFull').andCallThrough(); - }); - - it('data is not an object, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: videoPlayerCurrentTime, - data: undefined, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime)) - } - }); - }); - - it('data contains speed, async is false', function () { - itSpec({ - asyncVal: false, - speedVal: speed, - positionVal: undefined, - data: { - speed: speed - }, - ajaxData: { - speed: speed - } - }); - }); - - it('data contains float position, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: newCurrentTime, - data: { - saved_video_position: newCurrentTime - }, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(newCurrentTime)) - } - }); - }); - - it('data contains speed and rounded position, async is false', function () { - itSpec({ - asyncVal: false, - speedVal: speed, - positionVal: Math.round(newCurrentTime), - data: { - speed: speed, - saved_video_position: Math.round(newCurrentTime) - }, - ajaxData: { - speed: speed, - saved_video_position: Time.formatFull(Math.round(newCurrentTime)) - } - }); - }); - - it('data contains empty object, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: undefined, - data: {}, - ajaxData: {} - }); - }); - - it('data contains position 0, async is true', function () { - itSpec({ - asyncVal: true, - speedVal: undefined, - positionVal: 0, - data: { - saved_video_position: 0 - }, - ajaxData: { - saved_video_position: Time.formatFull(Math.round(0)) - } - }); - }); - - return; - - function itSpec(value) { - var asyncVal = value.asyncVal, - speedVal = value.speedVal, - positionVal = value.positionVal, - data = value.data, - ajaxData = value.ajaxData; - - Initialize.prototype.saveState.call(state, asyncVal, data); - - if (speedVal) { - expect(state.storage.setItem).toHaveBeenCalledWith( - 'speed', - speedVal, - true - ); - } - if (positionVal) { - expect(state.storage.setItem).toHaveBeenCalledWith( - 'savedVideoPosition', - positionVal, - true - ); - expect(Time.formatFull).toHaveBeenCalledWith( - positionVal - ); - } - expect($.ajax).toHaveBeenCalledWith({ - url: state.config.saveStateUrl, - type: 'POST', - async: asyncVal, - dataType: 'json', - data: ajaxData - }); - } - }); - describe('getCurrentLanguage', function () { var msg; @@ -356,20 +204,12 @@ function (Initialize) { describe('when new speed is available', function () { beforeEach(function () { - Initialize.prototype.setSpeed.call(state, '0.75', true); + Initialize.prototype.setSpeed.call(state, '0.75'); }); it('set new speed', function () { expect(state.speed).toEqual('0.75'); }); - - it('save setting for new speed', function () { - expect(state.storage.setItem.calls[0].args) - .toEqual(['speed', '0.75', true]); - - expect(state.storage.setItem.calls[1].args) - .toEqual(['general_speed', '0.75']); - }); }); describe('when new speed is not available', function () { @@ -390,7 +230,7 @@ function (Initialize) { }; $.each(map, function(key, expected) { - Initialize.prototype.setSpeed.call(state, key, true); + Initialize.prototype.setSpeed.call(state, key); expect(state.speed).toBe(expected); }); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js index feb332122c..a790f8ecae 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_accessible_menu_spec.js @@ -5,6 +5,7 @@ afterEach(function () { $('source').remove(); state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -56,24 +57,6 @@ }); */ }); - - it('add ARIA attributes to button, menu, and menu items links', - function () { - expect(button).toHaveAttrs({ - 'role': 'button', - 'title': '.srt', - 'aria-disabled': 'false' - }); - - expect(menuList).toHaveAttr('role', 'menu'); - - menuItemsLinks.each(function(){ - expect($(this)).toHaveAttrs({ - 'role': 'menuitem', - 'aria-disabled': 'false' - }); - }); - }); }); describe('when running', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js new file mode 100644 index 0000000000..0a355e918d --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_bumper_spec.js @@ -0,0 +1,109 @@ +(function (WAIT_TIMEOUT) { + 'use strict'; + describe('VideoBumper', function () { + var state, oldOTBD, waitForPlaying; + + waitForPlaying = function (state) { + waitsFor(function () { + return state.el.hasClass('is-playing'); + }, 'Player is not playing.', WAIT_TIMEOUT); + }; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + jasmine.Clock.useMock(); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the bumper video', function () { + expect($('.is-bumper')).toExist(); + }); + + it('can show the main video on error', function () { + state.el.trigger('error'); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can show the main video once bumper ends', function () { + state.el.trigger('ended'); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can show the main video on skip', function () { + state.bumperState.videoBumper.skip(); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can stop the bumper video playing if it is too long', function () { + state.el.trigger('timeupdate', [state.bumperState.videoBumper.maxBumperDuration + 1]); + jasmine.Clock.tick(20); + expect($('.is-bumper')).not.toExist(); + waitForPlaying(state); + }); + + it('can save appropriate states correctly on ended', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.el.trigger('ended'); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on skip', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.bumperState.videoBumper.skip(); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on error', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.el.trigger('error'); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true}); + }); + + it('can save appropriate states correctly on skip and do not show again', function () { + var saveState = jasmine.createSpy('saveState'); + state.bumperState.videoSaveStatePlugin.saveState = saveState; + state.bumperState.videoBumper.skipAndDoNotShowAgain(); + expect(state.storage.getItem('isBumperShown')).toBeTruthy(); + jasmine.Clock.tick(20); + expect(saveState).toHaveBeenCalledWith(true, { + bumper_last_view_date: true, bumper_do_not_show_again: true}); + }); + + it('can destroy itself', function () { + state.bumperState.videoBumper.destroy(); + expect(state.videoBumper).toBeUndefined(); + }); + }); +}).call(this, window.WAIT_TIMEOUT); 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 39778d7ba6..f269bde541 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 @@ -11,14 +11,13 @@ }); afterEach(function () { - $('.subtitles').remove(); - // `source` tags should be removed to avoid memory leak bug that we // had before. Removing of `source` tag, not `video` tag, stops // loading video source and clears the memory. $('source').remove(); $.fn.scrollTo.reset(); state.storage.clear(); + state.videoPlayer.destroy(); window.onTouchBasedDevice = oldOTBD; }); @@ -121,11 +120,6 @@ }); }); - it('bind the hide caption button', function () { - state = jasmine.initializePlayer(); - expect($('.hide-subtitles')).toHandle('click'); - }); - it('bind the mouse movement', function () { state = jasmine.initializePlayer(); expect($('.subtitles')).toHandle('mouseover'); @@ -143,6 +137,27 @@ }); + it('can destroy itself', function () { + spyOn($, 'ajaxWithPrefix'); + state = jasmine.initializePlayer(); + var plugin = state.videoCaption; + + spyOn($.fn, 'off').andCallThrough(); + state.videoCaption.destroy(); + + expect(state.videoCaption).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'caption:fetch': plugin.fetchCaption, + 'caption:resize': plugin.onResize, + 'caption:update': plugin.onCaptionUpdate, + 'ended': plugin.pause, + 'fullscreen': plugin.onResize, + 'pause': plugin.pause, + 'play': plugin.play, + 'destroy': plugin.destroy + }); + }); + describe('renderLanguageMenu', function () { describe('is rendered', function () { it('if languages more than 1', function () { @@ -593,7 +608,7 @@ it(msg, function () { spyOn(Caption, 'fetchAvailableTranslations'); $.ajax.andCallFake(function (settings) { - settings.error([]); + _.result(settings, 'error'); }); state.config.transcriptLanguages = {}; @@ -612,7 +627,7 @@ xit(msg, function () { $.ajax .andCallFake(function (settings) { - settings.error([]); + _.result(settings, 'error'); }); state.config.transcriptLanguages = { @@ -690,7 +705,7 @@ msg = 'on error: captions are hidden if there are no transcript'; it(msg, function () { $.ajax.andCallFake(function (settings) { - settings.error(); + _.result(settings, 'error'); }); Caption.fetchAvailableTranslations(); @@ -907,8 +922,8 @@ $('.subtitles').css('maxHeight'), 10 ); videoWrapperHeight = $('.video-wrapper').height(); - progressSliderHeight = videoControl.sliderEl.height(); - controlHeight = videoControl.el.height(); + progressSliderHeight = state.el.find('.slider').height(); + controlHeight = state.el.find('.video-controls').height(); shouldBeHeight = videoWrapperHeight - 0.5 * progressSliderHeight - controlHeight; @@ -1043,7 +1058,6 @@ describe('toggle', function () { beforeEach(function () { state = jasmine.initializePlayer(); - spyOn(state.videoPlayer, 'log'); $('.subtitles li[data-index=1]').addClass('current'); }); @@ -1053,15 +1067,6 @@ state.videoCaption.toggle(jQuery.Event('click')); }); - it('log the hide_transcript event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'hide_transcript', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); - it('hide the caption', function () { expect(state.el).toHaveClass('closed'); }); @@ -1079,15 +1084,6 @@ jasmine.Clock.useMock(); }); - it('log the show_transcript event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'show_transcript', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); - it('show the caption', function () { expect(state.el).not.toHaveClass('closed'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js index 269a75053c..295b151a4f 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_context_menu_spec.js @@ -68,6 +68,7 @@ $('source').remove(); _.result(state.storage, 'clear'); _.result($('video').data('contextmenu'), 'destroy'); + _.result(state.videoPlayer, 'destroy'); }); describe('constructor', function () { @@ -219,12 +220,13 @@ it('mouse left/right-clicking behaves as expected on play/pause menu item', function () { var menuItem = menuItems.first(); + spyOn(state.videoPlayer, 'isPlaying'); spyOn(state.videoPlayer, 'play').andCallFake(function () { - state.videoControl.isPlaying = true; + state.videoPlayer.isPlaying.andReturn(true); state.el.trigger('play'); }); spyOn(state.videoPlayer, 'pause').andCallFake(function () { - state.videoControl.isPlaying = false; + state.videoPlayer.isPlaying.andReturn(false); state.el.trigger('pause'); }); // Left-click on 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 98569620c2..85794dc2d5 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 @@ -13,12 +13,13 @@ afterEach(function () { $('source').remove(); state.storage.clear(); - window.Video.previousState = null; + state.videoPlayer.destroy(); window.onTouchBasedDevice = oldOTBD; }); describe('constructor', function () { beforeEach(function () { + window.VideoState = {}; state = jasmine.initializePlayer(); }); @@ -28,83 +29,13 @@ '.slider', 'ul.vcr', 'a.play', - '.vidtime', - '.add-fullscreen' + '.vidtime' ].join(',') ); expect($('.video-controls').find('.vidtime')) .toHaveText('0:00 / 0:00'); }); - - it('add ARIA attributes to time control', function () { - var timeControl = $('div.slider > a'); - - expect(timeControl).toHaveAttrs({ - 'role': 'slider', - 'title': 'Video position', - 'aria-disabled': 'false' - }); - - expect(timeControl).toHaveAttr('aria-valuetext'); - }); - - it('add ARIA attributes to play control', function () { - var playControl = $('ul.vcr a'); - - expect(playControl).toHaveAttrs({ - 'role': 'button', - 'title': 'Play', - 'aria-disabled': 'false' - }); - }); - - it('add ARIA attributes to fullscreen control', function () { - var fullScreenControl = $('a.add-fullscreen'); - - expect(fullScreenControl).toHaveAttrs({ - 'role': 'button', - 'title': 'Fill browser', - 'aria-disabled': 'false' - }); - }); - - it('bind the playback button', function () { - expect($('.video_control')).toHandleWith( - 'click', - state.videoControl.togglePlayback - ); - }); - - describe('when on a non-touch based device', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - }); - - it('add the play class to video control', function () { - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr( - 'title', 'Play' - ); - }); - }); - - describe('when on a touch based device', function () { - beforeEach(function () { - window.onTouchBasedDevice.andReturn(['iPad']); - state = jasmine.initializePlayer(); - }); - - it( - 'does not add the play class to video control', - function () - { - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr( - 'title', 'Play' - ); - }); - }); }); describe('constructor with start-time', function () { @@ -115,6 +46,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 0 @@ -147,6 +79,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 15 @@ -181,6 +114,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: -15 @@ -215,6 +149,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 'a' @@ -249,6 +184,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, savedVideoPosition: 10000 @@ -278,13 +214,14 @@ describe('constructor with end-time', function () { it( - 'saved position is 0, timer slider and VCR set to 0:00 ' + + 'saved position is 0, timer slider and VCR set to 0:00 ' + 'and ending at specified end-time', function () { var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 0 @@ -319,6 +256,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 15 @@ -353,6 +291,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: -15 @@ -387,6 +326,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 'a' @@ -422,6 +362,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ end: 20, savedVideoPosition: 10000 @@ -457,6 +398,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -492,6 +434,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -527,6 +470,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -562,6 +506,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -597,6 +542,7 @@ var duration, sliderEl, expectedValue; runs(function () { + window.VideoState = {}; state = jasmine.initializePlayer({ start: 10, end: 20, @@ -625,217 +571,8 @@ }); }); - it('Controls height is actual on switch to fullscreen', function () { - spyOn($.fn, 'height').andCallFake(function (val) { - return _.isUndefined(val) ? 100: this; - }); - - state = jasmine.initializePlayer(); - $(state.el).trigger('fullscreen'); - - expect(state.videoControl.height).toBe(150); - }); - - describe('play', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - state.videoControl.play(); - }); - - it('switch playback button to play state', function () { - expect($('.video_control')).not.toHaveClass('play'); - expect($('.video_control')).toHaveClass('pause'); - expect($('.video_control')).toHaveAttr('title', 'Pause'); - }); - }); - - describe('pause', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - state.videoControl.pause(); - }); - - it('switch playback button to pause state', function () { - expect($('.video_control')).not.toHaveClass('pause'); - expect($('.video_control')).toHaveClass('play'); - expect($('.video_control')).toHaveAttr('title', 'Play'); - }); - }); - - describe('togglePlayback', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - }); - - describe( - 'when the control does not have play or pause class', - function () - { - beforeEach(function () { - $('.video_control').removeClass('play') - .removeClass('pause'); - }); - - describe('when the video is playing', function () { - beforeEach(function () { - $('.video_control').addClass('play'); - spyOnEvent(state.videoControl, 'pause'); - state.videoControl.togglePlayback( - $.Event('click') - ); - }); - - it('does not trigger the pause event', function () { - expect('pause').not - .toHaveBeenTriggeredOn(state.videoControl); - }); - }); - - describe('when the video is paused', function () { - beforeEach(function () { - $('.video_control').addClass('pause'); - spyOnEvent(state.videoControl, 'play'); - state.videoControl.togglePlayback( - $.Event('click') - ); - }); - - it('does not trigger the play event', function () { - expect('play').not - .toHaveBeenTriggeredOn(state.videoControl); - }); - }); - }); - }); - - describe('Play placeholder', function () { - 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'] - } - ]; - - beforeEach(function () { - jasmine.stubRequests(); - - spyOn(window.YT, 'Player').andCallThrough(); - }); - - it ('works correctly on calling proper methods', function () { - var btnPlay; - - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.showPlayPlaceholder(); - - expect(btnPlay).not.toHaveClass('is-hidden'); - expect(btnPlay).toHaveAttrs({ - 'aria-hidden': 'false', - 'tabindex': 0 - }); - - state.videoControl.hidePlayPlaceholder(); - - expect(btnPlay).toHaveClass('is-hidden'); - expect(btnPlay).toHaveAttrs({ - 'aria-hidden': 'true', - 'tabindex': -1 - }); - }); - - $.each(cases, function (index, data) { - var message = [ - (data.isShown) ? 'is' : 'is not', - ' shown on', - data.name - ].join(''); - - it(message, function () { - var btnPlay; - - window.onTouchBasedDevice.andReturn(data.isTouch); - state = jasmine.initializePlayer(); - 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 () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - state.videoControl.pause(); - - expect(btnPlay).not.toHaveClass('is-hidden'); - }); - - it( - 'is hidden on playing video on ' + device + - ' in HTML5 player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayer(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - - expect(btnPlay).toHaveClass('is-hidden'); - }); - - it( - 'is hidden on paused video on ' + device + - ' in YouTube player', - function () - { - var btnPlay; - - window.onTouchBasedDevice.andReturn([device]); - state = jasmine.initializePlayerYouTube(); - btnPlay = state.el.find('.btn-play'); - - state.videoControl.play(); - state.videoControl.pause(); - - expect(btnPlay).toHaveClass('is-hidden'); - }); - }); - }); - it('show', function () { var controls; - state = jasmine.initializePlayer(); controls = state.el.find('.video-controls'); controls.addClass('is-hidden'); @@ -843,5 +580,23 @@ state.videoControl.show(); expect(controls).not.toHaveClass('is-hidden'); }); + + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoControl.destroy(); + expect(state.videoControl).toBeUndefined(); + }); + + it('can focus the first control', function () { + var btnPlay; + state = jasmine.initializePlayer({focusFirstControl: true}); + btnPlay = state.el.find('.video-controls .play'); + waitsFor(function () { + return state.el.hasClass('is-initialized'); + }, 'Player is not initialized', WAIT_TIMEOUT); + runs(function () { + expect(btnPlay).toBeFocused(); + }); + }); }); }).call(this, window.WAIT_TIMEOUT); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js new file mode 100644 index 0000000000..e41b40e782 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_events_bumper_plugin_spec.js @@ -0,0 +1,157 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Events Bumper plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer('video_with_bumper.html'); + spyOn(Logger, 'log'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoEventsBumperPlugin, 'getCurrentTime').andReturn(10); + spyOn(state.bumperState.videoEventsBumperPlugin, 'getDuration').andReturn(20); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + it('can emit "edx.video.bumper.loaded" event', function () { + state.el.trigger('ready'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.loaded', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.played" event', function () { + state.el.trigger('play'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.played', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.stopped" event', function () { + state.el.trigger('ended'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + + Logger.log.reset(); + state.el.trigger('stop'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.stopped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.skipped" event', function () { + state.el.trigger('skip', [false]); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.skipped', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.dismissed" event', function () { + state.el.trigger('skip', [true]); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.dismissed', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.menu.shown" event', function () { + state.el.trigger('language_menu:show'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.shown', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.menu.hidden" event', function () { + state.el.trigger('language_menu:hide'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.menu.hidden', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.shown" event', function () { + state.el.trigger('captions:show'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.shown', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can emit "edx.video.bumper.transcript.hidden" event', function () { + state.el.trigger('captions:hide'); + expect(Logger.log).toHaveBeenCalledWith('edx.video.bumper.transcript.hidden', { + host_component_id: 'id', + bumper_id: 'xmodule/include/fixtures/test.mp4', + code: 'html5', + currentTime: 10, + duration: 20 + }); + }); + + it('can destroy itself', function () { + var plugin = state.bumperState.videoEventsBumperPlugin; + spyOn($.fn, 'off').andCallThrough(); + plugin.destroy(); + expect(state.bumperState.videoEventsBumperPlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'ready': plugin.onReady, + 'play': plugin.onPlay, + 'ended stop': plugin.onEnded, + 'skip': plugin.onSkip, + 'language_menu:show': plugin.onShowLanguageMenu, + 'language_menu:hide': plugin.onHideLanguageMenu, + 'captions:show': plugin.onShowCaptions, + 'captions:hide': plugin.onHideCaptions, + 'destroy': plugin.destroy + }); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js new file mode 100644 index 0000000000..78352e575f --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_events_plugin_spec.js @@ -0,0 +1,166 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Events plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer(); + spyOn(Logger, 'log'); + spyOn(state.videoEventsPlugin, 'getCurrentTime').andReturn(10); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + it('can emit "load_video" event', function () { + state.el.trigger('ready'); + expect(Logger.log).toHaveBeenCalledWith('load_video', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "play_video" event', function () { + state.el.trigger('play'); + expect(Logger.log).toHaveBeenCalledWith('play_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "pause_video" event', function () { + state.el.trigger('pause'); + expect(Logger.log).toHaveBeenCalledWith('pause_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "speed_change_video" event', function () { + state.el.trigger('speedchange', ['2.0', '1.0']); + expect(Logger.log).toHaveBeenCalledWith('speed_change_video', { + id: 'id', + code: 'html5', + current_time: 10, + old_speed: '1.0', + new_speed: '2.0' + }); + }); + + it('can emit "seek_video" event', function () { + state.el.trigger('seek', [1, 0, 'any']); + expect(Logger.log).toHaveBeenCalledWith('seek_video', { + id: 'id', + code: 'html5', + old_time: 0, + new_time: 1, + type: 'any' + }); + }); + + it('can emit "stop_video" event', function () { + state.el.trigger('ended'); + expect(Logger.log).toHaveBeenCalledWith('stop_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + + Logger.log.reset(); + state.el.trigger('stop'); + expect(Logger.log).toHaveBeenCalledWith('stop_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "skip_video" event', function () { + state.el.trigger('skip', [false]); + expect(Logger.log).toHaveBeenCalledWith('skip_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "do_not_show_again_video" event', function () { + state.el.trigger('skip', [true]); + expect(Logger.log).toHaveBeenCalledWith('do_not_show_again_video', { + id: 'id', + code: 'html5', + currentTime: 10 + }); + }); + + it('can emit "video_show_cc_menu" event', function () { + state.el.trigger('language_menu:show'); + expect(Logger.log).toHaveBeenCalledWith('video_show_cc_menu', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "video_hide_cc_menu" event', function () { + state.el.trigger('language_menu:hide'); + expect(Logger.log).toHaveBeenCalledWith('video_hide_cc_menu', { + id: 'id', + code: 'html5' + }); + }); + + it('can emit "show_transcript" event', function () { + state.el.trigger('captions:show'); + expect(Logger.log).toHaveBeenCalledWith('show_transcript', { + id: 'id', + code: 'html5', + current_time: 10 + }); + }); + + it('can emit "hide_transcript" event', function () { + state.el.trigger('captions:hide'); + expect(Logger.log).toHaveBeenCalledWith('hide_transcript', { + id: 'id', + code: 'html5', + current_time: 10 + }); + }); + + it('can destroy itself', function () { + var plugin = state.videoEventsPlugin; + spyOn($.fn, 'off').andCallThrough(); + state.videoEventsPlugin.destroy(); + expect(state.videoEventsPlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'ready': plugin.onReady, + 'play': plugin.onPlay, + 'pause': plugin.onPause, + 'ended stop': plugin.onEnded, + 'seek': plugin.onSeek, + 'skip': plugin.onSkip, + 'speedchange': plugin.onSpeedChange, + 'language_menu:show': plugin.onShowLanguageMenu, + 'language_menu:hide': plugin.onHideLanguageMenu, + 'captions:show': plugin.onShowCaptions, + 'captions:hide': plugin.onHideCaptions, + 'destroy': plugin.destroy + }); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js index ab3c12df6f..5f69d2c7c7 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/video_focus_grabber_spec.js @@ -26,6 +26,7 @@ afterEach(function () { // Turn jQuery animations back on. jQuery.fx.off = true; + state.videoPlayer.destroy(); }); it( diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js new file mode 100644 index 0000000000..215b891f41 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_full_screen_spec.js @@ -0,0 +1,102 @@ +(function () { + 'use strict'; + describe('VideoFullScreen', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + describe('constructor', function () { + beforeEach(function () { + state = jasmine.initializePlayer(); + }); + + it('renders the fullscreen control', function () { + expect($('.add-fullscreen')).toExist(); + expect(state.videoFullScreen.fullScreenState).toBe(false); + }); + + it('correctly adds ARIA attributes to fullscreen control', function () { + var fullScreenControl = $('.add-fullscreen'); + + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Fill browser', + 'aria-disabled': 'false' + }); + }); + + it('correctly triggers the event handler to toggle fullscreen mode', function () { + spyOn(state.videoFullScreen, 'exit'); + spyOn(state.videoFullScreen, 'enter'); + + state.videoFullScreen.fullScreenState = false; + state.videoFullScreen.toggle(); + expect(state.videoFullScreen.enter).toHaveBeenCalled(); + + state.videoFullScreen.fullScreenState = true; + state.videoFullScreen.toggle(); + expect(state.videoFullScreen.exit).toHaveBeenCalled(); + }); + + it('correctly updates ARIA on state change', function () { + var fullScreenControl = $('.add-fullscreen'); + fullScreenControl.click(); + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Exit full browser', + 'aria-disabled': 'false' + }); + fullScreenControl.click(); + expect(fullScreenControl).toHaveAttrs({ + 'role': 'button', + 'title': 'Fill browser', + 'aria-disabled': 'false' + }); + }); + + it('correctly can out of fullscreen by pressing esc', function () { + spyOn(state.videoCommands, 'execute'); + var esc = $.Event('keyup'); + esc.keyCode = 27; + state.isFullScreen = true; + $(document).trigger(esc); + expect(state.videoCommands.execute).toHaveBeenCalledWith('toggleFullScreen'); + }); + + it('can update video dimensions on state change', function () { + state.el.trigger('fullscreen', [true]); + expect(state.resizer.setMode).toHaveBeenCalledWith('both'); + state.el.trigger('fullscreen', [false]); + expect(state.resizer.setMode).toHaveBeenCalledWith('width'); + }); + + it('can destroy itself', function () { + state.videoFullScreen.destroy(); + expect($('.add-fullscreen')).not.toExist(); + expect(state.videoFullScreen).toBeUndefined(); + }); + }); + + it('Controls height is actual on switch to fullscreen', function () { + spyOn($.fn, 'height').andCallFake(function (val) { + return _.isUndefined(val) ? 100: this; + }); + + state = jasmine.initializePlayer(); + $(state.el).trigger('fullscreen'); + + expect(state.videoFullScreen.height).toBe(150); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js new file mode 100644 index 0000000000..877dc9861e --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_pause_control_spec.js @@ -0,0 +1,68 @@ +(function () { + 'use strict'; + describe('VideoPlayPauseControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer(); + spyOn(state.videoCommands, 'execute'); + spyOn(state.videoSaveStatePlugin, 'saveState'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control', function () { + expect($('.video_control.play')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update ARIA state on play', function () { + state.el.trigger('play'); + expect($('.video_control.pause')).toHaveAttrs({ + 'role': 'button', + 'title': 'Pause', + 'aria-disabled': 'false' + }); + }); + + it('can update ARIA state on video ends', function () { + state.el.trigger('play'); + state.el.trigger('ended'); + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update state on pause', function () { + state.el.trigger('pause'); + expect(state.videoSaveStatePlugin.saveState).toHaveBeenCalledWith(true); + }); + + it('can start video playing on click', function () { + $('.video_control.play').click(); + expect(state.videoCommands.execute).toHaveBeenCalledWith('togglePlayback'); + }); + + it('can destroy itself', function () { + state.videoPlayPauseControl.destroy(); + expect(state.videoPlayPauseControl).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js new file mode 100644 index 0000000000..d99c12e24c --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_placeholder_spec.js @@ -0,0 +1,151 @@ +(function () { + 'use strict'; + describe('VideoPlayPlaceholder', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(['iPad']); + + state = jasmine.initializePlayer(); + spyOn(state.videoCommands, 'execute'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + state.videoPlayer.destroy(); + window.onTouchBasedDevice = oldOTBD; + }); + + 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'] + } + ]; + + beforeEach(function () { + jasmine.stubRequests(); + spyOn(window.YT, 'Player').andCallThrough(); + }); + + it ('works correctly on calling proper methods', function () { + var btnPlay; + + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.videoPlayPlaceholder.show(); + + expect(btnPlay).not.toHaveClass('is-hidden'); + expect(btnPlay).toHaveAttrs({ + 'aria-hidden': 'false', + 'tabindex': 0 + }); + + state.videoPlayPlaceholder.hide(); + + expect(btnPlay).toHaveClass('is-hidden'); + expect(btnPlay).toHaveAttrs({ + 'aria-hidden': 'true', + 'tabindex': -1 + }); + }); + + $.each(cases, function (index, data) { + var message = [ + (data.isShown) ? 'is' : 'is not', + ' shown on', + data.name + ].join(''); + + it(message, function () { + var btnPlay; + + window.onTouchBasedDevice.andReturn(data.isTouch); + state = jasmine.initializePlayer(); + 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 () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + state.el.trigger('pause'); + expect(btnPlay).not.toHaveClass('is-hidden'); + }); + + it( + 'is hidden on playing video on ' + device + + ' in HTML5 player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayer(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + expect(btnPlay).toHaveClass('is-hidden'); + }); + + it( + 'is hidden on paused video on ' + device + + ' in YouTube player', + function () + { + var btnPlay; + + window.onTouchBasedDevice.andReturn([device]); + state = jasmine.initializePlayerYouTube(); + btnPlay = state.el.find('.btn-play'); + + state.el.trigger('play'); + state.el.trigger('pause'); + expect(btnPlay).toHaveClass('is-hidden'); + }); + }); + + it('starts play the video on click', function () { + $('.btn-play').click(); + expect(state.videoCommands.execute).toHaveBeenCalledWith('play'); + }); + + it('can destroy itself', function () { + state.videoPlayPlaceholder.destroy(); + expect(state.videoPlayPlaceholder).toBeUndefined(); + }); + }); +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js new file mode 100644 index 0000000000..9ccea6a0ab --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_play_skip_control_spec.js @@ -0,0 +1,64 @@ +(function () { + 'use strict'; + describe('VideoPlaySkipControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoCommands, 'execute'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control', function () { + expect($('.video_control.play')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + expect($('.video_control.play')).toHaveAttrs({ + 'role': 'button', + 'title': 'Play', + 'aria-disabled': 'false' + }); + }); + + it('can update state on play', function () { + state.el.trigger('play'); + expect($('.video_control.play')).not.toExist(); + expect($('.video_control.skip')).toExist(); + }); + + it('can start video playing on click', function () { + $('.video_control.play').click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('play'); + }); + + it('can skip the video on click', function () { + state.el.trigger('play'); + spyOn(state.bumperState.videoPlayer, 'isPlaying').andReturn(true); + $('.video_control.skip').first().click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip'); + }); + + it('can destroy itself', function () { + var plugin = state.bumperState.videoPlaySkipControl, + el = plugin.el; + spyOn($.fn, 'off').andCallThrough(); + plugin.destroy(); + expect(state.bumperState.videoPlaySkipControl).toBeUndefined(); + expect(el).not.toExist(); + expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy); + }); + }); +}).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 da910d7bb0..fc74ef9229 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 @@ -1,5 +1,4 @@ (function (requirejs, require, define, undefined) { - 'use strict'; require( @@ -21,6 +20,9 @@ function (VideoPlayer) { if (state.storage) { state.storage.clear(); } + if (state.videoPlayer) { + _.result(state.videoPlayer, 'destroy'); + } }); describe('constructor', function () { @@ -47,7 +49,7 @@ function (VideoPlayer) { expect(state.videoCaption).toBeDefined(); expect(state.speed).toEqual('1.50'); expect(state.config.transcriptTranslationUrl) - .toEqual('/transcript/translation'); + .toEqual('/transcript/translation/__lang__'); }); it('create video speed control', function () { @@ -71,18 +73,15 @@ function (VideoPlayer) { var events; jasmine.stubRequests(); - spyOn(window.YT, 'Player').andCallThrough(); - state = jasmine.initializePlayerYouTube(); - state.videoEl = $('video, iframe'); events = { onReady: state.videoPlayer.onReady, onStateChange: state.videoPlayer.onStateChange, - onPlaybackQualityChange: state.videoPlayer - .onPlaybackQualityChange + onPlaybackQualityChange: state.videoPlayer.onPlaybackQualityChange, + onError: state.videoPlayer.onError }; expect(YT.Player).toHaveBeenCalledWith('id', { @@ -156,7 +155,7 @@ function (VideoPlayer) { }); it('controls are in paused state', function () { - expect(state.videoControl.isPlaying).toBe(false); + expect(state.videoPlayer.isPlaying()).toBe(false); }); }); }); @@ -166,16 +165,10 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer, 'log').andCallThrough(); spyOn(state.videoPlayer, 'play').andCallThrough(); state.videoPlayer.onReady(); }); - it('log the load_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith('load_video'); - }); - it('autoplay the first video', function () { expect(state.videoPlayer.play).not.toHaveBeenCalled(); }); @@ -197,9 +190,7 @@ function (VideoPlayer) { var playbackRates = state.videoPlayer.player.getAvailablePlaybackRates(); state.currentPlayerMode = 'flash'; - state.videoPlayer.onReady(); - expect(playbackRates.length).toBe(4); expect(state.currentPlayerMode).toBe('html5'); }); @@ -209,10 +200,7 @@ function (VideoPlayer) { describe('when the video is unstarted', function () { beforeEach(function () { state = jasmine.initializePlayer(); - state.videoEl = $('video, iframe'); - - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); state.videoPlayer.onStateChange({ @@ -221,7 +209,7 @@ function (VideoPlayer) { }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { @@ -244,9 +232,7 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); - spyOn(state.videoPlayer, 'log').andCallThrough(); spyOn(window, 'setInterval').andReturn(100); - spyOn(state.videoControl, 'play'); spyOn($.fn, 'trigger').andCallThrough(); state.videoPlayer.onStateChange({ @@ -254,23 +240,6 @@ function (VideoPlayer) { }); }); - it('speed_change_video event is not logged when speed not change', function () { - expect(state.videoPlayer.log).not.toHaveBeenCalledWith( - 'speed_change_video', - { - current_time: state.videoPlayer.currentTime, - old_speed: state.speed, - new_speed: state.speed - } - ); - }); - - it('log the play_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'play_video', { currentTime: 0 } - ); - }); - it('set update interval', function () { expect(window.setInterval).toHaveBeenCalledWith( state.videoPlayer.update, 200 @@ -279,7 +248,7 @@ function (VideoPlayer) { }); it('play the video control', function () { - expect(state.videoControl.play).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('pause'); }); it('play the video caption', function () { @@ -295,10 +264,7 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); - spyOn(state.videoPlayer, 'log').andCallThrough(); - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); - state.videoPlayer.onStateChange({ data: YT.PlayerState.PLAYING }); @@ -310,18 +276,12 @@ function (VideoPlayer) { }); }); - it('log the pause_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'pause_video', { currentTime: 0 } - ); - }); - it('clear update interval', function () { expect(state.videoPlayer.updateInterval).toBeUndefined(); }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { @@ -334,32 +294,19 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer, 'log').andCallThrough(); - spyOn(state.videoControl, 'pause').andCallThrough(); spyOn($.fn, 'trigger').andCallThrough(); - state.videoPlayer.onStateChange({ data: YT.PlayerState.ENDED }); }); it('pause the video control', function () { - expect(state.videoControl.pause).toHaveBeenCalled(); + expect($('.video_control')).toHaveClass('play'); }); it('pause the video caption', function () { expect($.fn.trigger).toHaveBeenCalledWith('ended', {}); }); - - it('log stop_video event', function () { - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'stop_video', - { - currentTime: state.videoPlayer.currentTime - } - ); - }); }); }); @@ -397,25 +344,6 @@ function (VideoPlayer) { }); }); - it('slider event causes log update', function () { - runs(function () { - spyOn(state.videoPlayer, 'log'); - state.videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 2 } - ); - // Video player uses _.debounce (with a wait time in 300 ms) for seeking. - // That's why we have to do this tick(300). - jasmine.Clock.tick(300); - expect(state.videoPlayer.currentTime).toBe(2); - - expect(state.videoPlayer.log).toHaveBeenCalledWith('seek_video', { - old_time: jasmine.any(Number), - new_time: 2, - type: 'onSlideSeek' - }); - }); - }); - it('seek the player', function () { runs(function () { spyOn(state.videoPlayer.player, 'seekTo').andCallThrough(); @@ -469,24 +397,6 @@ function (VideoPlayer) { .andCallThrough(); }); - it('slider event causes log update', function () { - spyOn(state.videoPlayer, 'log'); - state.videoProgressSlider.onSlide( - jQuery.Event('slide'), { value: 2 } - ); - // Video player uses _.debounce (with a wait time in 300 ms) for seeking. - // That's why we have to do this tick(300). - jasmine.Clock.tick(300); - expect(state.videoPlayer.currentTime).toBe(2); - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'seek_video', { - old_time: 0, - new_time: 2, - type: 'onSlideSeek' - } - ); - }); - it('video has a correct speed', function () { state.speed = '2.0'; state.videoPlayer.onPlay(); @@ -785,7 +695,7 @@ function (VideoPlayer) { state = jasmine.initializePlayer(); state.videoEl = $('video, iframe'); spyOn($.fn, 'trigger').andCallThrough(); - state.videoControl.toggleFullScreen(jQuery.Event('click')); + $('.add-fullscreen').click(); }); it('replace the full screen button tooltip', function () { @@ -810,11 +720,10 @@ function (VideoPlayer) { state.videoEl = $('video, iframe'); spyOn($.fn, 'trigger').andCallThrough(); state.el.addClass('video-fullscreen'); - state.videoControl.fullScreenState = true; - state.videoControl.isFullScreen = true; - state.videoControl.fullScreenEl.attr('title', 'Exit-fullscreen'); - - state.videoControl.toggleFullScreen(jQuery.Event('click')); + state.videoFullScreen.fullScreenState = true; + state.videoFullScreen.isFullScreen = true; + state.videoFullScreen.fullScreenEl.attr('title', 'Exit-fullscreen'); + $('.add-fullscreen').click(); }); it('replace the full screen button tooltip', function () { @@ -835,83 +744,6 @@ function (VideoPlayer) { }); }); - describe('play', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'playVideo').andCallThrough(); - }); - - describe('when the player is not ready', function () { - beforeEach(function () { - state.videoPlayer.player.playVideo = void 0; - state.videoPlayer.play(); - }); - - it('does nothing', function () { - expect(state.videoPlayer.player.playVideo).toBeUndefined(); - }); - }); - - describe('when the player is ready', function () { - beforeEach(function () { - state.videoPlayer.player.playVideo.andReturn(true); - state.videoPlayer.play(); - }); - - it('delegate to the player', function () { - expect(state.videoPlayer.player.playVideo).toHaveBeenCalled(); - }); - }); - }); - - describe('isPlaying', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'getPlayerState').andCallThrough(); - }); - - describe('when the video is playing', function () { - beforeEach(function () { - state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PLAYING); - }); - - it('return true', function () { - expect(state.videoPlayer.isPlaying()).toBeTruthy(); - }); - }); - - describe('when the video is not playing', function () { - beforeEach(function () { - state.videoPlayer.player.getPlayerState.andReturn(YT.PlayerState.PAUSED); - }); - - it('return false', function () { - expect(state.videoPlayer.isPlaying()).toBeFalsy(); - }); - }); - }); - - describe('pause', function () { - beforeEach(function () { - state = jasmine.initializePlayer(); - - state.videoEl = $('video, iframe'); - - spyOn(state.videoPlayer.player, 'pauseVideo').andCallThrough(); - state.videoPlayer.pause(); - }); - - it('delegate to the player', function () { - expect(state.videoPlayer.player.pauseVideo).toHaveBeenCalled(); - }); - }); - describe('duration', function () { beforeEach(function () { state = jasmine.initializePlayer(); @@ -1016,9 +848,7 @@ function (VideoPlayer) { runs(function () { state = jasmine.initializePlayer(); - state.videoEl = $('video, iframe'); - controls = state.el.find('.video-controls'); }); @@ -1053,7 +883,6 @@ function (VideoPlayer) { saveState: jasmine.createSpy(), videoPlayer: { currentTime: 60, - log: jasmine.createSpy(), updatePlayTime: jasmine.createSpy(), setPlaybackRate: jasmine.createSpy(), player: jasmine.createSpyObj('player', ['setPlaybackRate']) @@ -1063,18 +892,6 @@ function (VideoPlayer) { }); describe('always', function () { - it('check if speed_change_video is logged', function () { - VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.videoPlayer.log).toHaveBeenCalledWith( - 'speed_change_video', - { - current_time: state.videoPlayer.currentTime, - old_speed: '1.50', - new_speed: '0.75' - } - ); - }); - it('convert the current time to the new speed', function () { state.isFlashMode.andReturn(true); VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); @@ -1083,10 +900,7 @@ function (VideoPlayer) { it('set video speed to the new speed', function () { VideoPlayer.prototype.onSpeedChange.call(state, '0.75', false); - expect(state.setSpeed).toHaveBeenCalledWith('0.75', true); - expect(state.saveState).toHaveBeenCalledWith(true, { - speed: '0.75' - }); + expect(state.setSpeed).toHaveBeenCalledWith('0.75'); expect(state.videoPlayer.setPlaybackRate) .toHaveBeenCalledWith('0.75'); }); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js new file mode 100644 index 0000000000..18a6f6874c --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_poster_spec.js @@ -0,0 +1,42 @@ +(function (WAIT_TIMEOUT) { + 'use strict'; + describe('VideoPoster', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the poster', function () { + expect($('.poster')).toExist(); + expect($('.btn-play')).toExist(); + }); + + it('can start playing the video on click', function () { + $('.btn-play').click(); + waitsFor(function () { + return state.el.hasClass('is-playing'); + }, 'Player is not playing.', WAIT_TIMEOUT); + }); + + it('destroy itself on "play" event', function () { + $('.btn-play').click(); + expect($('.poster')).not.toExist(); + }); + }); +}).call(this, window.WAIT_TIMEOUT); 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 d7a5685719..491e98fae7 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,6 +12,7 @@ $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -38,6 +39,18 @@ expect(state.videoProgressSlider.handle) .toBe('.slider .ui-slider-handle'); }); + + it('add ARIA attributes to time control', function () { + var timeControl = $('div.slider > a'); + + expect(timeControl).toHaveAttrs({ + 'role': 'slider', + 'title': 'Video position', + 'aria-disabled': 'false' + }); + + expect(timeControl).toHaveAttr('aria-valuetext'); + }); }); describe('on a touch-based device', function () { @@ -304,6 +317,13 @@ }); }); + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoProgressSlider.destroy(); + expect(state.videoProgressSlider).toBeUndefined(); + expect($('.slider')).toBeEmpty(); + }); + }); }).call(this); 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 1ade3cb9ce..0bf3722a4c 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 @@ -1,12 +1,13 @@ (function (undefined) { describe('VideoQualityControl', function () { - var state, qualityControl, qualityControlEl, videoPlayer, player; + var state, qualityControl, videoPlayer, player; afterEach(function () { $('source').remove(); if (state.storage) { state.storage.clear(); } + state.videoPlayer.destroy(); }); describe('constructor, YouTube mode', function () { @@ -105,6 +106,11 @@ expect(qualityControl.el).toHaveClass('active'); }); + it('can destroy itself', function () { + state.videoQualityControl.destroy(); + expect(state.videoQualityControl).toBeUndefined(); + expect($('.quality-control')).not.toExist(); + }); }); describe('constructor, HTML5 mode', function () { diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js new file mode 100644 index 0000000000..7c101cdb32 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_save_state_plugin_spec.js @@ -0,0 +1,230 @@ +(function (undefined) { + 'use strict'; + describe('VideoPlayer Save State plugin', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice') + .andReturn(null); + + jasmine.stubRequests(); + state = jasmine.initializePlayer(); + spyOn(state.storage, 'setItem'); + }); + + afterEach(function () { + $('source').remove(); + window.onTouchBasedDevice = oldOTBD; + state.storage.clear(); + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + }); + + describe('saveState function', function () { + var videoPlayerCurrentTime, newCurrentTime, speed; + + // We make sure that `currentTime` is a float. We need to test + // that Math.round() is called. + videoPlayerCurrentTime = 3.1242; + + // We have two times, because one is stored in + // `videoPlayer.currentTime`, and the other is passed directly to + // `saveState` in `data` object. In each case, there is different + // code that handles these times. They have to be different for + // test completeness sake. Also, make sure it is float, as is the + // time above. + newCurrentTime = 5.4; + speed = '0.75'; + + beforeEach(function () { + state.videoPlayer.currentTime = videoPlayerCurrentTime; + spyOn(Time, 'formatFull').andCallThrough(); + }); + + it('data is not an object, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: videoPlayerCurrentTime, + data: undefined, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(videoPlayerCurrentTime)) + } + }); + }); + + it('data contains speed, async is false', function () { + itSpec({ + asyncVal: false, + speedVal: speed, + positionVal: undefined, + data: { + speed: speed + }, + ajaxData: { + speed: speed + } + }); + }); + + it('data contains float position, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: newCurrentTime, + data: { + saved_video_position: newCurrentTime + }, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(newCurrentTime)) + } + }); + }); + + it('data contains speed and rounded position, async is false', function () { + itSpec({ + asyncVal: false, + speedVal: speed, + positionVal: Math.round(newCurrentTime), + data: { + speed: speed, + saved_video_position: Math.round(newCurrentTime) + }, + ajaxData: { + speed: speed, + saved_video_position: Time.formatFull(Math.round(newCurrentTime)) + } + }); + }); + + it('data contains empty object, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: undefined, + data: {}, + ajaxData: {} + }); + }); + + it('data contains position 0, async is true', function () { + itSpec({ + asyncVal: true, + speedVal: undefined, + positionVal: 0, + data: { + saved_video_position: 0 + }, + ajaxData: { + saved_video_position: Time.formatFull(Math.round(0)) + } + }); + }); + + function itSpec(value) { + var asyncVal = value.asyncVal, + speedVal = value.speedVal, + positionVal = value.positionVal, + data = value.data, + ajaxData = value.ajaxData; + + state.videoSaveStatePlugin.saveState(asyncVal, data); + + if (speedVal) { + expect(state.storage.setItem).toHaveBeenCalledWith( + 'speed', + speedVal, + true + ); + } + if (positionVal) { + expect(state.storage.setItem).toHaveBeenCalledWith( + 'savedVideoPosition', + positionVal, + true + ); + expect(Time.formatFull).toHaveBeenCalledWith( + positionVal + ); + } + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: asyncVal, + dataType: 'json', + data: ajaxData + }); + } + }); + + it('can save state on speed change', function () { + state.el.trigger('speedchange', ['2.0']); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {speed: '2.0'} + }); + }); + + it('can save state on page unload', function () { + $.ajax.reset(); + state.videoSaveStatePlugin.onUnload(); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: false, + dataType: 'json', + data: {saved_video_position: '00:00:00'} + }); + }); + + it('can save state on pause', function () { + state.el.trigger('pause'); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {saved_video_position: '00:00:00'} + }); + }); + + it('can save state on language change', function () { + state.el.trigger('language_menu:change', ['ua']); + expect(state.storage.setItem).toHaveBeenCalledWith('language', 'ua'); + }); + + it('can save information about youtube availability', function () { + state.el.trigger('youtube_availability', [true]); + expect($.ajax).toHaveBeenCalledWith({ + url: state.config.saveStateUrl, + type: 'POST', + async: true, + dataType: 'json', + data: {youtube_is_available: true} + }); + }); + + it('can destroy itself', function () { + var plugin = state.videoSaveStatePlugin; + spyOn($.fn, 'off').andCallThrough(); + state.videoSaveStatePlugin.destroy(); + expect(state.videoSaveStatePlugin).toBeUndefined(); + expect($.fn.off).toHaveBeenCalledWith({ + 'speedchange': plugin.onSpeedChange, + 'play': plugin.bindUnloadHandler, + 'pause destroy': plugin.saveStateHandler, + 'language_menu:change': plugin.onLanguageChange, + 'youtube_availability': plugin.onYoutubeAvailability + }); + expect($.fn.off).toHaveBeenCalledWith('destroy', plugin.destroy); + expect($.fn.off).toHaveBeenCalledWith('unload', plugin.onUnload); + }); + }); + +}).call(this); diff --git a/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js b/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js new file mode 100644 index 0000000000..da3a87845b --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/video_skip_control_spec.js @@ -0,0 +1,55 @@ +(function () { + 'use strict'; + describe('VideoSkipControl', function () { + var state, oldOTBD; + + beforeEach(function () { + oldOTBD = window.onTouchBasedDevice; + window.onTouchBasedDevice = jasmine + .createSpy('onTouchBasedDevice').andReturn(null); + state = jasmine.initializePlayer('video_with_bumper.html'); + $('.poster .btn-play').click(); + spyOn(state.bumperState.videoCommands, 'execute').andCallThrough(); + }); + + afterEach(function () { + $('source').remove(); + state.storage.clear(); + if (state.bumperState && state.bumperState.videoPlayer) { + state.bumperState.videoPlayer.destroy(); + } + if (state.videoPlayer) { + state.videoPlayer.destroy(); + } + window.onTouchBasedDevice = oldOTBD; + }); + + it('can render the control when video starts playing', function () { + expect($('.skip-control')).not.toExist(); + state.el.trigger('play'); + expect($('.skip-control')).toExist(); + }); + + it('add ARIA attributes to play control', function () { + state.el.trigger('play'); + expect($('.skip-control')).toHaveAttrs({ + 'role': 'button', + 'title': 'Do not show again', + 'aria-disabled': 'false' + }); + }); + + it('can skip the video on click', function () { + spyOn(state.bumperState.videoBumper, 'skipAndDoNotShowAgain'); + state.el.trigger('play'); + $('.skip-control').click(); + expect(state.bumperState.videoCommands.execute).toHaveBeenCalledWith('skip', true); + expect(state.bumperState.videoBumper.skipAndDoNotShowAgain).toHaveBeenCalled(); + }); + + it('can destroy itself', function () { + state.bumperState.videoPlaySkipControl.destroy(); + expect(state.bumperState.videoPlaySkipControl).toBeUndefined(); + }); + }); +}).call(this); 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 45db124f0a..d5b14e6b2d 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,6 +12,7 @@ $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); describe('constructor', function () { @@ -247,5 +248,13 @@ expect($('.speeds .value')).toHaveHtml('0.75x'); }); }); + + it('can destroy itself', function () { + state = jasmine.initializePlayer(); + state.videoSpeedControl.destroy(); + expect(state.videoSpeedControl).toBeUndefined(); + expect($('.video-speeds')).not.toExist(); + expect($('.speed-button')).not.toExist(); + }); }); }).call(this); 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 7ff313f956..e1edb571d3 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 @@ -13,6 +13,7 @@ describe('VideoVolumeControl', function () { $('source').remove(); window.onTouchBasedDevice = oldOTBD; state.storage.clear(); + state.videoPlayer.destroy(); }); it('Volume level has correct value even if cookie is broken', function () { @@ -35,8 +36,7 @@ describe('VideoVolumeControl', function () { }); it('render the volume control', function () { - expect(state.videoControl.secondaryControlsEl.html()) - .toContain('
\n'); + expect($('.volume')).toExist(); }); it('create the slider', function () { @@ -292,7 +292,7 @@ describe('VideoVolumeControl', function () { shiftKey: true }); }); - }) + }); describe('keyDownButtonHandler', function () { beforeEach(function () { @@ -308,6 +308,6 @@ describe('VideoVolumeControl', function () { })); expect(volumeControl.getMuteStatus()).toEqual(isMuted); }); - }) + }); }); }).call(this); diff --git a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js index cbf7df47ee..f0c1debcd0 100644 --- a/common/lib/xmodule/xmodule/js/src/video/00_resizer.js +++ b/common/lib/xmodule/xmodule/js/src/video/00_resizer.js @@ -177,9 +177,8 @@ function () { } }; - var cleanDelta = function () { - delta['height'] = 0; - delta['width'] = 0; + var resetDelta = function () { + delta['height'] = delta['width'] = 0; return module; }; @@ -200,12 +199,23 @@ function () { return module; }; + var destroy = function () { + var data = getData(); + data.element.css({ + 'height': '', 'width': '', 'top': '', 'left': '' + }); + removeCallbacks(); + resetDelta(); + mode = null; + }; + initialize.apply(module, arguments); return $.extend(true, module, { align: align, alignByWidthOnly: alignByWidthOnly, alignByHeightOnly: alignByHeightOnly, + destroy: destroy, setParams: initialize, setMode: setMode, setElement: setElement, @@ -218,7 +228,7 @@ function () { delta: { add: addDelta, substract: substractDelta, - reset: cleanDelta + reset: resetDelta } }); }; 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 05654966f5..ad7edc8b56 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -14,8 +14,8 @@ define( 'video/01_initialize.js', -['video/03_video_player.js', 'video/00_video_storage.js', 'video/00_i18n.js'], -function (VideoPlayer, VideoStorage, i18n) { +['video/03_video_player.js', 'video/00_i18n.js'], +function (VideoPlayer, i18n) { /** * @function * @@ -71,7 +71,6 @@ function (VideoPlayer, VideoStorage, i18n) { isYoutubeType: isYoutubeType, parseSpeed: parseSpeed, parseYoutubeStreams: parseYoutubeStreams, - saveState: saveState, setPlayerMode: setPlayerMode, setSpeed: setSpeed, speedToString: speedToString, @@ -145,9 +144,7 @@ function (VideoPlayer, VideoStorage, i18n) { _youtubeApiDeferred.resolve(); } - window.YT.ready(function () { - onYTApiReady(); - }); + window.YT.ready(onYTApiReady); } else { // There is only one global variable window.onYouTubeIframeAPIReady which // is supposed to be a function that will be called by the YouTube API @@ -191,9 +188,7 @@ function (VideoPlayer, VideoStorage, i18n) { // Attach a callback to our Deferred object to be called once the // YouTube API loads. window.onYouTubeIframeAPIReady.done(function () { - window.YT.ready(function () { - onYTApiReady(); - }); + window.YT.ready(onYTApiReady); }); } } else { @@ -212,20 +207,15 @@ function (VideoPlayer, VideoStorage, i18n) { // callback, which will set `state.youtubeApiAvailable` to `true`. // If something goes wrong at this stage, `state.youtubeApiAvailable` is // `false`. - _reportToServer(state, state.youtubeApiAvailable); + if (!state.youtubeIsAvailable) { + console.log('[Video info]: YouTube API is not available.'); + } + state.el.trigger('youtube_availability', [state.youtubeIsAvailable]); }, state.config.ytTestTimeout); $.getScript(document.location.protocol + '//' + state.config.ytApiUrl); } - function _reportToServer(state, youtubeIsAvailable) { - if (!youtubeIsAvailable) { - console.log('[Video info]: YouTube API is not available.'); - } - - state.saveState(true, { youtube_is_available: youtubeIsAvailable }); - } - // function _configureCaptions(state) // Configure displaying of captions. // @@ -296,8 +286,7 @@ function (VideoPlayer, VideoStorage, i18n) { state.videoType = 'html5'; - if (!state.config.sub || !state.config.sub.length) { - state.config.sub = ''; + if (!_.keys(state.config.transcriptLanguages).length) { state.config.showCaptions = false; } state.setSpeed(state.speed); @@ -328,8 +317,9 @@ function (VideoPlayer, VideoStorage, i18n) { function _initializeModules(state, i18n) { var dfd = $.Deferred(), modulesList = $.map(state.modules, function(module) { - if ($.isFunction(module)) { - return module(state, i18n); + var options = state.options[module.moduleName] || {}; + if (_.isFunction(module)) { + return module(state, i18n, options); } else if ($.isPlainObject(module)) { return module; } @@ -388,7 +378,6 @@ function (VideoPlayer, VideoStorage, i18n) { }, 'startTime': function (value) { value = parseInt(value, 10); - if (!isFinite(value) || value < 0) { return 0; } @@ -407,6 +396,13 @@ function (VideoPlayer, VideoStorage, i18n) { }, config = {}; + data = _.extend({ + startTime: 0, + endTime: null, + sub: '', + streams: '' + }, data); + $.each(data, function(option, value) { // Extract option that is in `extractKeys`. if ($.inArray(option, extractKeys) !== -1) { @@ -420,7 +416,7 @@ function (VideoPlayer, VideoStorage, i18n) { // Pre-process data. if (conversions[option]) { - if ($.isFunction(conversions[option])) { + if (_.isFunction(conversions[option])) { value = conversions[option].call(this, value); } else { throw new TypeError(option + ' is not a function.'); @@ -463,12 +459,11 @@ function (VideoPlayer, VideoStorage, i18n) { function initialize(element) { var self = this, - el = $(element).find('.video'), + el = this.el, + id = this.id, container = el.find('.video-wrapper'), - id = el.attr('id').replace(/video_/, ''), __dfd__ = $.Deferred(), - isTouch = onTouchBasedDevice() || '', - storage = VideoStorage('VideoState', id); + isTouch = onTouchBasedDevice() || ''; if (isTouch) { el.addClass('is-touch'); @@ -476,23 +471,18 @@ function (VideoPlayer, VideoStorage, i18n) { $.extend(this, { __dfd__: __dfd__, - el: el, container: container, - id: id, isFullScreen: false, - isTouch: isTouch, - storage: storage + isTouch: isTouch }); - console.log( - '[Video info]: Initializing video with id "' + id + '".' - ); + console.log('[Video info]: Initializing video with id "%s".', id); // We store all settings passed to us by the server in one place. These // are "read only", so don't modify them. All variable content lives in // 'state' object. // jQuery .data() return object with keys in lower camelCase format. - this.config = $.extend({}, _getConfiguration(el.data(), storage), { + this.config = $.extend({}, _getConfiguration(this.metadata, this.storage), { element: element, fadeOutTimeout: 1400, captionsFreezeTime: 10000, @@ -602,26 +592,18 @@ function (VideoPlayer, VideoStorage, i18n) { // true: Parsing of YouTube video IDs went OK, and we can proceed // onwards to play YouTube videos. function parseYoutubeStreams(youtubeStreams) { - var _this; - - if ( - typeof youtubeStreams === 'undefined' || - youtubeStreams.length === 0 - ) { + if (_.isUndefined(youtubeStreams) || !youtubeStreams.length) { return false; } - _this = this; this.videos = {}; - $.each(youtubeStreams.split(/,/), function (index, video) { + _.each(youtubeStreams.split(/,/), function (video) { var speed; - video = video.split(/:/); - speed = _this.speedToString(video[0]); - - _this.videos[speed] = video[1]; - }); + speed = this.speedToString(video[0]); + this.videos[speed] = video[1]; + }, this); return _.isString(this.videos['1.0']); } @@ -633,23 +615,21 @@ function (VideoPlayer, VideoStorage, i18n) { // example the length of the video can be determined from the meta // data. function fetchMetadata() { - var _this = this, + var self = this, metadataXHRs = []; this.metadata = {}; - $.each(this.videos, function (speed, url) { - var xhr = _this.getVideoMetadata(url, function (data) { + metadataXHRs = _.map(this.videos, function (url, speed) { + return self.getVideoMetadata(url, function (data) { if (data.data) { - _this.metadata[data.data.id] = data.data; + self.metadata[data.data.id] = data.data; } }); - - metadataXHRs.push(xhr); }); $.when.apply(this, metadataXHRs).done(function () { - _this.el.trigger('metadata_received'); + self.el.trigger('metadata_received'); // Not only do we trigger the "metadata_received" event, we also // set a flag to notify that metadata has been received. This @@ -657,7 +637,7 @@ function (VideoPlayer, VideoStorage, i18n) { // to know that metadata has been received. This is important in // cases when some code will subscribe to the "metadata_received" // event after it has been triggered. - _this.youtubeMetadataReceived = true; + self.youtubeMetadataReceived = true; }); } @@ -666,23 +646,21 @@ function (VideoPlayer, VideoStorage, i18n) { // // Create a separate array of available speeds. function parseSpeed() { - this.speeds = ($.map(this.videos, function (url, speed) { - return speed; - })).sort(); + this.speeds = _.keys(this.videos).sort(); } - function setSpeed(newSpeed, updateStorage) { + function setSpeed(newSpeed) { // Possible speeds for each player type. // HTML5 = [0.75, 1, 1.25, 1.5] // Youtube Flash = [0.75, 1, 1.25, 1.5] // Youtube HTML5 = [0.25, 0.5, 1, 1.5, 2] var map = { - '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash - '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash - '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 - '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 - '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash - }; + '0.25': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.50': '0.75', // Youtube HTML5 -> HTML5 or Youtube Flash + '0.75': '0.50', // HTML5 or Youtube Flash -> Youtube HTML5 + '1.25': '1.50', // HTML5 or Youtube Flash -> Youtube HTML5 + '2.0': '1.50' // Youtube HTML5 -> HTML5 or Youtube Flash + }; if (_.contains(this.speeds, newSpeed)) { this.speed = newSpeed; @@ -690,57 +668,21 @@ function (VideoPlayer, VideoStorage, i18n) { newSpeed = map[newSpeed]; this.speed = _.contains(this.speeds, newSpeed) ? newSpeed : '1.0'; } - - if (updateStorage) { - this.storage.setItem('speed', this.speed, true); - this.storage.setItem('general_speed', this.speed); - } } function getVideoMetadata(url, callback) { - var successHandler, xhr; - - if (typeof url !== 'string') { + if (!(_.isString(url))) { url = this.videos['1.0'] || ''; } - successHandler = ($.isFunction(callback)) ? callback : null; - xhr = $.ajax({ + + return $.ajax({ url: [ document.location.protocol, '//', this.config.ytTestUrl, url, '?v=2&alt=jsonc' ].join(''), dataType: 'jsonp', timeout: this.config.ytTestTimeout, - success: successHandler - }); - - return xhr; - } - - function saveState(async, data) { - - if (!($.isPlainObject(data))) { - data = { - saved_video_position: this.videoPlayer.currentTime - }; - } - - if (data.speed) { - this.storage.setItem('speed', data.speed, true); - } - - if (data.hasOwnProperty('saved_video_position')) { - this.storage.setItem('savedVideoPosition', data.saved_video_position, true); - - data.saved_video_position = Time.formatFull(data.saved_video_position); - } - - $.ajax({ - url: this.config.saveStateUrl, - type: 'POST', - async: async ? true : false, - dataType: 'json', - data: data, + success: _.isFunction(callback) ? callback : null }); } 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 d055b85d62..dc3fd7974b 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 @@ -110,6 +110,54 @@ function () { }); }; + Player.prototype.onError = function (event) { + if ($.isFunction(this.config.events.onError)) { + this.config.events.onError(); + } + }; + + Player.prototype.destroy = function () { + this.video.removeEventListener('loadedmetadata', this.onLoadedMetadata, false); + this.video.removeEventListener('play', this.onPlay, false); + this.video.removeEventListener('playing', this.onPlaying, false); + this.video.removeEventListener('pause', this.onPause, false); + this.video.removeEventListener('ended', this.onEnded, false); + this.el + .find('.video-player div').removeClass('hidden') + .end() + .find('.video-player h3').addClass('hidden') + .end().removeClass('is-initialized') + .find('.spinner').attr({'aria-hidden': 'false'}); + this.videoEl.remove(); + }; + + Player.prototype.onLoadedMetadata = function () { + this.playerState = HTML5Video.PlayerState.PAUSED; + if ($.isFunction(this.config.events.onReady)) { + this.config.events.onReady(null); + } + }; + + Player.prototype.onPlay = function () { + this.playerState = HTML5Video.PlayerState.BUFFERING; + this.callStateChangeCallback(); + }; + + Player.prototype.onPlaying = function () { + this.playerState = HTML5Video.PlayerState.PLAYING; + this.callStateChangeCallback(); + }; + + Player.prototype.onPause = function () { + this.playerState = HTML5Video.PlayerState.PAUSED; + this.callStateChangeCallback(); + }; + + Player.prototype.onEnded = function () { + this.playerState = HTML5Video.PlayerState.ENDED; + this.callStateChangeCallback(); + }; + return Player; /* @@ -152,6 +200,7 @@ function () { var isTouch = onTouchBasedDevice() || '', sourceList, _this, errorMessage, lastSource; + _.bindAll(this, 'onLoadedMetadata', 'onPlay', 'onPlaying', 'onPause', 'onEnded'); 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 @@ -226,6 +275,8 @@ function () { lastSource = this.videoEl.find('source').last(); lastSource.on('error', this.showErrorMessage.bind(this)); + lastSource.on('error', this.onError.bind(this)); + this.videoEl.on('error', this.onError.bind(this)); if (/iP(hone|od)/i.test(isTouch[0])) { this.videoEl.prop('controls', true); @@ -280,35 +331,11 @@ function () { // When the
- -
    -
  1. -
${_('Go back to start of transcript.')} @@ -116,8 +50,8 @@ % if transcript_download_format: ${_('Download transcript')}
- ${'.' + transcript_download_format} -
    + ${'.' + transcript_download_format} +