From 65adb7b272072526763eae2e5e0fdb2fb4715c36 Mon Sep 17 00:00:00 2001 From: polesye Date: Mon, 3 Mar 2014 09:04:44 +0200 Subject: [PATCH] BLD-896: Fix cc button in HTML5 videos. --- .../xmodule/js/spec/video/general_spec.js | 28 +-- .../js/spec/video/video_caption_spec.js | 71 +++++-- .../js/spec/video/video_player_spec.js | 1 - .../xmodule/js/src/video/01_initialize.js | 6 - .../xmodule/js/src/video/03_video_player.js | 7 +- .../xmodule/js/src/video/09_video_caption.js | 21 +- .../xmodule/video_module/video_module.py | 83 ++++---- .../courseware/features/video.feature | 61 +++++- lms/djangoapps/courseware/features/video.py | 190 +++++++++--------- .../courseware/tests/test_video_handlers.py | 59 ++++-- 10 files changed, 300 insertions(+), 227 deletions(-) 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 42bd385802..800108d91a 100644 --- a/common/lib/xmodule/xmodule/js/spec/video/general_spec.js +++ b/common/lib/xmodule/xmodule/js/spec/video/general_spec.js @@ -75,32 +75,8 @@ expect(state.el).toBe('#video_id'); }); - it('parse the videos if subtitles exist', function () { - var sub = 'Z5KLxerq05Y'; - - expect(state.videos).toEqual({ - '0.75': sub, - '1.0': sub, - '1.25': sub, - '1.50': sub - }); - }); - - it( - 'parse the videos if subtitles do not exist', - function () - { - var sub = ''; - - $('#example').find('.video').data('sub', ''); - state = new window.Video('#example'); - - expect(state.videos).toEqual({ - '0.75': sub, - '1.0': sub, - '1.25': sub, - '1.50': sub - }); + it('doesn\'t have `videos` dictionary', function () { + expect(state.videos).toBeUndefined(); }); it('parse Html5 sources', function () { 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 a9e5236c93..24e238fcb0 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 @@ -7,8 +7,6 @@ window.onTouchBasedDevice = jasmine.createSpy('onTouchBasedDevice') .andReturn(null); - state = jasmine.initializePlayer(); - videoControl = state.videoControl; $.fn.scrollTo.reset(); }); @@ -29,18 +27,20 @@ describe('always', function () { beforeEach(function () { spyOn($, 'ajaxWithPrefix').andCallThrough(); - state = jasmine.initializePlayer(); }); it('create the caption element', function () { + state = jasmine.initializePlayer(); expect($('.video')).toContain('ol.subtitles'); }); it('add caption control to video player', function () { + state = jasmine.initializePlayer(); expect($('.video')).toContain('a.hide-subtitles'); }); it('add ARIA attributes to caption control', function () { + state = jasmine.initializePlayer(); var captionControl = $('a.hide-subtitles'); expect(captionControl).toHaveAttrs({ 'role': 'button', @@ -49,7 +49,11 @@ }); }); - it('fetch the caption', function () { + it('fetch the caption in HTML5 mode', function () { + runs(function () { + state = jasmine.initializePlayer(); + }); + waitsFor(function () { if (state.videoCaption.loaded === true) { return true; @@ -62,29 +66,62 @@ expect($.ajaxWithPrefix).toHaveBeenCalledWith({ url: '/transcript/translation', notifyOnError: false, - data: { - videoId: 'Z5KLxerq05Y', - language: 'en' - }, + data: jasmine.any(Object), success: jasmine.any(Function), error: jasmine.any(Function) }); + expect($.ajaxWithPrefix.mostRecentCall.args[0].data) + .toEqual({ + language: 'en' + }); + }); + }); + + it('fetch the caption in Youtube mode', function () { + runs(function () { + state = jasmine.initializePlayerYouTube(); + }); + + waitsFor(function () { + if (state.videoCaption.loaded === true) { + return true; + } + + return false; + }, 'Expect captions to be loaded.', WAIT_TIMEOUT); + + runs(function () { + expect($.ajaxWithPrefix).toHaveBeenCalledWith({ + url: '/transcript/translation', + notifyOnError: false, + data: jasmine.any(Object), + success: jasmine.any(Function), + error: jasmine.any(Function) + }); + expect($.ajaxWithPrefix.mostRecentCall.args[0].data) + .toEqual({ + language: 'en', + videoId: 'abcdefghijkl' + }); }); }); it('bind window resize event', function () { + state = jasmine.initializePlayer(); expect($(window)).toHandleWith( 'resize', state.videoCaption.resize ); }); it('bind the hide caption button', function () { + state = jasmine.initializePlayer(); expect($('.hide-subtitles')).toHandleWith( 'click', state.videoCaption.toggle ); }); it('bind the mouse movement', function () { + state = jasmine.initializePlayer(); expect($('.subtitles')).toHandleWith( 'mouseover', state.videoCaption.onMouseEnter ); @@ -103,8 +140,9 @@ }); it('bind the scroll', function () { - expect($('.subtitles')) - .toHandleWith('scroll', state.videoControl.showControls); + state = jasmine.initializePlayer(); + expect($('.subtitles')) + .toHandleWith('scroll', state.videoControl.showControls); }); }); @@ -284,7 +322,8 @@ describe('when no captions file was specified', function () { beforeEach(function () { state = jasmine.initializePlayer('video_all.html', { - 'sub': '' + 'sub': '', + 'transcriptLanguages': {}, }); }); @@ -395,6 +434,8 @@ }); it('reRenderCaption', function () { + state = jasmine.initializePlayer(); + var Caption = state.videoCaption, li; @@ -426,14 +467,6 @@ spyOn(state, 'youtubeId').andReturn('Z5KLxerq05Y'); }); - it('do not fetch captions, if 1.0 speed is absent', function () { - state.youtubeId.andReturn(void(0)); - Caption.fetchCaption(); - - expect($.ajaxWithPrefix).not.toHaveBeenCalled(); - expect(Caption.hideCaptions).not.toHaveBeenCalled(); - }); - it('show caption on language change', function () { Caption.loaded = true; Caption.fetchCaption(); 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 db75ea75e7..2ffa89d25d 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 @@ -45,7 +45,6 @@ function (VideoPlayer) { it('create video caption', function () { expect(state.videoCaption).toBeDefined(); - expect(state.youtubeId('1.0')).toEqual('Z5KLxerq05Y'); expect(state.speed).toEqual('1.50'); expect(state.config.transcriptTranslationUrl) .toEqual('/transcript/translation'); 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 7cea30aa9e..8be4bf9e06 100644 --- a/common/lib/xmodule/xmodule/js/src/video/01_initialize.js +++ b/common/lib/xmodule/xmodule/js/src/video/01_initialize.js @@ -202,12 +202,6 @@ function (VideoPlayer, VideoStorage) { ); state.speeds = ['0.75', '1.0', '1.25', '1.50']; - state.videos = { - '0.75': state.config.sub, - '1.0': state.config.sub, - '1.25': state.config.sub, - '1.50': state.config.sub - }; // We must have at least one non-YouTube video source available. // Otherwise, return a negative. diff --git a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js index 41333de7d4..70d883baf8 100644 --- a/common/lib/xmodule/xmodule/js/src/video/03_video_player.js +++ b/common/lib/xmodule/xmodule/js/src/video/03_video_player.js @@ -461,7 +461,7 @@ function (HTML5Video, Resizer) { this.videoPlayer.log( 'pause_video', { - 'currentTime': this.videoPlayer.currentTime + currentTime: this.videoPlayer.currentTime } ); @@ -482,7 +482,7 @@ function (HTML5Video, Resizer) { this.videoPlayer.log( 'play_video', { - 'currentTime': this.videoPlayer.currentTime + currentTime: this.videoPlayer.currentTime } ); @@ -863,8 +863,7 @@ function (HTML5Video, Resizer) { // Default parameters that always get logged. logInfo = { - 'id': this.id, - 'code': this.youtubeId() + id: this.id }; // If extra parameters were passed to the log. diff --git a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js index 06ff56e180..f71c4a17c4 100644 --- a/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js +++ b/common/lib/xmodule/xmodule/js/src/video/09_video_caption.js @@ -226,14 +226,10 @@ function () { */ function fetchCaption() { var self = this, - Caption = self.videoCaption; - // Check whether the captions file was specified. This is the point - // where we either stop with the caption panel (so that a white empty - // panel to the right of the video will not be shown), or carry on - // further. - if (!this.youtubeId('1.0')) { - return false; - } + Caption = self.videoCaption, + data = { + language: this.getCurrentLanguage() + }; if (Caption.loaded) { Caption.hideCaptions(false); @@ -245,15 +241,16 @@ function () { Caption.fetchXHR.abort(); } + if (this.videoType === 'youtube') { + data.videoId = this.youtubeId(); + } + // Fetch the captions file. If no file was specified, or if an error // occurred, then we hide the captions panel, and the "CC" button Caption.fetchXHR = $.ajaxWithPrefix({ url: self.config.transcriptTranslationUrl, notifyOnError: false, - data: { - videoId: this.youtubeId(), - language: this.getCurrentLanguage() - }, + data: data, success: function (captions) { Caption.captions = captions.text; Caption.start = captions.start; diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 467b912ea0..afcc64887b 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -10,7 +10,6 @@ in-browser HTML5 video method (when in HTML5 mode). in XML. """ -import os import json import logging from operator import itemgetter @@ -329,7 +328,7 @@ class VideoModule(VideoFields, XModule): `available_translations`: returns list of languages, for which SRT files exist. For 'en' check if SJSON exists. """ if dispatch == 'translation': - if 'language' not in request.GET or 'videoId' not in request.GET: + if 'language' not in request.GET: log.info("Invalid /transcript GET parameters.") return Response(status=400) @@ -341,7 +340,7 @@ class VideoModule(VideoFields, XModule): self.transcript_language = lang try: - transcript = self.translation(request.GET.get('videoId')) + transcript = self.translation(request.GET.get('videoId', None)) except (TranscriptException, NotFoundError) as ex: log.info(ex.message) response = Response(status=404) @@ -390,26 +389,30 @@ class VideoModule(VideoFields, XModule): return response - def translation(self, subs_id): + def translation(self, youtube_id): """ This is called to get transcript file for specific language. - subs_id: str: must be on of: self.sub or one of youtube_ids. + youtube_id: str: must be one of youtube_ids or None if HTML video Logic flow: - If english -> give back `sub` subtitles: - Return what we have in contentstore for given subs_id, - We should not regenerate needed transcripts, if, for example, they present for youtube 1.0 speed, - and we need for other speeds. Such generation should be done in transcripts workflow. - If non-english: - a) extract subs_id from srt file name - if non-youtube: - b) try to find sjson by subs_id and return if sucessful - c) otherwise generate sjson from srt and return it. - if youtube: - b) try to find sjson by subs_id and return if sucessful - c) generate sjson from srt for all youtube speeds + If youtube_id doesn't exist, we have a video in HTML5 mode. Otherwise, + video video in Youtube or Flash modes. + + if youtube: + If english -> give back youtube_id subtitles: + Return what we have in contentstore for given youtube_id. + If non-english: + a) extract youtube_id from srt file name. + b) try to find sjson by youtube_id and return if successful. + c) generate sjson from srt for all youtube speeds. + if non-youtube: + If english -> give back `sub` subtitles: + Return what we have in contentstore for given subs_if that is stored in self.sub. + If non-english: + a) try to find previously generated sjson. + b) otherwise generate sjson from srt and return it. Filenames naming: en: subs_videoid.srt.sjson @@ -418,28 +421,36 @@ class VideoModule(VideoFields, XModule): Raises: NotFoundError if for 'en' subtitles no asset is uploaded. """ - if self.transcript_language == 'en': - return asset(self.location, subs_id).data - if not self.youtube_id_1_0: # Non-youtube (HTML5) case: - return get_or_create_sjson(self) + if youtube_id: + # Youtube case: + if self.transcript_language == 'en': + return asset(self.location, youtube_id).data + + youtube_ids = youtube_speed_dict(self) + assert youtube_id in youtube_ids + + try: + sjson_transcript = asset(self.location, youtube_id, self.transcript_language).data + except (NotFoundError): + log.info("Can't find content in storage for %s transcript: generating.", youtube_id) + generate_sjson_for_all_speeds( + self, + self.transcripts[self.transcript_language], + {speed: youtube_id for youtube_id, speed in youtube_ids.iteritems()}, + self.transcript_language + ) + sjson_transcript = asset(self.location, youtube_id, self.transcript_language).data + + return sjson_transcript + else: + # HTML5 case + if self.transcript_language == 'en': + return asset(self.location, self.sub).data + else: + return get_or_create_sjson(self) - # Youtube case: - youtube_ids = youtube_speed_dict(self) - assert subs_id in youtube_ids - try: - sjson_transcript = asset(self.location, subs_id, self.transcript_language).data - except (NotFoundError): - log.info("Can't find content in storage for %s transcript: generating.", subs_id) - generate_sjson_for_all_speeds( - self, - self.transcripts[self.transcript_language], - {speed: subs_id for subs_id, speed in youtube_ids.iteritems()}, - self.transcript_language - ) - sjson_transcript = asset(self.location, subs_id, self.transcript_language).data - return sjson_transcript class VideoDescriptor(VideoFields, TabsEditingDescriptor, EmptyDataRawDescriptor): diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 6ea5c5c9b2..36d051a22c 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -2,7 +2,7 @@ Feature: LMS Video component As a student, I want to view course videos in LMS - # 0 + # 1 Scenario: Video component stores position correctly when page is reloaded Given the course has a Video component in Youtube mode Then when I view the video it has rendered in Youtube mode @@ -13,51 +13,51 @@ Feature: LMS Video component And I click video button "play" Then I see video starts playing from "0:10" position - # 1 + # 2 Scenario: Video component is fully rendered in the LMS in HTML5 mode Given the course has a Video component in HTML5 mode Then when I view the video it has rendered in HTML5 mode And all sources are correct - # 2 + # 3 # Firefox doesn't have HTML5 (only mp4 - fix here) @skip_firefox Scenario: Autoplay is disabled in LMS for a Video component Given the course has a Video component in HTML5 mode Then when I view the video it does not have autoplay enabled - # 3 + # 4 # Youtube testing Scenario: Video component is fully rendered in the LMS in Youtube mode with HTML5 sources Given youtube server is up and response time is 0.4 seconds And the course has a Video component in Youtube_HTML5 mode Then when I view the video it has rendered in Youtube mode - # 4 + # 5 Scenario: Video component is not rendered in the LMS in Youtube mode with HTML5 sources Given youtube server is up and response time is 2 seconds And the course has a Video component in Youtube_HTML5 mode Then when I view the video it has rendered in HTML5 mode - # 5 + # 6 Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources Given youtube server is up and response time is 2 seconds And the course has a Video component in Youtube mode Then when I view the video it has rendered in Youtube mode - # 6 + # 7 Scenario: Video component is rendered in the LMS in Youtube mode with HTML5 sources that doesn't supported by browser Given youtube server is up and response time is 2 seconds And the course has a Video component in Youtube_HTML5_Unsupported_Video mode Then when I view the video it has rendered in Youtube mode - # 7 + # 8 Scenario: Video component is rendered in the LMS in HTML5 mode with HTML5 sources that doesn't supported by browser Given the course has a Video component in HTML5_Unsupported_Video mode Then error message is shown And error message has correct text - # 8 + # 9 Scenario: Video component stores speed correctly when each video is in separate sequence Given I am registered for the course "test_course" And it has a video "A" in "Youtube" mode in position "1" of sequential @@ -79,8 +79,8 @@ Feature: LMS Video component When I open video "C" Then video "C" should start playing at speed "1.0" - # 9 - Scenario: Language menu in Video component works correctly + # 10 + Scenario: Language menu works correctly in Video component Given the course has a Video component in Youtube mode: | transcripts | sub | | {"zh": "OEoXaMPEzfM"} | OEoXaMPEzfM | @@ -90,3 +90,42 @@ Feature: LMS Video component Then I see "好 各位同学" text in the captions And I select language with code "en" And I see "Hi, welcome to Edx." text in the captions + + # 11 + Scenario: CC button works correctly w/o english transcript in HTML5 mode of Video component + Given the course has a Video component in HTML5 mode: + | transcripts | + | {"zh": "OEoXaMPEzfM"} | + And I make sure captions are opened + Then I see "好 各位同学" text in the captions + + # 12 + Scenario: CC button works correctly only w/ english transcript in HTML5 mode of Video component + Given I am registered for the course "test_course" + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "HTML5" mode: + | sub | + | OEoXaMPEzfM | + And I make sure captions are opened + Then I see "Hi, welcome to Edx." text in the captions + + # 13 + Scenario: CC button works correctly w/o english transcript in Youtube mode of Video component + Given the course has a Video component in Youtube mode: + | transcripts | + | {"zh": "OEoXaMPEzfM"} | + And I make sure captions are opened + Then I see "好 各位同学" text in the captions + + # 14 + Scenario: CC button works correctly if transcripts and sub fields are empty, but transcript file exists is assets (Youtube mode of Video component) + Given I am registered for the course "test_course" + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Youtube" mode + And I make sure captions are opened + Then I see "Hi, welcome to Edx." text in the captions + + # 15 + Scenario: CC button is hidden if no translations + Given the course has a Video component in Youtube mode + Then button "CC" is hidden diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 986d811684..6b44c4405d 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -9,7 +9,6 @@ from django.conf import settings from cache_toolbox.core import del_cached_content from xmodule.contentstore.content import StaticContent import os -from functools import partial from xmodule.contentstore.django import contentstore TEST_ROOT = settings.COMMON_TEST_DATA_ROOT LANGUAGES = settings.ALL_LANGUAGES @@ -46,48 +45,6 @@ VIDEO_BUTTONS = { coursenum = 'test_course' sequence = {} -@step('when I view the (.*) it does not have autoplay enabled$') -def does_not_autoplay(_step, video_type): - assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False') - - -@step('the course has a Video component in (.*) mode(?:\:)?$') -def view_video(_step, player_mode): - - i_am_registered_for_the_course(_step, coursenum) - - # Make sure we have a video - add_video_to_course(coursenum, player_mode.lower(), _step.hashes) - visit_scenario_item('SECTION') - - -@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential(?:\:)?$') -def add_video(_step, player_id, player_mode, position): - sequence[player_id] = position - add_video_to_course(coursenum, player_mode.lower(), _step.hashes, display_name=player_id) - - -@step('I open the section with videos$') -def visit_video_section(_step): - visit_scenario_item('SECTION') - - -@step('I select the "([^"]*)" speed on video "([^"]*)"$') -def change_video_speed(_step, speed, player_id): - _navigate_to_an_item_in_a_sequence(sequence[player_id]) - _change_video_speed(speed) - - -@step('I open video "([^"]*)"$') -def open_video(_step, player_id): - _navigate_to_an_item_in_a_sequence(sequence[player_id]) - - -@step('video "([^"]*)" should start playing at speed "([^"]*)"$') -def check_video_speed(_step, player_id, speed): - speed_css = '.speeds p.active' - assert world.css_has_text(speed_css, '{0}x'.format(speed)) - def add_video_to_course(course, player_mode, hashes, display_name='Video'): category = 'video' @@ -129,16 +86,105 @@ def add_video_to_course(course, player_mode, hashes, display_name='Video'): if 'transcripts' in kwargs['metadata']: kwargs['metadata']['transcripts'] = json.loads(kwargs['metadata']['transcripts']) + course_location = world.scenario_dict['COURSE'].location if 'sub' in kwargs['metadata']: - _upload_file(kwargs['metadata']['sub'], 'en', world.scenario_dict['COURSE'].location) + filename = _get_transcript_filename(kwargs['metadata']['sub'], 'en') + _upload_file(filename, course_location) for lang, videoId in kwargs['metadata']['transcripts'].items(): - _upload_file(videoId, lang, world.scenario_dict['COURSE'].location) + filename = _get_transcript_filename(videoId, lang) + _upload_file(filename, course_location) world.scenario_dict['VIDEO'] = world.ItemFactory.create(**kwargs) +def _get_transcript_filename(videoId, lang): + if lang == 'en': + return 'subs_{0}.srt.sjson'.format(videoId) + else: + return '{0}_subs_{1}.srt.sjson'.format(lang, videoId) + + +def _upload_file(filename, location): + path = os.path.join(TEST_ROOT, 'uploads/', filename) + f = open(os.path.abspath(path)) + mime_type = "application/json" + + content_location = StaticContent.compute_location( + location.org, location.course, filename + ) + content = StaticContent(content_location, filename, mime_type, f.read()) + contentstore().save(content) + del_cached_content(content.location) + + +def _navigate_to_an_item_in_a_sequence(number): + sequence_css = 'a[data-element="{0}"]'.format(number) + world.css_click(sequence_css) + + +def _change_video_speed(speed): + world.browser.execute_script("$('.speeds').addClass('open')") + speed_css = 'li[data-speed="{0}"] a'.format(speed) + world.css_click(speed_css) + + +def _open_menu(menu): + world.browser.execute_script("$('{selector}').parent().addClass('open')".format( + selector=VIDEO_MENUS[menu] + )) + + +@step('when I view the (.*) it does not have autoplay enabled$') +def does_not_autoplay(_step, video_type): + assert(world.css_find('.%s' % video_type)[0]['data-autoplay'] == 'False') + + +@step('the course has a Video component in (.*) mode(?:\:)?$') +def view_video(_step, player_mode): + + i_am_registered_for_the_course(_step, coursenum) + + # Make sure we have a video + add_video_to_course(coursenum, player_mode.lower(), _step.hashes) + visit_scenario_item('SECTION') + + +@step('a video in "([^"]*)" mode(?:\:)?$') +def add_video(_step, player_mode): + add_video_to_course(coursenum, player_mode.lower(), _step.hashes) + visit_scenario_item('SECTION') + + +@step('a video "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential(?:\:)?$') +def add_video_in_position(_step, player_id, player_mode, position): + sequence[player_id] = position + add_video_to_course(coursenum, player_mode.lower(), _step.hashes, display_name=player_id) + + +@step('I open the section with videos$') +def visit_video_section(_step): + visit_scenario_item('SECTION') + + +@step('I select the "([^"]*)" speed on video "([^"]*)"$') +def change_video_speed(_step, speed, player_id): + _navigate_to_an_item_in_a_sequence(sequence[player_id]) + _change_video_speed(speed) + + +@step('I open video "([^"]*)"$') +def open_video(_step, player_id): + _navigate_to_an_item_in_a_sequence(sequence[player_id]) + + +@step('video "([^"]*)" should start playing at speed "([^"]*)"$') +def check_video_speed(_step, player_id, speed): + speed_css = '.speeds p.active' + assert world.css_has_text(speed_css, '{0}x'.format(speed)) + + @step('youtube server is up and response time is (.*) seconds$') def set_youtube_response_timeout(_step, time): world.youtube.config['time_to_response'] = float(time) @@ -232,47 +278,6 @@ def click_button(_step, button): world.css_find(VIDEO_BUTTONS[button]).click() -def _upload_file(videoId, lang, location): - if lang == 'en': - filename = 'subs_{0}.srt.sjson'.format(videoId) - else: - filename = '{0}_subs_{1}.srt.sjson'.format(lang, videoId) - - path = os.path.join(TEST_ROOT, 'uploads/', filename) - f = open(os.path.abspath(path)) - mime_type = "application/json" - - content_location = StaticContent.compute_location( - location.org, location.course, filename - ) - - sc_partial = partial(StaticContent, content_location, filename, mime_type) - content = sc_partial(f.read()) - - (thumbnail_content, thumbnail_location) = contentstore().generate_thumbnail( - content, - tempfile_path=None - ) - del_cached_content(thumbnail_location) - - if thumbnail_content is not None: - content.thumbnail_location = thumbnail_location - - contentstore().save(content) - del_cached_content(content.location) - - -def _navigate_to_an_item_in_a_sequence(number): - sequence_css = 'a[data-element="{0}"]'.format(number) - world.css_click(sequence_css) - - -def _change_video_speed(speed): - world.browser.execute_script("$('.speeds').addClass('open')") - speed_css = 'li[data-speed="{0}"] a'.format(speed) - world.css_click(speed_css) - - @step('I click video button "([^"]*)"$') def click_button_video(_step, button_type): world.wait_for_ajax_complete() @@ -295,7 +300,12 @@ def seek_video_to_n_seconds(_step, seconds): world.browser.execute_script(jsCode) -def _open_menu(menu): - world.browser.execute_script("$('{selector}').parent().addClass('open')".format( - selector=VIDEO_MENUS[menu] - )) +@step('I have a "([^"]*)" transcript file in assets$') +def upload_to_assets(_step, filename): + _upload_file(filename, world.scenario_dict['COURSE'].location) + + +@step('button "([^"]*)" is hidden$') +def is_hidden_button(_step, button): + assert not world.css_visible(VIDEO_BUTTONS[button]) + diff --git a/lms/djangoapps/courseware/tests/test_video_handlers.py b/lms/djangoapps/courseware/tests/test_video_handlers.py index d7e635f57a..190a06acf8 100644 --- a/lms/djangoapps/courseware/tests/test_video_handlers.py +++ b/lms/djangoapps/courseware/tests/test_video_handlers.py @@ -189,17 +189,22 @@ class TestVideoTranscriptTranslation(TestVideo): # Tests for `translation` dispatch: def test_translation_fails(self): - # No videoId - request = Request.blank('/translation?language=ru') + # No language + request = Request.blank('/translation') response = self.item.transcript(request=request, dispatch='translation') self.assertEqual(response.status, '400 Bad Request') + # No videoId - HTML5 video with language that is not in available languages + request = Request.blank('/translation?language=ru') + response = self.item.transcript(request=request, dispatch='translation') + self.assertEqual(response.status, '404 Not Found') + # Language is not in available languages request = Request.blank('/translation?language=ru&videoId=12345') response = self.item.transcript(request=request, dispatch='translation') self.assertEqual(response.status, '404 Not Found') - def test_translaton_en_success(self): + def test_translaton_en_youtube_success(self): subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} good_sjson = _create_file(json.dumps(subs)) _upload_sjson_file(good_sjson, self.item_descriptor.location) @@ -210,25 +215,7 @@ class TestVideoTranscriptTranslation(TestVideo): response = self.item.transcript(request=request, dispatch='translation') self.assertDictEqual(json.loads(response.body), subs) - def test_translaton_non_en_non_youtube_success(self): - subs = { - u'end': [100], - u'start': [12], - u'text': [ - u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' - ] - } - self.non_en_file.seek(0) - _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) - subs_id = _get_subs_id(self.non_en_file.name) - - # manually clean youtube_id_1_0, as it has default value - self.item.youtube_id_1_0 = "" - request = Request.blank('/translation?language=uk&videoId={}'.format(subs_id)) - response = self.item.transcript(request=request, dispatch='translation') - self.assertDictEqual(json.loads(response.body), subs) - - def test_translation_non_en_youtube(self): + def test_translation_non_en_youtube_success(self): subs = { u'end': [100], u'start': [12], @@ -270,6 +257,34 @@ class TestVideoTranscriptTranslation(TestVideo): } self.assertDictEqual(json.loads(response.body), calculated_1_5) + def test_translaton_en_html5_success(self): + subs = {"start": [10], "end": [100], "text": ["Hi, welcome to Edx."]} + good_sjson = _create_file(json.dumps(subs)) + _upload_sjson_file(good_sjson, self.item_descriptor.location) + subs_id = _get_subs_id(good_sjson.name) + + self.item.sub = subs_id + request = Request.blank('/translation?language=en') + response = self.item.transcript(request=request, dispatch='translation') + self.assertDictEqual(json.loads(response.body), subs) + + def test_translaton_non_en_html5_success(self): + subs = { + u'end': [100], + u'start': [12], + u'text': [ + u'\u041f\u0440\u0438\u0432\u0456\u0442, edX \u0432\u0456\u0442\u0430\u0454 \u0432\u0430\u0441.' + ] + } + self.non_en_file.seek(0) + _upload_file(self.non_en_file, self.item_descriptor.location, os.path.split(self.non_en_file.name)[1]) + + # manually clean youtube_id_1_0, as it has default value + self.item.youtube_id_1_0 = "" + request = Request.blank('/translation?language=uk') + response = self.item.transcript(request=request, dispatch='translation') + self.assertDictEqual(json.loads(response.body), subs) + class TestVideoTranscriptsDownload(TestVideo): """