diff --git a/cms/djangoapps/contentstore/features/transcripts.py b/cms/djangoapps/contentstore/features/transcripts.py index 9542e22104..00f898fbb6 100644 --- a/cms/djangoapps/contentstore/features/transcripts.py +++ b/cms/djangoapps/contentstore/features/transcripts.py @@ -201,7 +201,9 @@ def upload_file(_step, file_name): @step('I see "([^"]*)" text in the captions') def check_text_in_the_captions(_step, text): - assert world.browser.is_text_present(text.strip(), 5) + world.wait_for(lambda _: world.css_text('.subtitles')) + actual_text = world.css_text('.subtitles') + assert (text in actual_text) @step('I see value "([^"]*)" in the field "([^"]*)"$') diff --git a/common/lib/xmodule/xmodule/js/spec/video/async_process_spec.js b/common/lib/xmodule/xmodule/js/spec/video/async_process_spec.js new file mode 100644 index 0000000000..c26ec7c8e7 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/async_process_spec.js @@ -0,0 +1,97 @@ +(function (require) { +require( +['video/00_async_process.js'], +function (AsyncProcess) { + var getArrayNthLength = function (n, multiplier) { + var result = [], + mul = multiplier || 1; + + for (var i = 0; i < n; i++) { + result[i] = i * mul; + } + + return result; + }, + items = getArrayNthLength(1000); + + describe('AsyncProcess', function () { + it ('Array is processed successfully', function () { + var processedArray, + expectedArray = getArrayNthLength(1000, 2), + process = function (item) { + return 2 * item; + }; + + runs(function () { + AsyncProcess.array(items, process).done(function (result) { + processedArray = result; + }); + }); + + waitsFor(function () { + return processedArray; + }, 'Array processing takes too much time', WAIT_TIMEOUT); + + runs(function () { + expect(processedArray).toEqual(expectedArray); + }); + }); + + it ('If non-array is passed, error callback is called', function () { + var isError, + process = function () {}; + + runs(function () { + AsyncProcess.array('string', process).fail(function () { + isError = true; + }); + }); + + waitsFor(function () { + return isError; + }, 'Error callback wasn\'t called', WAIT_TIMEOUT); + + runs(function () { + expect(isError).toBeTruthy(); + }); + }); + + it ('If an empty array is passed, returns initial array', function () { + var processedArray, + process = function () {}; + + runs(function () { + AsyncProcess.array([], process).done(function (result) { + processedArray = result; + }); + }); + + waitsFor(function () { + return processedArray; + }, 'Array processing takes too much time', WAIT_TIMEOUT); + + runs(function () { + expect(processedArray).toEqual([]); + }); + }); + + it ('If no process function passed, returns initial array', function () { + var processedArray; + + runs(function () { + AsyncProcess.array(items).done(function (result) { + processedArray = result; + }); + }); + + waitsFor(function () { + return processedArray; + }, 'Array processing takes too much time', WAIT_TIMEOUT); + + runs(function () { + expect(processedArray).toEqual(items); + }); + }); + }); +}); +}(RequireJS.require)); diff --git a/common/lib/xmodule/xmodule/js/spec/video/sjson_spec.js b/common/lib/xmodule/xmodule/js/spec/video/sjson_spec.js new file mode 100644 index 0000000000..b827246104 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/spec/video/sjson_spec.js @@ -0,0 +1,35 @@ +(function (require) { +require( +['video/00_sjson.js'], +function (Sjson) { + describe('Sjson', function () { + var data = jasmine.stubbedCaption, + sjson; + + beforeEach(function() { + sjson = new Sjson(data); + }); + + it ('returns captions', function () { + expect(sjson.getCaptions()).toEqual(data.text); + }); + + it ('returns start times', function () { + expect(sjson.getStartTimes()).toEqual(data.start); + }); + + it ('returns correct length', function () { + expect(sjson.getSize()).toEqual(data.text.length); + }); + + it('search returns a correct caption index', function () { + expect(sjson.search(0)).toEqual(-1); + expect(sjson.search(3120)).toEqual(1); + expect(sjson.search(6270)).toEqual(2); + expect(sjson.search(8490)).toEqual(2); + expect(sjson.search(21620)).toEqual(4); + expect(sjson.search(24920)).toEqual(5); + }); + }); +}); +}(RequireJS.require)); 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 2f159bc023..34ae9e9b66 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 @@ -92,7 +92,7 @@ }); expect($.ajaxWithPrefix.mostRecentCall.args[0].data) .toEqual({ - videoId: 'abcdefghijkl' + videoId: 'cogebirgzzM' }); }); }); @@ -237,73 +237,94 @@ describe('when on a non touch-based device', function () { beforeEach(function () { - state = jasmine.initializePlayer(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); it('render the caption', function () { - var captionsData; + runs(function () { + var captionsData; - captionsData = jasmine.stubbedCaption; - $('.subtitles li[data-index]').each( - function (index, link) { + captionsData = jasmine.stubbedCaption; + $('.subtitles li[data-index]').each( + function (index, link) { - expect($(link)).toHaveData('index', index); - expect($(link)).toHaveData( - 'start', captionsData.start[index] - ); - expect($(link)).toHaveAttr('tabindex', 0); - expect($(link)).toHaveText(captionsData.text[index]); + expect($(link)).toHaveData('index', index); + expect($(link)).toHaveData( + 'start', captionsData.start[index] + ); + expect($(link)).toHaveAttr('tabindex', 0); + expect($(link)).toHaveText(captionsData.text[index]); + }); }); }); it('add a padding element to caption', function () { - expect($('.subtitles li:first').hasClass('spacing')) - .toBe(true); - expect($('.subtitles li:last').hasClass('spacing')) - .toBe(true); + runs(function () { + expect($('.subtitles li:first').hasClass('spacing')) + .toBe(true); + expect($('.subtitles li:last').hasClass('spacing')) + .toBe(true); + }); }); it('bind all the caption link', function () { - var handlerList = ['captionMouseOverOut', 'captionClick', - 'captionMouseDown', 'captionFocus', 'captionBlur', - 'captionKeyDown' - ]; + runs(function () { + var handlerList = ['captionMouseOverOut', 'captionClick', + 'captionMouseDown', 'captionFocus', 'captionBlur', + 'captionKeyDown' + ]; - $.each(handlerList, function(index, handler) { - spyOn(state.videoCaption, handler); - }); - $('.subtitles li[data-index]').each( - function (index, link) { + $.each(handlerList, function(index, handler) { + spyOn(state.videoCaption, handler); + }); + $('.subtitles li[data-index]').each( + function (index, link) { - $(link).trigger('mouseover'); - expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled(); + $(link).trigger('mouseover'); + expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled(); - state.videoCaption.captionMouseOverOut.reset(); - $(link).trigger('mouseout'); - expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled(); + state.videoCaption.captionMouseOverOut.reset(); + $(link).trigger('mouseout'); + expect(state.videoCaption.captionMouseOverOut).toHaveBeenCalled(); - $(this).click(); - expect(state.videoCaption.captionClick).toHaveBeenCalled(); + $(this).click(); + expect(state.videoCaption.captionClick).toHaveBeenCalled(); - $(this).trigger('mousedown'); - expect(state.videoCaption.captionMouseDown).toHaveBeenCalled(); + $(this).trigger('mousedown'); + expect(state.videoCaption.captionMouseDown).toHaveBeenCalled(); - $(this).trigger('focus'); - expect(state.videoCaption.captionFocus).toHaveBeenCalled(); + $(this).trigger('focus'); + expect(state.videoCaption.captionFocus).toHaveBeenCalled(); - $(this).trigger('blur'); - expect(state.videoCaption.captionBlur).toHaveBeenCalled(); + $(this).trigger('blur'); + expect(state.videoCaption.captionBlur).toHaveBeenCalled(); - $(this).trigger('keydown'); - expect(state.videoCaption.captionKeyDown).toHaveBeenCalled(); + $(this).trigger('keydown'); + expect(state.videoCaption.captionKeyDown).toHaveBeenCalled(); + }); }); }); it('set rendered to true', function () { - state = jasmine.initializePlayer(); - expect(state.videoCaption.rendered).toBeTruthy(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); + + runs(function () { + expect(state.videoCaption.rendered).toBeTruthy(); + }); }); }); @@ -345,49 +366,58 @@ beforeEach(function () { jasmine.Clock.useMock(); spyOn(window, 'clearTimeout'); - state = jasmine.initializePlayer(); + + runs(function () { + state = jasmine.initializePlayer(); + jasmine.Clock.tick(50); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); describe('when cursor is outside of the caption box', function () { - beforeEach(function () { - $(window).trigger(jQuery.Event('mousemove')); - jasmine.Clock.tick(state.config.captionsFreezeTime); - }); - it('does not set freezing timeout', function () { - expect(state.videoCaption.frozen).toBeFalsy(); + runs(function () { + expect(state.videoCaption.frozen).toBeFalsy(); + }); }); }); describe('when cursor is in the caption box', function () { beforeEach(function () { spyOn(state.videoCaption, 'onMouseLeave'); - $('.subtitles').trigger(jQuery.Event('mouseenter')); - jasmine.Clock.tick(state.config.captionsFreezeTime); + runs(function () { + $(window).trigger(jQuery.Event('mousemove')); + jasmine.Clock.tick(state.config.captionsFreezeTime); + $('.subtitles').trigger(jQuery.Event('mouseenter')); + jasmine.Clock.tick(state.config.captionsFreezeTime); + }); }); it('set the freezing timeout', function () { - expect(state.videoCaption.frozen).not.toBeFalsy(); - expect(state.videoCaption.onMouseLeave).toHaveBeenCalled(); + runs(function () { + expect(state.videoCaption.frozen).not.toBeFalsy(); + expect(state.videoCaption.onMouseLeave).toHaveBeenCalled(); + }); }); describe('when the cursor is moving', function () { - beforeEach(function () { - $('.subtitles').trigger(jQuery.Event('mousemove')); - }); - it('reset the freezing timeout', function () { - expect(window.clearTimeout).toHaveBeenCalled(); + runs(function () { + $('.subtitles').trigger(jQuery.Event('mousemove')); + expect(window.clearTimeout).toHaveBeenCalled(); + }); }); }); describe('when the mouse is scrolling', function () { - beforeEach(function () { - $('.subtitles').trigger(jQuery.Event('mousewheel')); - }); - it('reset the freezing timeout', function () { - expect(window.clearTimeout).toHaveBeenCalled(); + runs(function () { + $('.subtitles').trigger(jQuery.Event('mousewheel')); + expect(window.clearTimeout).toHaveBeenCalled(); + }); }); }); }); @@ -441,25 +471,6 @@ }); }); - it('reRenderCaption', function () { - state = jasmine.initializePlayer(); - - var Caption = state.videoCaption, - li; - - Caption.captions = ['test']; - Caption.start = [500]; - - spyOn(Caption, 'addPaddings'); - - Caption.reRenderCaption(); - li = $('ol.subtitles li'); - - expect(Caption.addPaddings).toHaveBeenCalled(); - expect(li.length).toBe(1); - expect(li).toHaveData('start', '500'); - }); - describe('fetchCaption', function () { var Caption, msg; @@ -467,7 +478,6 @@ state = jasmine.initializePlayer(); Caption = state.videoCaption; spyOn($, 'ajaxWithPrefix').andCallThrough(); - spyOn(Caption, 'reRenderCaption'); spyOn(Caption, 'renderCaption'); spyOn(Caption, 'bindHandlers'); spyOn(Caption, 'updatePlayTime'); @@ -511,7 +521,6 @@ expect(Caption.bindHandlers).toHaveBeenCalled(); expect(Caption.renderCaption).not.toHaveBeenCalled(); expect(Caption.updatePlayTime).not.toHaveBeenCalled(); - expect(Caption.reRenderCaption).not.toHaveBeenCalled(); expect(Caption.loaded).toBeTruthy(); }); @@ -527,7 +536,6 @@ expect(Caption.bindHandlers).not.toHaveBeenCalled(); expect(Caption.renderCaption).not.toHaveBeenCalled(); expect(Caption.updatePlayTime).not.toHaveBeenCalled(); - expect(Caption.reRenderCaption).not.toHaveBeenCalled(); expect(Caption.loaded).toBeTruthy(); }); @@ -539,9 +547,8 @@ expect($.ajaxWithPrefix).toHaveBeenCalled(); expect(Caption.bindHandlers).not.toHaveBeenCalled(); - expect(Caption.renderCaption).not.toHaveBeenCalled(); + expect(Caption.renderCaption).toHaveBeenCalled(); expect(Caption.updatePlayTime).toHaveBeenCalled(); - expect(Caption.reRenderCaption).toHaveBeenCalled(); expect(Caption.loaded).toBeTruthy(); }); @@ -553,19 +560,18 @@ expect(Caption.bindHandlers).toHaveBeenCalled(); expect(Caption.renderCaption).toHaveBeenCalled(); expect(Caption.updatePlayTime).not.toHaveBeenCalled(); - expect(Caption.reRenderCaption).not.toHaveBeenCalled(); expect(Caption.loaded).toBeTruthy(); }); it('on success: re-rendered correct', function () { Caption.loaded = true; + Caption.rendered = true; Caption.fetchCaption(); expect($.ajaxWithPrefix).toHaveBeenCalled(); expect(Caption.bindHandlers).not.toHaveBeenCalled(); - expect(Caption.renderCaption).not.toHaveBeenCalled(); + expect(Caption.renderCaption).toHaveBeenCalled(); expect(Caption.updatePlayTime).toHaveBeenCalled(); - expect(Caption.reRenderCaption).toHaveBeenCalled(); expect(Caption.loaded).toBeTruthy(); }); @@ -680,53 +686,57 @@ }); }); - describe('search', function () { - it('return a correct caption index', function () { - state = jasmine.initializePlayer(); - expect(state.videoCaption.search(0)).toEqual(-1); - expect(state.videoCaption.search(3120)).toEqual(1); - expect(state.videoCaption.search(6270)).toEqual(2); - expect(state.videoCaption.search(8490)).toEqual(2); - expect(state.videoCaption.search(21620)).toEqual(4); - expect(state.videoCaption.search(24920)).toEqual(5); - }); - }); - describe('play', function () { describe('when the caption was not rendered', function () { beforeEach(function () { window.onTouchBasedDevice.andReturn(['iPad']); - state = jasmine.initializePlayer(); - state.videoCaption.play(); + + runs(function () { + state = jasmine.initializePlayer(); + state.videoCaption.play(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); it('render the caption', function () { - var captionsData; + runs(function () { + var captionsData; - captionsData = jasmine.stubbedCaption; - $('.subtitles li[data-index]').each( - function (index, link) { + captionsData = jasmine.stubbedCaption; + $('.subtitles li[data-index]').each( + function (index, link) { - expect($(link)).toHaveData('index', index); - expect($(link)).toHaveData( - 'start', captionsData.start[index] - ); - expect($(link)).toHaveAttr('tabindex', 0); - expect($(link)).toHaveText(captionsData.text[index]); + expect($(link)).toHaveData('index', index); + expect($(link)).toHaveData( + 'start', captionsData.start[index] + ); + expect($(link)).toHaveAttr('tabindex', 0); + expect($(link)).toHaveText(captionsData.text[index]); + }); }); + }); it('add a padding element to caption', function () { - expect($('.subtitles li:first')).toBe('.spacing'); - expect($('.subtitles li:last')).toBe('.spacing'); + runs(function () { + expect($('.subtitles li:first')).toBe('.spacing'); + expect($('.subtitles li:last')).toBe('.spacing'); + }); }); it('set rendered to true', function () { - expect(state.videoCaption.rendered).toBeTruthy(); + runs(function () { + expect(state.videoCaption.rendered).toBeTruthy(); + }); }); it('set playing to true', function () { - expect(state.videoCaption.playing).toBeTruthy(); + runs(function () { + expect(state.videoCaption.playing).toBeTruthy(); + }); }); }); }); @@ -745,219 +755,269 @@ describe('updatePlayTime', function () { beforeEach(function () { - state = jasmine.initializePlayer(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); describe('when the video speed is 1.0x', function () { - beforeEach(function () { - state.videoSpeedControl.currentSpeed = '1.0'; - state.videoCaption.updatePlayTime(25.000); - }); - it('search the caption based on time', function () { - expect(state.videoCaption.currentIndex).toEqual(5); + runs(function () { + state.videoCaption.updatePlayTime(25.000); + expect(state.videoCaption.currentIndex).toEqual(5); + + // Flash mode + spyOn(state, 'isFlashMode').andReturn(true); + state.speed = '1.0'; + state.videoCaption.updatePlayTime(25.000); + expect(state.videoCaption.currentIndex).toEqual(5); + }); }); }); describe('when the video speed is not 1.0x', function () { - beforeEach(function () { - state.videoSpeedControl.currentSpeed = '0.75'; - state.videoCaption.updatePlayTime(25.000); - }); - it('search the caption based on 1.0x speed', function () { - expect(state.videoCaption.currentIndex).toEqual(5); + runs(function () { + state.videoCaption.updatePlayTime(25.000); + expect(state.videoCaption.currentIndex).toEqual(5); + + // Flash mode + state.speed = '2.0'; + spyOn(state, 'isFlashMode').andReturn(true); + state.videoCaption.updatePlayTime(25.000); + expect(state.videoCaption.currentIndex).toEqual(9); + state.speed = '0.75'; + state.videoCaption.updatePlayTime(25.000); + expect(state.videoCaption.currentIndex).toEqual(3); + }); }); }); describe('when the index is not the same', function () { beforeEach(function () { - state.videoCaption.currentIndex = 1; - $('.subtitles li[data-index=5]').addClass('current'); - state.videoCaption.updatePlayTime(25.000); + runs(function () { + state.videoCaption.currentIndex = 1; + $('.subtitles li[data-index=5]').addClass('current'); + state.videoCaption.updatePlayTime(25.000); + }); }); it('deactivate the previous caption', function () { - expect($('.subtitles li[data-index=1]')) - .not.toHaveClass('current'); + runs(function () { + expect($('.subtitles li[data-index=1]')) + .not.toHaveClass('current'); + }); }); it('activate new caption', function () { - expect($('.subtitles li[data-index=5]')) - .toHaveClass('current'); + runs(function () { + expect($('.subtitles li[data-index=5]')) + .toHaveClass('current'); + }); }); it('save new index', function () { - expect(state.videoCaption.currentIndex).toEqual(5); + runs(function () { + expect(state.videoCaption.currentIndex).toEqual(5); + }); }); - // Disabled 11/25/13 due to flakiness in master - xit('scroll caption to new position', function () { - expect($.fn.scrollTo).toHaveBeenCalled(); + it('scroll caption to new position', function () { + runs(function () { + expect($.fn.scrollTo).toHaveBeenCalled(); + }); }); }); describe('when the index is the same', function () { - beforeEach(function () { - state.videoCaption.currentIndex = 1; - $('.subtitles li[data-index=3]').addClass('current'); - state.videoCaption.updatePlayTime(15.000); - }); - it('does not change current subtitle', function () { - expect($('.subtitles li[data-index=3]')) - .toHaveClass('current'); + runs(function () { + state.videoCaption.currentIndex = 1; + $('.subtitles li[data-index=3]').addClass('current'); + state.videoCaption.updatePlayTime(15.000); + expect($('.subtitles li[data-index=3]')) + .toHaveClass('current'); + }); }); }); }); describe('resize', function () { beforeEach(function () { - state = jasmine.initializePlayer(); - videoControl = state.videoControl; - $('.subtitles li[data-index=1]').addClass('current'); - state.videoCaption.resize(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); + + runs(function () { + videoControl = state.videoControl; + $('.subtitles li[data-index=1]').addClass('current'); + state.videoCaption.resize(); + }); }); describe('set the height of caption container', function () { it('when CC button is enabled', function () { - var realHeight = parseInt( - $('.subtitles').css('maxHeight'), 10 - ), - shouldBeHeight = $('.video-wrapper').height(); + runs(function () { + var realHeight = parseInt( + $('.subtitles').css('maxHeight'), 10 + ), + shouldBeHeight = $('.video-wrapper').height(); - // Because of some problems with rounding on different - // environments: Linux * Mac * FF * Chrome - expect(realHeight).toBeCloseTo(shouldBeHeight, 2); + // Because of some problems with rounding on different + // environments: Linux * Mac * FF * Chrome + expect(realHeight).toBeCloseTo(shouldBeHeight, 2); + }); }); it('when CC button is disabled ', function () { - var realHeight, videoWrapperHeight, progressSliderHeight, - controlHeight, shouldBeHeight; + runs(function () { + var realHeight, videoWrapperHeight, progressSliderHeight, + controlHeight, shouldBeHeight; - state.captionsHidden = true; - state.videoCaption.setSubtitlesHeight(); + state.captionsHidden = true; + state.videoCaption.setSubtitlesHeight(); - realHeight = parseInt( - $('.subtitles').css('maxHeight'), 10 - ); - videoWrapperHeight = $('.video-wrapper').height(); - progressSliderHeight = videoControl.sliderEl.height(); - controlHeight = videoControl.el.height(); - shouldBeHeight = videoWrapperHeight - - 0.5 * progressSliderHeight - - controlHeight; + realHeight = parseInt( + $('.subtitles').css('maxHeight'), 10 + ); + videoWrapperHeight = $('.video-wrapper').height(); + progressSliderHeight = videoControl.sliderEl.height(); + controlHeight = videoControl.el.height(); + shouldBeHeight = videoWrapperHeight - + 0.5 * progressSliderHeight - + controlHeight; - expect(realHeight).toBe(shouldBeHeight); + expect(realHeight).toBe(shouldBeHeight); + }); }); }); it('set the height of caption spacing', function () { - var firstSpacing, lastSpacing; + runs(function () { + var firstSpacing, lastSpacing; - firstSpacing = Math.abs(parseInt( - $('.subtitles .spacing:first').css('height'), 10 - )); - lastSpacing = Math.abs(parseInt( - $('.subtitles .spacing:last').css('height'), 10 - )); + firstSpacing = Math.abs(parseInt( + $('.subtitles .spacing:first').css('height'), 10 + )); + lastSpacing = Math.abs(parseInt( + $('.subtitles .spacing:last').css('height'), 10 + )); - expect(firstSpacing - state.videoCaption.topSpacingHeight()) - .toBeLessThan(1); - expect(lastSpacing - state.videoCaption.bottomSpacingHeight()) - .toBeLessThan(1); + expect(firstSpacing - state.videoCaption.topSpacingHeight()) + .toBeLessThan(1); + expect(lastSpacing - state.videoCaption.bottomSpacingHeight()) + .toBeLessThan(1); + }); }); it('scroll caption to new position', function () { - expect($.fn.scrollTo).toHaveBeenCalled(); + runs(function () { + expect($.fn.scrollTo).toHaveBeenCalled(); + }); }); }); - // Disabled 11/25/13 due to flakiness in master xdescribe('scrollCaption', function () { beforeEach(function () { - state = jasmine.initializePlayer(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); describe('when frozen', function () { - beforeEach(function () { - state.videoCaption.frozen = true; - $('.subtitles li[data-index=1]').addClass('current'); - state.videoCaption.scrollCaption(); - }); - it('does not scroll the caption', function () { - expect($.fn.scrollTo).not.toHaveBeenCalled(); + runs(function () { + state.videoCaption.frozen = true; + $('.subtitles li[data-index=1]').addClass('current'); + state.videoCaption.scrollCaption(); + expect($.fn.scrollTo).not.toHaveBeenCalled(); + }); }); }); describe('when not frozen', function () { beforeEach(function () { - state.videoCaption.frozen = false; + runs(function () { + state.videoCaption.frozen = false; + }); }); describe('when there is no current caption', function () { - beforeEach(function () { - state.videoCaption.scrollCaption(); - }); - it('does not scroll the caption', function () { - expect($.fn.scrollTo).not.toHaveBeenCalled(); + runs(function () { + state.videoCaption.scrollCaption(); + expect($.fn.scrollTo).not.toHaveBeenCalled(); + }); }); }); describe('when there is a current caption', function () { - beforeEach(function () { - $('.subtitles li[data-index=1]').addClass('current'); - state.videoCaption.scrollCaption(); - }); - it('scroll to current caption', function () { - expect($.fn.scrollTo).toHaveBeenCalled(); + runs(function () { + $('.subtitles li[data-index=1]').addClass('current'); + state.videoCaption.scrollCaption(); + expect($.fn.scrollTo).toHaveBeenCalled(); + }); }); }); }); }); - // Disabled 10/9/13 due to flakiness in master xdescribe('seekPlayer', function () { beforeEach(function () { - state = jasmine.initializePlayer(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + var duration = state.videoPlayer.duration(), + isRendered = state.videoCaption.rendered; + + return isRendered && duration; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); describe('when the video speed is 1.0x', function () { - beforeEach(function () { - state.videoSpeedControl.currentSpeed = '1.0'; - $('.subtitles li[data-start="14910"]').trigger('click'); - }); - it('trigger seek event with the correct time', function () { - expect(state.videoPlayer.currentTime).toEqual(14.91); + runs(function () { + state.videoSpeedControl.currentSpeed = '1.0'; + $('.subtitles li[data-start="14910"]').trigger('click'); + expect(state.videoPlayer.currentTime).toEqual(14.91); + }); }); }); describe('when the video speed is not 1.0x', function () { - beforeEach(function () { - state.videoSpeedControl.currentSpeed = '0.75'; - $('.subtitles li[data-start="14910"]').trigger('click'); - }); - it('trigger seek event with the correct time', function () { - expect(state.videoPlayer.currentTime).toEqual(14.91); + runs(function () { + state.videoSpeedControl.currentSpeed = '0.75'; + $('.subtitles li[data-start="14910"]').trigger('click'); + expect(state.videoPlayer.currentTime).toEqual(14.91); + }); }); }); describe('when the player type is Flash at speed 0.75x', function () { - - beforeEach(function () { - state.videoSpeedControl.currentSpeed = '0.75'; - state.currentPlayerMode = 'flash'; - $('.subtitles li[data-start="14910"]').trigger('click'); - }); - it('trigger seek event with the correct time', function () { - expect(state.videoPlayer.currentTime).toEqual(15); + runs(function () { + state.videoSpeedControl.currentSpeed = '0.75'; + state.currentPlayerMode = 'flash'; + $('.subtitles li[data-start="14910"]').trigger('click'); + expect(state.videoPlayer.currentTime).toEqual(15); + }); }); }); }); @@ -998,7 +1058,6 @@ beforeEach(function () { state.el.addClass('closed'); state.videoCaption.toggle(jQuery.Event('click')); - jasmine.Clock.useMock(); }); @@ -1039,41 +1098,59 @@ describe('caption accessibility', function () { beforeEach(function () { - state = jasmine.initializePlayer(); + runs(function () { + state = jasmine.initializePlayer(); + }); + + waitsFor(function () { + return state.videoCaption.rendered; + }, 'Captions are not rendered', WAIT_TIMEOUT); }); describe('when getting focus through TAB key', function () { beforeEach(function () { - state.videoCaption.isMouseFocus = false; - $('.subtitles li[data-index=0]').trigger( - jQuery.Event('focus') - ); + runs(function () { + state.videoCaption.isMouseFocus = false; + $('.subtitles li[data-index=0]').trigger( + jQuery.Event('focus') + ); + }); }); it('shows an outline around the caption', function () { - expect($('.subtitles li[data-index=0]')) - .toHaveClass('focused'); + runs(function () { + expect($('.subtitles li[data-index=0]')) + .toHaveClass('focused'); + }); }); it('has automatic scrolling disabled', function () { - expect(state.videoCaption.autoScrolling).toBe(false); + runs(function () { + expect(state.videoCaption.autoScrolling).toBe(false); + }); }); }); describe('when loosing focus through TAB key', function () { beforeEach(function () { - $('.subtitles li[data-index=0]').trigger( - jQuery.Event('blur') - ); + runs(function () { + $('.subtitles li[data-index=0]').trigger( + jQuery.Event('blur') + ); + }); }); it('does not show an outline around the caption', function () { - expect($('.subtitles li[data-index=0]')) - .not.toHaveClass('focused'); + runs(function () { + expect($('.subtitles li[data-index=0]')) + .not.toHaveClass('focused'); + }); }); it('has automatic scrolling enabled', function () { - expect(state.videoCaption.autoScrolling).toBe(true); + runs(function () { + expect(state.videoCaption.autoScrolling).toBe(true); + }); }); }); @@ -1083,20 +1160,26 @@ function () { beforeEach(function () { - state.videoCaption.isMouseFocus = false; - $('.subtitles li[data-index=0]') - .trigger(jQuery.Event('focus')); - $('.subtitles li[data-index=0]') - .trigger(jQuery.Event('mousedown')); + runs(function () { + state.videoCaption.isMouseFocus = false; + $('.subtitles li[data-index=0]') + .trigger(jQuery.Event('focus')); + $('.subtitles li[data-index=0]') + .trigger(jQuery.Event('mousedown')); + }); }); it('does not show an outline around it', function () { - expect($('.subtitles li[data-index=0]')) - .not.toHaveClass('focused'); + runs(function () { + expect($('.subtitles li[data-index=0]')) + .not.toHaveClass('focused'); + }); }); it('has automatic scrolling enabled', function () { - expect(state.videoCaption.autoScrolling).toBe(true); + runs(function () { + expect(state.videoCaption.autoScrolling).toBe(true); + }); }); }); @@ -1108,29 +1191,37 @@ var subDataLiIdx__0, subDataLiIdx__1; beforeEach(function () { - subDataLiIdx__0 = $('.subtitles li[data-index=0]'); - subDataLiIdx__1 = $('.subtitles li[data-index=1]'); + runs(function () { + subDataLiIdx__0 = $('.subtitles li[data-index=0]'); + subDataLiIdx__1 = $('.subtitles li[data-index=1]'); - state.videoCaption.isMouseFocus = false; + state.videoCaption.isMouseFocus = false; - subDataLiIdx__0.trigger(jQuery.Event('focus')); - subDataLiIdx__0.trigger(jQuery.Event('blur')); + subDataLiIdx__0.trigger(jQuery.Event('focus')); + subDataLiIdx__0.trigger(jQuery.Event('blur')); - state.videoCaption.isMouseFocus = true; + state.videoCaption.isMouseFocus = true; - subDataLiIdx__1.trigger(jQuery.Event('mousedown')); + subDataLiIdx__1.trigger(jQuery.Event('mousedown')); + }); }); it('does not show an outline around the first', function () { - expect(subDataLiIdx__0).not.toHaveClass('focused'); + runs(function () { + expect(subDataLiIdx__0).not.toHaveClass('focused'); + }); }); it('does not show an outline around the second', function () { - expect(subDataLiIdx__1).not.toHaveClass('focused'); + runs(function () { + expect(subDataLiIdx__1).not.toHaveClass('focused'); + }); }); it('has automatic scrolling enabled', function () { - expect(state.videoCaption.autoScrolling).toBe(true); + runs(function () { + expect(state.videoCaption.autoScrolling).toBe(true); + }); }); }); }); diff --git a/common/lib/xmodule/xmodule/js/src/video/00_async_process.js b/common/lib/xmodule/xmodule/js/src/video/00_async_process.js new file mode 100644 index 0000000000..55a2cb1448 --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/00_async_process.js @@ -0,0 +1,59 @@ +(function (define) { +define( +'video/00_async_process.js', +[], +function() { +"use strict"; +/** + * Provides convenient way to process big amount of data without UI blocking. + * + * @param {array} list Array to process. + * @param {function} process Calls this function on each item in the list. + * @return {array} Returns a Promise object to observe when all actions of a + certain type bound to the collection, queued or not, have finished. + */ + var AsyncProcess = { + array: function (list, process) { + if (!_.isArray(list)) { + return $.Deferred().reject().promise(); + } + + if (!_.isFunction(process) || !list.length) { + return $.Deferred().resolve(list).promise(); + } + + var MAX_DELAY = 50, // maximum amount of time that js code should be allowed to run continuously + dfd = $.Deferred(), + result = [], + index = 0, + len = list.length; + + var getCurrentTime = function () { + return (new Date()).getTime(); + }; + + var handler = function () { + var start = getCurrentTime(); + + do { + result[index] = process(list[index], index); + index++; + } while (index < len && getCurrentTime() - start < MAX_DELAY); + + if (index < len) { + setTimeout(handler, 25); + } else { + dfd.resolve(result); + } + }; + + setTimeout(handler, 25); + + return dfd.promise(); + } + }; + + return AsyncProcess; +}); +}(RequireJS.define)); + diff --git a/common/lib/xmodule/xmodule/js/src/video/00_sjson.js b/common/lib/xmodule/xmodule/js/src/video/00_sjson.js new file mode 100644 index 0000000000..b140a2e31c --- /dev/null +++ b/common/lib/xmodule/xmodule/js/src/video/00_sjson.js @@ -0,0 +1,65 @@ +(function (define) { + +define( +'video/00_sjson.js', +[], +function() { +"use strict"; + + var Sjson = function (data) { + var sjson = { + start: data.start.concat(), + text: data.text.concat() + }, + module = {}; + + var getter = function (propertyName) { + return function () { + return sjson[propertyName]; + }; + }; + + var getStartTimes = getter('start'); + + var getCaptions = getter('text'); + + var size = function () { + return sjson.text.length; + }; + + var search = function (time) { + var start = getStartTimes(), + max = size() - 1, + min = 0, + index; + + if (time < start[min]) { + return -1; + } + while (min < max) { + index = Math.ceil((max + min) / 2); + + if (time < start[index]) { + max = index - 1; + } + + if (time >= start[index]) { + min = index; + } + } + + return min; + }; + + return { + getCaptions: getCaptions, + getStartTimes: getStartTimes, + getSize: size, + search: search + }; + }; + + return Sjson; +}); +}(RequireJS.define)); + 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 5d8e2a5007..3fd5832f8a 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 @@ -1,10 +1,10 @@ -(function (requirejs, require, define) { +(function (define) { // VideoCaption module. define( 'video/09_video_caption.js', -[], -function () { +['video/00_sjson.js', 'video/00_async_process.js'], +function (Sjson, AsyncProcess) { /** * @desc VideoCaption module exports a function. @@ -21,16 +21,11 @@ function () { * @returns {undefined} */ return function (state) { - var dfd = $.Deferred(); - state.videoCaption = {}; - _makeFunctionsPublic(state); - state.videoCaption.renderElements(); - dfd.resolve(); - return dfd.promise(); + return $.Deferred().resolve().promise(); }; // *************************************************************** @@ -65,10 +60,8 @@ function () { renderCaption: renderCaption, renderElements: renderElements, renderLanguageMenu: renderLanguageMenu, - reRenderCaption: reRenderCaption, resize: resize, scrollCaption: scrollCaption, - search: search, seekPlayer: seekPlayer, setSubtitlesHeight: setSubtitlesHeight, toggle: toggle, @@ -133,19 +126,47 @@ function () { // mousemove, etc.). function bindHandlers() { var self = this, - Caption = this.videoCaption; + Caption = this.videoCaption, + events = [ + 'mouseover', 'mouseout', 'mousedown', 'click', 'focus', 'blur', + 'keydown' + ].join(' '); Caption.hideSubtitlesEl.on({ 'click': Caption.toggle }); - Caption.subtitlesEl.on({ - mouseenter: Caption.onMouseEnter, - mouseleave: Caption.onMouseLeave, - mousemove: Caption.onMovement, - mousewheel: Caption.onMovement, - DOMMouseScroll: Caption.onMovement - }); + Caption.subtitlesEl + .on({ + mouseenter: Caption.onMouseEnter, + mouseleave: Caption.onMouseLeave, + mousemove: Caption.onMovement, + mousewheel: Caption.onMovement, + DOMMouseScroll: Caption.onMovement + }) + .on(events, 'li[data-index]', function (event) { + switch (event.type) { + case 'mouseover': + case 'mouseout': + Caption.captionMouseOverOut(event); + break; + case 'mousedown': + Caption.captionMouseDown(event); + break; + case 'click': + Caption.captionClick(event); + break; + case 'focusin': + Caption.captionFocus(event); + break; + case 'focusout': + Caption.captionBlur(event); + break; + case 'keydown': + Caption.captionKeyDown(event); + break; + } + }); if (Caption.showLanguageMenu) { Caption.container.on({ @@ -154,12 +175,6 @@ function () { }); } - this.el.on('speedchange', function () { - if (self.isFlashMode()) { - Caption.fetchCaption(); - } - }); - if ((this.videoType === 'html5') && (this.config.autohideHtml5)) { Caption.subtitlesEl.on('scroll', this.videoControl.showControls); } @@ -241,7 +256,7 @@ function () { if (this.videoType === 'youtube') { data = { - videoId: this.youtubeId() + videoId: this.youtubeId('1.0') }; } @@ -251,13 +266,15 @@ function () { url: self.config.transcriptTranslationUrl + '/' + language, notifyOnError: false, data: data, - success: function (captions) { - Caption.captions = captions.text; - Caption.start = captions.start; + success: function (response) { + Caption.sjson = new Sjson(response); + + var start = Caption.sjson.getStartTimes(), + captions = Caption.sjson.getCaptions(); if (Caption.loaded) { if (Caption.rendered) { - Caption.reRenderCaption(); + Caption.renderCaption(start, captions); Caption.updatePlayTime(self.videoPlayer.currentTime); } } else { @@ -269,7 +286,7 @@ function () { ) ); } else { - Caption.renderCaption(); + Caption.renderCaption(start, captions); } Caption.bindHandlers(); @@ -334,7 +351,6 @@ function () { .height(this.videoCaption.bottomSpacingHeight()); this.videoCaption.scrollCaption(); - this.videoCaption.setSubtitlesHeight(); } @@ -380,90 +396,53 @@ function () { }); } - function buildCaptions (container, captions, start) { - var fragment = document.createDocumentFragment(); + function buildCaptions (container, start, captions) { + var process = function(text, index) { + var liEl = $('
  • ', { + 'data-index': index, + 'data-start': start[index], + 'tabindex': 0 + }).html(text); - $.each(captions, function(index, text) { - var liEl = $('
  • '); + return liEl[0]; + }; - liEl.html(text); - - liEl.attr({ - 'data-index': index, - 'data-start': start[index], - 'tabindex': 0 - }); - - fragment.appendChild(liEl[0]); + return AsyncProcess.array(captions, process).done(function (list) { + container.append(list); }); - - container.append([fragment]); } - function renderCaption() { - var Caption = this.videoCaption, - events = ['mouseover', 'mouseout', 'mousedown', 'click', 'focus', - 'blur', 'keydown'].join(' '); - - Caption.setSubtitlesHeight(); - - buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start); - - Caption.subtitlesEl.on(events, 'li[data-index]', function (event) { - switch (event.type) { - case 'mouseover': - case 'mouseout': - Caption.captionMouseOverOut(event); - break; - case 'mousedown': - Caption.captionMouseDown(event); - break; - case 'click': - Caption.captionClick(event); - break; - case 'focusin': - Caption.captionFocus(event); - break; - case 'focusout': - Caption.captionBlur(event); - break; - case 'keydown': - Caption.captionKeyDown(event); - break; - } - }); - - // Enables or disables automatic scrolling of the captions when the - // video is playing. This feature has to be disabled when tabbing - // through them as it interferes with that action. Initially, have this - // flag enabled as we assume mouse use. Then, if the first caption - // (through forward tabbing) or the last caption (through backwards - // tabbing) gets the focus, disable that feature. Re-enable it if tabbing - // then cycles out of the the captions. - Caption.autoScrolling = true; - // Keeps track of where the focus is situated in the array of captions. - // Used to implement the automatic scrolling behavior and decide if the - // outline around a caption has to be hidden or shown on a mouseenter - // or mouseleave. Initially, no caption has the focus, set the - // index to -1. - Caption.currentCaptionIndex = -1; - // Used to track if the focus is coming from a click or tabbing. This - // has to be known to decide if, when a caption gets the focus, an - // outline has to be drawn (tabbing) or not (mouse click). - Caption.isMouseFocus = false; - Caption.addPaddings(); - Caption.rendered = true; - } - - function reRenderCaption() { + function renderCaption(start, captions) { var Caption = this.videoCaption; - Caption.currentIndex = null; + var onRender = function () { + Caption.addPaddings(); + // Enables or disables automatic scrolling of the captions when the + // video is playing. This feature has to be disabled when tabbing + // through them as it interferes with that action. Initially, have + // this flag enabled as we assume mouse use. Then, if the first + // caption (through forward tabbing) or the last caption (through + // backwards tabbing) gets the focus, disable that feature. + // Re-enable it if tabbing then cycles out of the the captions. + Caption.autoScrolling = true; + // Keeps track of where the focus is situated in the array of + // captions. Used to implement the automatic scrolling behavior and + // decide if the outline around a caption has to be hidden or shown + // on a mouseenter or mouseleave. Initially, no caption has the + // focus, set the index to -1. + Caption.currentCaptionIndex = -1; + // Used to track if the focus is coming from a click or tabbing. This + // has to be known to decide if, when a caption gets the focus, an + // outline has to be drawn (tabbing) or not (mouse click). + Caption.isMouseFocus = false; + Caption.rendered = true; + }; + + Caption.rendered = false; Caption.subtitlesEl.empty(); - buildCaptions(Caption.subtitlesEl, Caption.captions, Caption.start); - Caption.addPaddings(); - Caption.rendered = true; + Caption.setSubtitlesHeight(); + buildCaptions(Caption.subtitlesEl, start, captions).done(onRender); } function addPaddings() { @@ -529,7 +508,7 @@ function () { // off again as it may have been enabled in captionBlur. if ( captionIndex <= 1 || - captionIndex >= this.videoCaption.captions.length - 2 + captionIndex >= this.videoCaption.sjson.getSize() - 2 ) { this.videoCaption.autoScrolling = false; } @@ -547,7 +526,7 @@ function () { // tabbing back out of the captions or on the last element and tabbing // forward out of the captions. if (captionIndex === 0 || - captionIndex === this.videoCaption.captions.length - 1) { + captionIndex === this.videoCaption.sjson.getSize() - 1) { this.videoCaption.autoScrolling = true; } @@ -579,38 +558,13 @@ function () { } } - function search(time) { - var index, max, min; - - if (this.videoCaption.loaded) { - min = 0; - max = this.videoCaption.start.length - 1; - - if (time < this.videoCaption.start[min]) { - return -1; - } - while (min < max) { - index = Math.ceil((max + min) / 2); - - if (time < this.videoCaption.start[index]) { - max = index - 1; - } - - if (time >= this.videoCaption.start[index]) { - min = index; - } - } - - return min; - } - - return undefined; - } - function play() { if (this.videoCaption.loaded) { if (!this.videoCaption.rendered) { - this.videoCaption.renderCaption(); + var start = this.videoCaption.sjson.getStartTimes(), + captions = this.videoCaption.sjson.getCaptions(); + + this.videoCaption.renderCaption(start, captions); } this.videoCaption.playing = true; @@ -627,20 +581,12 @@ function () { var newIndex; if (this.videoCaption.loaded) { - // Current mode === 'flash' can only be for YouTube videos. So, we - // don't have to also check for videoType === 'youtube'. if (this.isFlashMode()) { - // Total play time changes with speed change. Also there is - // a 250 ms delay we have to take into account. - time = Math.round( - Time.convert(time, this.speed, '1.0') * 1000 + 100 - ); - } else { - // Total play time remains constant when speed changes. - time = Math.round(time * 1000 + 100); + time = Time.convert(time, this.speed, '1.0'); } - newIndex = this.videoCaption.search(time); + time = Math.round(time * 1000 + 100); + newIndex = this.videoCaption.sjson.search(time); if ( typeof newIndex !== 'undefined' && @@ -658,39 +604,27 @@ function () { .addClass('current'); this.videoCaption.currentIndex = newIndex; - this.videoCaption.scrollCaption(); } } } function seekPlayer(event) { - var time; + var time = parseInt($(event.target).data('start'), 10); - event.preventDefault(); - - // Current mode === 'flash' can only be for YouTube videos. So, we - // don't have to also check for videoType === 'youtube'. if (this.isFlashMode()) { - // Total play time changes with speed change. Also there is - // a 250 ms delay we have to take into account. - time = Math.round( - Time.convert( - $(event.target).data('start'), '1.0', this.speed - ) / 1000 - ); - } else { - // Total play time remains constant when speed changes. - time = parseInt($(event.target).data('start'), 10)/1000; + time = Math.round(Time.convert(time, '1.0', this.speed)); } this.trigger( 'videoPlayer.onCaptionSeek', { 'type': 'onCaptionSeek', - 'time': time + 'time': time/1000 } ); + + event.preventDefault(); } function calculateOffset(element) { @@ -801,4 +735,4 @@ function () { } }); -}(RequireJS.requirejs, RequireJS.require, RequireJS.define)); +}(RequireJS.define)); diff --git a/common/lib/xmodule/xmodule/video_module/video_module.py b/common/lib/xmodule/xmodule/video_module/video_module.py index 7e50faa15c..edbe9fa370 100644 --- a/common/lib/xmodule/xmodule/video_module/video_module.py +++ b/common/lib/xmodule/xmodule/video_module/video_module.py @@ -72,6 +72,8 @@ class VideoModule(VideoFields, VideoStudentViewHandlers, XModule): 'js': [ resource_string(module, 'js/src/video/00_video_storage.js'), resource_string(module, 'js/src/video/00_resizer.js'), + resource_string(module, 'js/src/video/00_async_process.js'), + resource_string(module, 'js/src/video/00_sjson.js'), resource_string(module, 'js/src/video/01_initialize.js'), resource_string(module, 'js/src/video/025_focus_grabber.js'), resource_string(module, 'js/src/video/02_html5_video.js'), diff --git a/lms/djangoapps/courseware/features/video.feature b/lms/djangoapps/courseware/features/video.feature index 9faf78f86c..05adcb090e 100644 --- a/lms/djangoapps/courseware/features/video.feature +++ b/lms/djangoapps/courseware/features/video.feature @@ -111,18 +111,18 @@ Feature: LMS Video component # 11 Scenario: Language menu works correctly in Video component - Given I am registered for the course "test_course" - And I have a "chinese_transcripts.srt" transcript file in assets - And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets - And it has a video in "Youtube" mode: + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Youtube" mode: | transcripts | sub | | {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | - And I make sure captions are closed - And I see video menu "language" with correct items - And I select language with code "zh" - 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 + And I make sure captions are closed + And I see video menu "language" with correct items + And I select language with code "zh" + 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 # 12 Scenario: CC button works correctly w/o english transcript in HTML5 mode of Video component @@ -237,29 +237,31 @@ Feature: LMS Video component # 21 Scenario: Download button works correctly for non-english transcript in Youtube mode of Video component - Given I am registered for the course "test_course" - And I have a "chinese_transcripts.srt" transcript file in assets - And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets - And it has a video in "Youtube" mode: + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "Youtube" mode: | transcripts | sub | download_track | | {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true | - Then I can download transcript in "srt" format that has text "Hi, welcome to Edx." - And I select language with code "zh" - And I see "好 各位同学" text in the captions - Then I can download transcript in "srt" format that has text "好 各位同学" + And I see "Hi, welcome to Edx." text in the captions + Then I can download transcript in "srt" format that has text "Hi, welcome to Edx." + And I select language with code "zh" + And I see "好 各位同学" text in the captions + Then I can download transcript in "srt" format that has text "好 各位同学" # 22 Scenario: Download button works correctly for non-english transcript in HTML5 mode of Video component - Given I am registered for the course "test_course" - And I have a "chinese_transcripts.srt" transcript file in assets - And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets - And it has a video in "HTML5" mode: + Given I am registered for the course "test_course" + And I have a "chinese_transcripts.srt" transcript file in assets + And I have a "subs_OEoXaMPEzfM.srt.sjson" transcript file in assets + And it has a video in "HTML5" mode: | transcripts | sub | download_track | | {"zh": "chinese_transcripts.srt"} | OEoXaMPEzfM | true | - Then I can download transcript in "srt" format that has text "Hi, welcome to Edx." - And I select language with code "zh" - And I see "好 各位同学" text in the captions - Then I can download transcript in "srt" format that has text "好 各位同学" + And I see "Hi, welcome to Edx." text in the captions + Then I can download transcript in "srt" format that has text "Hi, welcome to Edx." + And I select language with code "zh" + And I see "好 各位同学" text in the captions + Then I can download transcript in "srt" format that has text "好 各位同学" # 23 Scenario: Download button works correctly w/o english transcript in HTML5 mode of Video component @@ -298,8 +300,10 @@ Feature: LMS Video component And a video "D" in "Youtube_HTML5" mode in position "3" of sequential And I open the section with videos Then videos have rendered in "HTML5" mode - And I see "Hi, welcome to Edx." text in the captions - And I see "Equal transcripts" text in the captions + And I see text in the captions: + | text | + | Hi, welcome to Edx. | + | Equal transcripts | When I open video "C" Then the video has rendered in "HTML5" mode And I make sure captions are opened diff --git a/lms/djangoapps/courseware/features/video.py b/lms/djangoapps/courseware/features/video.py index 938b22bae5..7332e45072 100644 --- a/lms/djangoapps/courseware/features/video.py +++ b/lms/djangoapps/courseware/features/video.py @@ -240,25 +240,25 @@ def set_window_dimensions(width, height): def duration(): - """ - Total duration of the video, in seconds. - """ - elapsed_time, duration = video_time() - return duration + """ + Total duration of the video, in seconds. + """ + elapsed_time, duration = video_time() + return duration def video_time(): - """ - Return a tuple `(elapsed_time, duration)`, each in seconds. - """ - # The full time has the form "0:32 / 3:14" - full_time = world.css_text('div.vidtime') + """ + Return a tuple `(elapsed_time, duration)`, each in seconds. + """ + # The full time has the form "0:32 / 3:14" + full_time = world.css_text('div.vidtime') - # Split the time at the " / ", to get ["0:32", "3:14"] - elapsed_str, duration_str = full_time.split(' / ') + # Split the time at the " / ", to get ["0:32", "3:14"] + elapsed_str, duration_str = full_time.split(' / ') - # Convert each string to seconds - return (parse_time_str(elapsed_str), parse_time_str(duration_str)) + # Convert each string to seconds + return (parse_time_str(elapsed_str), parse_time_str(duration_str)) def parse_time_str(time_str): @@ -316,13 +316,13 @@ def visit_video_section(_step): @step('I select the "([^"]*)" speed$') def i_select_video_speed(_step, speed): - change_video_speed(speed) + change_video_speed(speed) @step('I select the "([^"]*)" speed on video "([^"]*)"$') def change_video_speed_on_video(_step, speed, player_id): - navigate_to_an_item_in_a_sequence(world.video_sequences[player_id]) - change_video_speed(speed) + navigate_to_an_item_in_a_sequence(world.video_sequences[player_id]) + change_video_speed(speed) @step('I open video "([^"]*)"$') @@ -419,7 +419,15 @@ def i_see_menu(_step, menu): @step('I see "([^"]*)" text in the captions$') def check_text_in_the_captions(_step, text): - assert world.browser.is_text_present(text.strip()) + world.wait_for(lambda _: world.css_text('.subtitles')) + actual_text = world.css_text('.subtitles') + assert (text in actual_text) + + +@step('I see text in the captions:') +def check_captions(_step): + for index, video in enumerate(_step.hashes): + assert (video.get('text') in world.css_text('.subtitles', index=index)) @step('I select language with code "([^"]*)"$') @@ -441,14 +449,14 @@ def select_language(_step, code): # Make sure that all ajax requests that affects the display of captions are finished. # For example, request to get new translation etc. world.wait_for_ajax_complete() - assert world.css_visible('.subtitles') - + world.wait_for_visible('.subtitles') @step('I click video button "([^"]*)"$') def click_button(_step, button): world.css_click(VIDEO_BUTTONS[button]) + @step('I see video starts playing from "([^"]*)" position$') def start_playing_video_from_n_seconds(_step, position): world.wait_for( @@ -504,9 +512,7 @@ def video_alignment(_step, transcript_visibility): height = abs(expected['height'] - real['height']) <= 5 # Restore initial window size - set_window_dimensions( - initial['width'], initial['height'] - ) + set_window_dimensions(initial['width'], initial['height']) assert all([width, height])