Commit fe8e6026 by polesye

BLD-852: Fix video positioning in full view mode.

parent b292f6c2
......@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected.
Blades: Fix bug when transcript cutting off view in full view mode. BLD-852.
Blades: Show start time or starting position on slider and VCR. BLD-823.
Common: Upgraded CodeMirror to 3.21.0 with an accessibility patch applied.
......
......@@ -256,6 +256,11 @@ div.video {
margin: 0 lh() 0 0;
padding: 0;
@media (max-width: 1120px) {
margin-right: lh(.5);
font-size: em(14);
}
li {
float: left;
margin-bottom: 0;
......@@ -292,11 +297,13 @@ div.video {
}
div.vidtime {
padding-left: lh(.75);
font-weight: bold;
line-height: 46px; //height of play pause buttons
padding-left: lh(.75);
-webkit-font-smoothing: antialiased;
padding-left: lh(.75);
@media (max-width: 1120px) {
padding-left: lh(.5);
}
}
}
}
......@@ -389,8 +396,8 @@ div.video {
.menu{
width: 131px;
@media (max-width: 1024px) {
width: 101px;
@media (max-width: 1120px) {
width: 80px;
}
}
......@@ -403,9 +410,9 @@ div.video {
min-width: 116px;
text-indent: 0;
@media (max-width: 1024px) {
@media (max-width: 1120px) {
min-width: 0;
width: 86px;
width: 60px;
}
h3 {
......@@ -418,7 +425,7 @@ div.video {
text-transform: uppercase;
color: #999;
@media (max-width: 1024px) {
@media (max-width: 1120px) {
display: none;
}
}
......@@ -429,7 +436,7 @@ div.video {
margin-bottom: 0;
padding: 0 lh(.5) 0 0;
@media (max-width: 1024px) {
@media (max-width: 1120px) {
padding: 0 lh(.5) 0 lh(.5);
}
......@@ -676,9 +683,10 @@ div.video {
vertical-align: middle;
&.closed {
ol.subtitles {
right: -(flex-grid(4));
width: auto;
div.tc-wrapper {
article.video-wrapper {
width: 100%;
}
}
}
......@@ -698,17 +706,16 @@ div.video {
div.tc-wrapper {
@include clearfix;
display: table;
width: 100%;
height: 100%;
position: static;
article.video-wrapper {
width: 100%;
display: table-cell;
height: 100%;
width: 75%;
vertical-align: middle;
float: none;
margin-right: 0;
object, iframe, video{
position: absolute;
......@@ -727,16 +734,12 @@ div.video {
}
ol.subtitles {
@include box-sizing(border-box);
@include transition(none);
background: rgba(#000, .8);
bottom: 0;
background: #000;
height: 100%;
max-height: 460px;
max-width: flex-grid(3);
width: 25%;
padding: lh();
position: fixed;
right: 0;
top: 0;
visibility: visible;
li {
......
......@@ -240,12 +240,19 @@
'setParams',
'setMode'
],
obj = {};
obj = {},
delta = {
add: jasmine.createSpy().andReturn(obj),
substract: jasmine.createSpy().andReturn(obj),
reset: jasmine.createSpy().andReturn(obj)
};
$.each(methods, function (index, method) {
obj[method] = jasmine.createSpy(method).andReturn(obj);
});
obj.delta = delta;
return obj;
}());
......
......@@ -18,7 +18,7 @@ function (Resizer) {
'</div>',
'</div>'
].join(''),
config, container, element, originalConsoleLog;
config, container, element;
beforeEach(function () {
setFixtures(html);
......@@ -30,14 +30,9 @@ function (Resizer) {
element: element
};
originalConsoleLog = window.console.log;
spyOn(console, 'log');
});
afterEach(function () {
window.console.log = originalConsoleLog;
});
it('When Initialize without required parameters, log message is shown',
function () {
new Resizer({ });
......@@ -134,7 +129,7 @@ function (Resizer) {
expect(spiesList[0].calls.length).toEqual(1);
});
it('All callbacks are removed', function () {
it('all callbacks are removed', function () {
$.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy);
});
......@@ -147,7 +142,7 @@ function (Resizer) {
});
});
it('Specific callback is removed', function () {
it('specific callback is removed', function () {
$.each(spiesList, function (index, spy) {
resizer.callbacks.add(spy);
});
......@@ -176,9 +171,86 @@ function (Resizer) {
});
});
});
describe('Delta', function () {
var resizer;
beforeEach(function () {
resizer = new Resizer(config);
});
it('adding delta align correctly by height', function () {
var delta = 100,
expectedHeight = container.height() + delta,
realHeight;
resizer
.delta.add(delta, 'height')
.setMode('height');
realHeight = element.height();
expect(realHeight).toBe(expectedHeight);
});
it('adding delta align correctly by width', function () {
var delta = 100,
expectedWidth = container.width() + delta,
realWidth;
resizer
.delta.add(delta, 'width')
.setMode('width');
realWidth = element.width();
expect(realWidth).toBe(expectedWidth);
});
it('substract delta align correctly by height', function () {
var delta = 100,
expectedHeight = container.height() - delta,
realHeight;
resizer
.delta.substract(delta, 'height')
.setMode('height');
realHeight = element.height();
expect(realHeight).toBe(expectedHeight);
});
it('substract delta align correctly by width', function () {
var delta = 100,
expectedWidth = container.width() - delta,
realWidth;
resizer
.delta.substract(delta, 'width')
.setMode('width');
realWidth = element.width();
expect(realWidth).toBe(expectedWidth);
});
it('reset delta', function () {
var delta = 100,
expectedWidth = container.width(),
realWidth;
resizer
.delta.substract(delta, 'width')
.delta.reset()
.setMode('width');
realWidth = element.width();
expect(realWidth).toBe(expectedWidth);
});
});
});
});
......
......@@ -106,13 +106,6 @@
});
});
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(
......
......@@ -549,6 +549,17 @@
});
});
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();
......
......@@ -711,6 +711,7 @@ function (VideoPlayer) {
state.videoEl = $('video, iframe');
spyOn(state.videoCaption, 'resize').andCallThrough();
spyOn($.fn, 'trigger').andCallThrough();
state.videoControl.toggleFullScreen(jQuery.Event('click'));
});
......@@ -725,7 +726,8 @@ function (VideoPlayer) {
it('tell VideoCaption to resize', function () {
expect(state.videoCaption.resize).toHaveBeenCalled();
expect(state.resizer.setMode).toHaveBeenCalled();
expect(state.resizer.setMode).toHaveBeenCalledWith('both');
expect(state.resizer.delta.substract).toHaveBeenCalled();
});
});
......@@ -758,6 +760,7 @@ function (VideoPlayer) {
expect(state.videoCaption.resize).toHaveBeenCalled();
expect(state.resizer.setMode)
.toHaveBeenCalledWith('width');
expect(state.resizer.delta.reset).toHaveBeenCalled();
});
});
});
......
......@@ -13,20 +13,24 @@ function () {
elementRatio: null
},
callbacksList = [],
delta = {
height: 0,
width: 0
},
module = {},
mode = null,
config;
var initialize = function (params) {
if (config) {
config = $.extend(true, config, params);
} else {
config = $.extend(true, {}, defaults, params);
if (!config) {
config = defaults;
}
config = $.extend(true, {}, config, params);
if (!config.element) {
console.log(
'[Video info]: Required parameter `element` is not passed.'
'Required parameter `element` is not passed.'
);
}
......@@ -35,8 +39,8 @@ function () {
var getData = function () {
var container = $(config.container),
containerWidth = container.width(),
containerHeight = container.height(),
containerWidth = container.width() + delta.width,
containerHeight = container.height() + delta.height,
containerRatio = config.containerRatio,
element = $(config.element),
......@@ -74,7 +78,6 @@ function () {
default:
if (data.containerRatio >= data.elementRatio) {
alignByHeightOnly();
} else {
alignByWidthOnly();
}
......@@ -142,7 +145,7 @@ function () {
addCallback(decorator);
} else {
console.error('[Video info]: TypeError: Argument is not a function.');
console.error('TypeError: Argument is not a function.');
}
return module;
......@@ -168,6 +171,29 @@ function () {
}
};
var cleanDelta = function () {
delta['height'] = 0;
delta['width'] = 0;
return module;
};
var addDelta = function (value, side) {
if (_.isNumber(value) && _.isNumber(delta[side])) {
delta[side] += value;
}
return module;
};
var substractDelta = function (value, side) {
if (_.isNumber(value) && _.isNumber(delta[side])) {
delta[side] -= value;
}
return module;
};
initialize.apply(module, arguments);
return $.extend(true, module, {
......@@ -181,6 +207,11 @@ function () {
once: addOnceCallback,
remove: removeCallback,
removeAll: removeCallbacks
},
delta: {
add: addDelta,
substract: substractDelta,
reset: cleanDelta
}
});
};
......
......@@ -221,7 +221,7 @@ function (HTML5Video, Resizer) {
state.resizer = new Resizer({
element: state.videoEl,
elementRatio: videoWidth/videoHeight,
container: state.videoEl.parent()
container: state.container
})
.callbacks.once(function() {
state.trigger('videoCaption.resize', null);
......@@ -235,7 +235,11 @@ function (HTML5Video, Resizer) {
});
}
$(window).bind('resize', _.debounce(state.resizer.align, 100));
$(window).on('resize', _.debounce(function () {
state.trigger('videoControl.updateControlsHeight', null);
state.trigger('videoCaption.resize', null);
state.resizer.align();
}, 100));
}
// function _restartUsingFlash(state)
......
......@@ -40,6 +40,7 @@ function () {
showPlayPlaceholder: showPlayPlaceholder,
toggleFullScreen: toggleFullScreen,
togglePlayback: togglePlayback,
updateControlsHeight: updateControlsHeight,
updateVcrVidTime: updateVcrVidTime
};
......@@ -83,6 +84,8 @@ function () {
'role': 'slider',
'title': gettext('Video slider')
});
state.videoControl.updateControlsHeight();
}
// function _bindHandlers(state)
......@@ -91,6 +94,23 @@ function () {
function _bindHandlers(state) {
state.videoControl.playPauseEl.on('click', state.videoControl.togglePlayback);
state.videoControl.fullScreenEl.on('click', state.videoControl.toggleFullScreen);
state.el.on('fullscreen', function (event, isFullScreen) {
var height = state.videoControl.updateControlsHeight();
if (isFullScreen) {
state.resizer
.delta
.substract(height, 'height')
.setMode('both');
} else {
state.resizer
.delta
.reset()
.setMode('width');
}
});
$(document).on('keyup', state.videoControl.exitFullScreen);
if ((state.videoType === 'html5') && (state.config.autohideHtml5)) {
......@@ -110,12 +130,22 @@ function () {
});
}
}
function _getControlsHeight(control) {
return control.el.height() + 0.5 * control.sliderEl.height();
}
// ***************************************************************
// Public functions start here.
// These are available via the 'state' object. Their context ('this' keyword) is the 'state' object.
// The magic private function that makes them available and sets up their context is makeFunctionsPublic().
// ***************************************************************
function updateControlsHeight () {
this.videoControl.height = _getControlsHeight(this.videoControl);
return this.videoControl.height;
}
function show() {
this.videoControl.el.removeClass('is-hidden');
this.el.trigger('controls:show', arguments);
......@@ -234,13 +264,6 @@ function () {
this.videoControl.fullScreenState = this.isFullScreen = false;
fullScreenClassNameEl.removeClass('video-fullscreen');
text = gettext('Fill browser');
this.resizer
.setParams({
container: this.videoEl.parent()
})
.setMode('width');
win.scrollTop(this.scrollPos);
} else {
this.scrollPos = win.scrollTop();
......@@ -248,13 +271,6 @@ function () {
this.videoControl.fullScreenState = this.isFullScreen = true;
fullScreenClassNameEl.addClass('video-fullscreen');
text = gettext('Exit full browser');
this.resizer
.setParams({
container: window
})
.setMode('both');
}
this.videoControl.fullScreenEl
......@@ -262,6 +278,7 @@ function () {
.text(text);
this.trigger('videoCaption.resize', null);
this.el.trigger('fullscreen', [this.isFullScreen]);
}
function exitFullScreen(event) {
......
......@@ -135,7 +135,6 @@ function () {
var self = this,
Caption = this.videoCaption;
$(window).bind('resize', Caption.resize);
Caption.hideSubtitlesEl.on({
'click': Caption.toggle
});
......@@ -754,8 +753,12 @@ function () {
});
}
if (this.resizer && !this.isFullScreen) {
this.resizer.alignByWidthOnly();
if (this.resizer) {
if (this.isFullScreen) {
this.resizer.setMode('both');
} else {
this.resizer.alignByWidthOnly();
}
}
this.videoCaption.setSubtitlesHeight();
......@@ -769,17 +772,8 @@ function () {
}
function captionHeight() {
var paddingTop;
if (this.isFullScreen) {
paddingTop = parseInt(
this.videoCaption.subtitlesEl.css('padding-top'), 10
);
return $(window).height() -
this.videoControl.el.height() -
0.5 * this.videoControl.sliderEl.height() -
2 * paddingTop;
return this.container.height() - this.videoControl.height;
} else {
return this.container.height();
}
......
......@@ -83,7 +83,7 @@ Feature: LMS Video component
Scenario: Language menu works correctly in Video component
Given the course has a Video component in Youtube mode:
| transcripts | sub |
| {"zh": "OEoXaMPEzfM"} | OEoXaMPEzfM |
| {"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"
......@@ -95,7 +95,7 @@ Feature: LMS Video component
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"} |
| {"zh": "chinese_transcripts.srt"} |
And I make sure captions are opened
Then I see "好 各位同学" text in the captions
......@@ -113,7 +113,7 @@ Feature: LMS Video component
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"} |
| {"zh": "chinese_transcripts.srt"} |
And I make sure captions are opened
Then I see "好 各位同学" text in the captions
......@@ -129,3 +129,29 @@ Feature: LMS Video component
Scenario: CC button is hidden if no translations
Given the course has a Video component in Youtube mode
Then button "CC" is hidden
# 16
Scenario: Video is aligned correctly if transcript is visible in fullscreen mode
Given the course has a Video component in HTML5 mode:
| sub |
| OEoXaMPEzfM |
And I make sure captions are opened
And I click video button "fullscreen"
Then I see video aligned correctly with enabled transcript
# 17
Scenario: Video is aligned correctly if transcript is hidden in fullscreen mode
Given the course has a Video component in Youtube mode
And I click video button "fullscreen"
Then I see video aligned correctly without enabled transcript
# 18
Scenario: Video is aligned correctly on transcript toggle in fullscreen mode
Given the course has a Video component in Youtube mode:
| sub |
| OEoXaMPEzfM |
And I make sure captions are opened
And I click video button "fullscreen"
Then I see video aligned correctly with enabled transcript
And I click video button "CC"
Then I see video aligned correctly without enabled transcript
......@@ -21,27 +21,24 @@ HTML5_SOURCES = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.webm',
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.ogv',
]
HTML5_SOURCES_INCORRECT = [
'https://s3.amazonaws.com/edx-course-videos/edx-intro/edX-FA12-cware-1_100.mp99',
]
VIDEO_BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
'fullscreen': '.add-fullscreen',
}
VIDEO_MENUS = {
'language': '.lang .menu',
'speed': '.speed .menu',
}
VIDEO_BUTTONS = {
'CC': '.hide-subtitles',
'volume': '.volume',
'play': '.video_control.play',
'pause': '.video_control.pause',
}
coursenum = 'test_course'
sequence = {}
......@@ -83,23 +80,22 @@ def add_video_to_course(course, player_mode, hashes, display_name='Video'):
if hashes:
kwargs['metadata'].update(hashes[0])
course_location = world.scenario_dict['COURSE'].location
if 'sub' in kwargs['metadata']:
filename = _get_sjson_filename(kwargs['metadata']['sub'], 'en')
_upload_file(filename, course_location)
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']:
filename = _get_transcript_filename(kwargs['metadata']['sub'], 'en')
_upload_file(filename, course_location)
for lang, videoId in kwargs['metadata']['transcripts'].items():
filename = _get_transcript_filename(videoId, lang)
for lang, filename in kwargs['metadata']['transcripts'].items():
_upload_file(filename, course_location)
world.scenario_dict['VIDEO'] = world.ItemFactory.create(**kwargs)
def _get_transcript_filename(videoId, lang):
def _get_sjson_filename(videoId, lang):
if lang == 'en':
return 'subs_{0}.srt.sjson'.format(videoId)
else:
......@@ -120,7 +116,7 @@ def _upload_file(filename, location):
def _navigate_to_an_item_in_a_sequence(number):
sequence_css = 'a[data-element="{0}"]'.format(number)
sequence_css = '#sequence-list a[data-element="{0}"]'.format(number)
world.css_click(sequence_css)
......@@ -136,6 +132,33 @@ def _open_menu(menu):
))
def _get_all_dimensions():
video = _get_dimensions('.video-player iframe, .video-player video')
wrapper = _get_dimensions('.tc-wrapper')
controls = _get_dimensions('.video-controls')
progress_slider = _get_dimensions('.video-controls > .slider')
expected = dict(wrapper)
expected['height'] -= controls['height'] + 0.5 * progress_slider['height']
return (video, expected)
def _get_dimensions(selector):
element = world.css_find(selector).first
return element._element.size
def _get_window_dimensions():
return world.browser.driver.get_window_size()
def _set_window_dimensions(width, height):
world.browser.driver.set_window_size(width, height)
# Wait 200 ms when JS finish resizing
world.wait(0.2)
@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')
......@@ -143,10 +166,7 @@ def does_not_autoplay(_step, video_type):
@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')
......@@ -203,7 +223,7 @@ def video_is_rendered(_step, mode):
@step('all sources are correct$')
def all_sources_are_correct(_step):
elements = world.css_find('.video video source')
elements = world.css_find('.video-player video source')
sources = [source['src'].split('?')[0] for source in elements]
assert set(sources) == set(HTML5_SOURCES)
......@@ -273,15 +293,8 @@ def select_language(_step, code):
world.wait_for_ajax_complete()
@step('I click on video button "([^"]*)"$')
def click_button(_step, button):
world.css_find(VIDEO_BUTTONS[button]).click()
@step('I click video button "([^"]*)"$')
def click_button_video(_step, button_type):
world.wait_for_ajax_complete()
button = button_type.strip()
def click_button(_step, button):
world.css_click(VIDEO_BUTTONS[button])
......@@ -309,3 +322,26 @@ def upload_to_assets(_step, filename):
def is_hidden_button(_step, button):
assert not world.css_visible(VIDEO_BUTTONS[button])
@step('I see video aligned correctly (with(?:out)?) enabled transcript$')
def video_alignment(_step, transcript_visibility):
# Width of the video container in css equal 75% of window if transcript enabled
wrapper_width = 75 if transcript_visibility == "with" else 100
initial = _get_window_dimensions()
_set_window_dimensions(300, 600)
real, expected = _get_all_dimensions()
width = round(100 * real['width']/expected['width']) == wrapper_width
_set_window_dimensions(600, 300)
real, expected = _get_all_dimensions()
height = abs(expected['height'] - real['height']) <= 5
# Restore initial window size
_set_window_dimensions(
initial['width'], initial['height']
)
assert all([width, height])
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment