Commit 4f28b81f by Chris Committed by GitHub

Merge pull request #13334 from edx/clrux/ac-571-video-transcript-download

AC-571 updating video download/transcript area
parents 13d7ea46 0ebc9047
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.video { .video {
@include clearfix(); @include clearfix();
background: rgb(240, 243, 245); // UXPL grayscale-cool xx-light; background: rgb(245, 245, 245); // UXPL grayscale x-back
display: block; display: block;
margin: 0 -12px; margin: 0 -12px;
padding: 12px; padding: 12px;
...@@ -74,70 +74,68 @@ ...@@ -74,70 +74,68 @@
width: 0px; width: 0px;
height: 0px; height: 0px;
} }
.wrapper-downloads { .downloads-heading {
margin: 0; margin: 1em 0 0 0;
padding: 0;
.video-download-button {
display: inline-block;
vertical-align: top;
margin: ($baseline*0.75) ($baseline/2) 0 0;
> a {
@include transition(all $tmg-f2 ease-in-out 0s);
@include font-size(14);
line-height : 14px;
float: left;
border-radius: 3px;
background-color: $very-light-text;
padding: ($baseline*0.75);
color: $lighter-base-font-color;
&:hover,
&:focus {
background-color: $action-primary-active-bg;
color: $very-light-text;
}
}
}
.video-tracks {
> a {
border-radius: 3px 0 0 3px;
}
> a.external-track {
border-radius: 3px;
}
} }
.wrapper-downloads {
display: flex;
.hd {
margin: 0;
}
.wrapper-download-video,
.wrapper-download-transcripts,
.wrapper-handouts,
.branding {
flex: 1;
margin-top: $baseline;
@include padding-right($baseline);
vertical-align: top;
}
.wrapper-download-video {
.video-sources {
margin: 0;
}
}
.wrapper-download-transcripts {
.list-download-transcripts {
margin: 0;
padding: 0;
list-style: none;
.transcript-option {
margin: 0;
}
}
}
.branding {
@include padding-right(0);
.branding { .host-tag {
display: inline-block; position: absolute;
float: right; left: -9999em;
margin: 15px 0 0 10px; display: inline-block;
vertical-align: top; vertical-align: middle;
color: $base-font-color;
.host-tag { }
@include margin-right($baseline/2);
position: absolute;
left: -9999em;
display: inline-block;
vertical-align: middle;
font-size: 70%;
color: #777;
}
.brand-logo { .brand-logo {
display: inline-block; display: inline-block;
max-width: 100%; max-width: 100%;
max-height: ($baseline*2); max-height: ($baseline*2);
padding: ($baseline/4) 0; padding: ($baseline/4) 0;
vertical-align: middle; vertical-align: middle;
} }
}
} }
}
.video-wrapper { .video-wrapper {
@include float(left); @include float(left);
......
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
<div class="tc-wrapper"> <div class="tc-wrapper">
<article class="video-wrapper"> <article class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span> <span tabindex="0" class="spinner" aria-hidden="false" aria-label="Loading video player"></span>
<span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span> <span tabindex="-1" class="btn-play is-hidden" aria-hidden="true" aria-label="Play video"></span>
<div class="video-player-pre"></div> <div class="video-player-pre"></div>
<section class="video-player"> <section class="video-player">
<div id="id"></div> <div id="id"></div>
...@@ -29,22 +29,32 @@ ...@@ -29,22 +29,32 @@
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
<ul class="wrapper-downloads"> <h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts">Downloads and transcripts</h3>
<li class="video-tracks"> <div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts">
<div class="a11y-menu-container"> <div class="wrapper-download-video">
<a class="a11y-menu-button" href="#" title=".srt">.srt</a> <h4 class="hd hd-5">Video</h4>
<ol class="a11y-menu-list"> <a class="btn-link video-sources video-download-button" href="#">
<li class="a11y-menu-item"> Download video file
<a class="a11y-menu-item-link" href="#txt" title="Text (.txt) file" data-value="txt">Text (.txt) file</a> </a>
</li> </div>
<li class="a11y-menu-item active"> <div class="wrapper-download-transcripts">
<a class="a11y-menu-item-link" href="#srt" title="SubRip (.srt) file" data-value="srt">SubRip (.srt) file</a> <h4 class="hd hd-5">Transcripts</h4>
</li> <ul class="list-download-transcripts">
</ol> <li class="transcript-option">
<a href="#" class="btn btn-link" data-href="txt">Download Text (.txt) file</a>
</li>
<li class="transcript-option">
<a href="#" class="btn btn-link" data-href="srt">Download SubRip (.srt) file</a>
</li>
</ul>
<a href="#" class="external-track">Download transcript</a>
</div> </div>
</li> <div class="wrapper-handouts">
</ul> <h4 class="hd hd-5">Handouts</h4>
<a href="#">Download Handout</a>
</div>
</div>
</div> </div>
</div> </div>
......
(function(undefined) {
describe('Video Accessible Menu', function() {
var state;
afterEach(function() {
$('source').remove();
state.storage.clear();
state.videoPlayer.destroy();
});
describe('constructor', function() {
describe('always', function() {
var videoTracks, container, button, menu, menuItems,
menuItemsLinks;
beforeEach(function() {
state = jasmine.initializePlayer();
videoTracks = $('li.video-tracks');
container = videoTracks.children('div.a11y-menu-container');
button = container.children('a.a11y-menu-button');
menuList = container.children('ol.a11y-menu-list');
menuItems = menuList.children('li.a11y-menu-item');
menuItemsLinks = menuItems.children('a.a11y-menu-item-link');
});
it('add the accessible menu', function() {
var activeMenuItem;
// Make sure we have the expected HTML structure:
// Menu container exists
expect(container.length).toBe(1);
// Only one button and one menu list per menu container.
expect(button.length).toBe(1);
expect(menuList.length).toBe(1);
// At least one menu item and one menu link per menu
// container. Exact length test?
expect(menuItems.length).toBeGreaterThan(0);
expect(menuItemsLinks.length).toBeGreaterThan(0);
expect(menuItems.length).toBe(menuItemsLinks.length);
// And one menu item is active
activeMenuItem = menuItems.filter('.active');
expect(activeMenuItem.length).toBe(1);
expect(activeMenuItem.children('a.a11y-menu-item-link'))
.toHaveData('value', 'srt');
expect(activeMenuItem.children('a.a11y-menu-item-link'))
.toHaveHtml('SubRip (.srt) file');
/* TO DO: Check that all the anchors contain correct text.
$.each(li.toArray().reverse(), function (index, link) {
expect($(link)).toHaveData(
'speed', state.videoSpeedControl.speeds[index]
);
expect($(link).find('a').text()).toBe(
state.videoSpeedControl.speeds[index] + 'x'
);
});
*/
});
});
describe('when running', function() {
var videoTracks, container, button, menu, menuItems,
menuItemsLinks, KEY = $.ui.keyCode,
keyPressEvent = function(key) {
return $.Event('keydown', {keyCode: key});
},
tabBackPressEvent = function() {
return $.Event('keydown',
{keyCode: KEY.TAB, shiftKey: true});
},
tabForwardPressEvent = function() {
return $.Event('keydown',
{keyCode: KEY.TAB, shiftKey: false});
},
// Get previous element in array or cyles back to the last
// if it is the first.
previousSpeed = function(index) {
return speedEntries.eq(index < 1 ?
speedEntries.length - 1 :
index - 1);
},
// Get next element in array or cyles back to the first if
// it is the last.
nextSpeed = function(index) {
return speedEntries.eq(index >= speedEntries.length - 1 ?
0 :
index + 1);
};
beforeEach(function() {
state = jasmine.initializePlayer();
videoTracks = $('li.video-tracks');
container = videoTracks.children('div.a11y-menu-container');
button = container.children('a.a11y-menu-button');
menuList = container.children('ol.a11y-menu-list');
menuItems = menuList.children('li.a11y-menu-item');
menuItemsLinks = menuItems.children('a.a11y-menu-item-link');
spyOn($.fn, 'focus').and.callThrough();
});
it('open/close the menu on mouseenter/mouseleave', function() {
container.mouseenter();
expect(container).toHaveClass('open');
container.mouseleave();
expect(container).not.toHaveClass('open');
});
it('do not close the menu on mouseleave if a menu item has ' +
'focus', function() {
// Open menu. Focus is on last menu item.
container.trigger(keyPressEvent(KEY.ENTER));
container.mouseenter().mouseleave();
expect(container).toHaveClass('open');
});
it('close the menu on click', function() {
container.mouseenter().click();
expect(container).not.toHaveClass('open');
});
it('close the menu on outside click', function() {
container.trigger(keyPressEvent(KEY.ENTER));
$(window).click();
expect(container).not.toHaveClass('open');
});
it('open the menu on ENTER keydown', function() {
container.trigger(keyPressEvent(KEY.ENTER));
expect(container).toHaveClass('open');
expect(menuItemsLinks.last().focus).toHaveBeenCalled();
});
it('open the menu on SPACE keydown', function() {
container.trigger(keyPressEvent(KEY.SPACE));
expect(container).toHaveClass('open');
expect(menuItemsLinks.last().focus).toHaveBeenCalled();
});
it('open the menu on UP keydown', function() {
container.trigger(keyPressEvent(KEY.UP));
expect(container).toHaveClass('open');
expect(menuItemsLinks.last().focus).toHaveBeenCalled();
});
it('close the menu on ESCAPE keydown', function() {
container.trigger(keyPressEvent(KEY.ESCAPE));
expect(container).not.toHaveClass('open');
});
it('UP and DOWN keydown function as expected on menu items',
function() {
// Iterate through list in both directions and check if
// things wrap up correctly.
var lastEntry = menuItemsLinks.length - 1, i;
// First open menu
container.trigger(keyPressEvent(KEY.UP));
// Iterate with UP key until we have looped.
for (i = lastEntry; i >= 0; i--) {
menuItemsLinks.eq(i).trigger(keyPressEvent(KEY.UP));
}
// Iterate with DOWN key until we have looped.
for (i = 0; i <= lastEntry; i++) {
menuItemsLinks.eq(i).trigger(keyPressEvent(KEY.DOWN));
}
// Test if each element has been called twice.
expect($.fn.focus.calls.count())
.toEqual(2 * menuItemsLinks.length + 1);
});
it('ESC keydown on menu item closes menu', function() {
// First open menu. Focus is on last speed entry.
container.trigger(keyPressEvent(KEY.UP));
menuItemsLinks.last().trigger(keyPressEvent(KEY.ESCAPE));
// Menu is closed and focus has been returned to speed
// control.
expect(container).not.toHaveClass('open');
expect(container.focus).toHaveBeenCalled();
});
it('ENTER keydown on menu item selects its data and closes menu',
function() {
// First open menu.
container.trigger(keyPressEvent(KEY.UP));
// Focus on '.txt'
menuItemsLinks.eq(0).focus();
menuItemsLinks.eq(0).trigger(keyPressEvent(KEY.ENTER));
// Menu is closed, focus has been returned to container
// and file format is '.txt'.
/* TO DO
expect(container.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
*/
});
it('SPACE keydown on menu item selects its data and closes menu',
function() {
// First open menu.
container.trigger(keyPressEvent(KEY.UP));
// Focus on '.txt'
menuItemsLinks.eq(0).focus();
menuItemsLinks.eq(0).trigger(keyPressEvent(KEY.SPACE));
// Menu is closed, focus has been returned to container
// and file format is '.txt'.
/* TO DO
expect(speedControl.focus).toHaveBeenCalled();
expect($('.video_speeds li[data-speed="1.50"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('1.50x');
*/
});
// TO DO? No such behavior implemented.
xit('TAB + SHIFT keydown on speed entry closes menu and gives ' +
'focus to Play/Pause control', function() {
// First open menu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.UP));
speedEntries.last().trigger(tabBackPressEvent());
// Menu is closed and focus has been given to Play/Pause
// control.
expect(state.videoControl.playPauseEl.focus)
.toHaveBeenCalled();
});
// TO DO? No such behavior implemented.
xit('TAB keydown on speed entry closes menu and gives focus ' +
'to Volume control', function() {
// First open menu. Focus is on last speed entry.
speedControl.trigger(keyPressEvent(KEY.UP));
speedEntries.last().trigger(tabForwardPressEvent());
// Menu is closed and focus has been given to Volume
// control.
expect(state.videoVolumeControl.buttonEl.focus)
.toHaveBeenCalled();
});
});
});
// TODO
xdescribe('change file format', function() {
describe('when new file format is not the same', function() {
beforeEach(function() {
state = jasmine.initializePlayer();
state.videoSpeedControl.setSpeed(1.0);
spyOn(state.videoPlayer, 'onSpeedChange').and.callThrough();
$('li[data-speed="0.75"] .speed-link').click();
});
it('trigger speedChange event', function() {
expect(state.videoPlayer.onSpeedChange).toHaveBeenCalled();
expect(state.videoSpeedControl.currentSpeed).toEqual(0.75);
});
});
});
// TODO
xdescribe('onSpeedChange', function() {
beforeEach(function() {
state = jasmine.initializePlayer();
$('li[data-speed="1.0"] .speed-link').addClass('active');
state.videoSpeedControl.setSpeed(0.75);
});
it('set the new speed as active', function() {
expect($('.video_speeds li[data-speed="1.0"]'))
.not.toHaveClass('active');
expect($('.video_speeds li[data-speed="0.75"]'))
.toHaveClass('active');
expect($('.speeds p.active')).toHaveHtml('0.75x');
});
});
});
}).call(this);
(function(define) { (function(define) {
'use strict'; 'use strict';
// VideoAccessibleMenu module. // VideoTranscriptDownloadHandler module.
define( define(
'video/035_video_accessible_menu.js', [], 'video/035_video_accessible_menu.js', ['underscore'],
function() { function(_) {
/** /**
* Video Download Transcript control module. * Video Download Transcript control module.
* @exports video/035_video_accessible_menu.js * @exports video/035_video_accessible_menu.js
...@@ -11,231 +11,59 @@ function() { ...@@ -11,231 +11,59 @@ function() {
* @param {jquery Element} element * @param {jquery Element} element
* @param {Object} options * @param {Object} options
*/ */
var VideoAccessibleMenu = function(element, options) { var VideoTranscriptDownloadHandler = function(element, options) {
if (!(this instanceof VideoAccessibleMenu)) { if (!(this instanceof VideoTranscriptDownloadHandler)) {
return new VideoAccessibleMenu(element, options); return new VideoTranscriptDownloadHandler(element, options);
} }
_.bindAll(this, 'openMenu', 'openMenuHandler', 'closeMenu', 'closeMenuHandler', 'toggleMenuHandler', _.bindAll(this, 'clickHandler');
'clickHandler', 'keyDownHandler', 'render', 'menuItemsLinksFocused', 'changeFileType', 'setValue'
);
this.container = element; this.container = element;
this.options = options || {}; this.options = options || {};
if (this.container.find('.video-tracks')) { if (this.container.find('.wrapper-downloads .wrapper-download-transcripts')) {
this.initialize(); this.initialize();
} }
return false;
}; };
VideoAccessibleMenu.prototype = { VideoTranscriptDownloadHandler.prototype = {
/** Initializes the module. */ // Initializes the module.
initialize: function() { initialize: function() {
this.value = this.options.storage.getItem('transcript_download_format'); this.value = this.options.storage.getItem('transcript_download_format');
this.el = this.container.find('.video-tracks .a11y-menu-container'); this.el = this.container.find('.list-download-transcripts');
this.render(); this.el.on('click', '.btn-link', this.clickHandler);
this.bindHandlers();
}, },
/** // Event handler. We delay link clicks until the file type is set
* Creates any necessary DOM elements, attach them, and set their,
* initial configuration.
*/
render: function() {
var value, msg;
// For the time being, we assume that the menu structure is present in
// the template HTML. In the future accessible menu plugin, everything
// inside <div class='menu-container'></div> will be generated in this
// file.
this.button = this.el.children('.a11y-menu-button');
this.menuList = this.el.children('.a11y-menu-list');
this.menuItems = this.menuList.children('.a11y-menu-item');
this.menuItemsLinks = this.menuItems.children('.a11y-menu-item-link');
value = (function(val, activeElement) {
return val || activeElement.find('a').data('value') || 'srt';
}(this.value, this.menuItems.filter('.active')));
msg = '.' + value;
if (value) {
this.setValue(value);
this.button.text(gettext(msg));
}
},
/** Bind any necessary function callbacks to DOM events. */
bindHandlers: function() {
// Attach various events handlers to menu container.
this.el.on({
'mouseenter': this.openMenuHandler,
'mouseleave': this.closeMenuHandler,
'click': this.toggleMenuHandler,
'keydown': this.keyDownHandler
});
// Attach click and keydown event handlers to individual menu items.
this.menuItems
.on('click', 'a.a11y-menu-item-link', this.clickHandler)
.on('keydown', 'a.a11y-menu-item-link', this.keyDownHandler);
},
// Get previous element in array or cyles back to the last if it is the
// first.
previousMenuItemLink: function(links, index) {
return index < 1 ? links.last() : links.eq(index - 1);
},
// Get next element in array or cyles back to the first if it is the last.
nextMenuItemLink: function(links, index) {
return index >= links.length - 1 ? links.first() : links.eq(index + 1);
},
menuItemsLinksFocused: function() {
return this.menuItemsLinks.is(':focus');
},
openMenu: function(withoutHandler) {
// When menu items have focus, the menu stays open on
// mouseleave. A closeMenuHandler is added to the window
// element to have clicks close the menu when they happen
// outside of it. We namespace the click event to easily remove it (and
// only it) in closeMenu.
this.el.addClass('open');
this.button.text('...');
if (!withoutHandler) {
$(window).on('click.currentMenu', this.closeMenuHandler);
}
// @TODO: onOpen callback
},
closeMenu: function(withoutHandler) {
// Remove the previously added clickHandler from window element.
var msg = '.' + this.value;
this.el.removeClass('open');
this.button.text(gettext(msg));
if (!withoutHandler) {
$(window).off('click.currentMenu');
}
// @TODO: onClose callback
},
openMenuHandler: function() {
this.openMenu(true);
return false;
},
closeMenuHandler: function(event) {
// Only close the menu if no menu item link has focus or `click` event.
if (!this.menuItemsLinksFocused() || event.type === 'click') {
this.closeMenu(true);
}
return false;
},
toggleMenuHandler: function() {
if (this.el.hasClass('open')) {
this.closeMenu(true);
} else {
this.openMenu(true);
}
return false;
},
// Various event handlers. They all return false to stop propagation and
// prevent default behavior.
clickHandler: function(event) { clickHandler: function(event) {
this.changeFileType.call(this, event); var that = this,
this.closeMenu(true); fileType,
return false; data,
}, downloadUrl;
keyDownHandler: function(event) {
var KEY = $.ui.keyCode,
keyCode = event.keyCode,
target = $(event.currentTarget),
index;
if (target.is('a.a11y-menu-item-link')) {
index = target.parent().index();
switch (keyCode) {
// Scroll up menu, wrapping at the top. Keep menu open.
case KEY.UP:
this.previousMenuItemLink(this.menuItemsLinks, index).focus();
break;
// Scroll down menu, wrapping at the bottom. Keep menu
// open.
case KEY.DOWN:
this.nextMenuItemLink(this.menuItemsLinks, index).focus();
break;
// Close menu.
case KEY.TAB:
this.closeMenu();
// TODO
// What has to happen here? In speed menu, tabbing backward
// will give focus to Play/Pause button and tabbing
// forward to Volume button.
break;
// Close menu, give focus to button and change
// file type.
case KEY.ENTER:
case KEY.SPACE:
this.button.focus();
this.changeFileType.call(this, event);
this.closeMenu();
break;
// Close menu and give focus to speed control.
case KEY.ESCAPE:
this.closeMenu();
this.button.focus();
break;
}
return false;
}
else {
switch (keyCode) {
// Open menu and focus on last element of list above it.
case KEY.ENTER:
case KEY.SPACE:
case KEY.UP:
this.openMenu();
this.menuItemsLinks.last().focus();
break;
// Close menu.
case KEY.ESCAPE:
this.closeMenu();
break;
}
// We do not stop propagation and default behavior on a TAB
// keypress.
return event.keyCode === KEY.TAB;
}
     },
setValue: function(value) {
this.value = value;
this.menuItems
.removeClass('active')
.find("a[data-value='" + value + "']")
.parent()
.addClass('active');
},
changeFileType: function(event) { event.preventDefault();
var fileType = $(event.currentTarget).data('value'),
data = {'transcript_download_format': fileType};
this.setValue(fileType); fileType = $(event.target).data('value');
this.options.storage.setItem('transcript_download_format', fileType); data = {transcript_download_format: fileType};
downloadUrl = $(event.target).attr('href');
$.ajax({ $.ajax({
url: this.options.saveStateUrl, url: this.options.saveStateUrl,
type: 'POST', type: 'POST',
dataType: 'json', dataType: 'json',
data: data data: data,
success: function() {
that.options.storage.setItem('transcript_download_format', fileType);
},
complete: function() {
document.location.href = downloadUrl;
}
}); });
} }
}; };
return VideoAccessibleMenu; return VideoTranscriptDownloadHandler;
}); });
}(RequireJS.define)); }(RequireJS.define));
...@@ -57,7 +57,10 @@ VIDEO_MENUS = { ...@@ -57,7 +57,10 @@ VIDEO_MENUS = {
'language': '.lang .menu', 'language': '.lang .menu',
'speed': '.speed .menu', 'speed': '.speed .menu',
'download_transcript': '.video-tracks .a11y-menu-list', 'download_transcript': '.video-tracks .a11y-menu-list',
'transcript-format': '.video-tracks .a11y-menu-button', 'transcript-format': {
'srt': '.wrapper-download-transcripts .list-download-transcripts .btn-link[data-value="srt"]',
'txt': '.wrapper-download-transcripts .list-download-transcripts .btn-link[data-value="txt"]'
},
'transcript-skip': '.sr-is-focusable.transcript-start', 'transcript-skip': '.sr-is-focusable.transcript-start',
} }
...@@ -584,7 +587,7 @@ class VideoPage(PageObject): ...@@ -584,7 +587,7 @@ class VideoPage(PageObject):
bool: Transcript download result. bool: Transcript download result.
""" """
transcript_selector = self.get_element_selector(VIDEO_MENUS['transcript-format']) transcript_selector = self.get_element_selector(VIDEO_MENUS['transcript-format'][transcript_format])
# check if we have a transcript with correct format # check if we have a transcript with correct format
if '.' + transcript_format not in self.q(css=transcript_selector).text[0]: if '.' + transcript_format not in self.q(css=transcript_selector).text[0]:
...@@ -595,16 +598,15 @@ class VideoPage(PageObject): ...@@ -595,16 +598,15 @@ class VideoPage(PageObject):
'txt': 'text/plain', 'txt': 'text/plain',
} }
transcript_url_selector = self.get_element_selector(VIDEO_BUTTONS['download_transcript']) link = self.q(css=transcript_selector)
url = self.q(css=transcript_url_selector).attrs('href')[0] url = link.attrs('href')[0]
link.click()
result, headers, content = self._get_transcript(url) result, headers, content = self._get_transcript(url)
if result is False: if result is False:
return False return False
if formats[transcript_format] not in headers.get('content-type', ''):
return False
if text_to_search not in content.decode('utf-8'): if text_to_search not in content.decode('utf-8'):
return False return False
...@@ -674,45 +676,6 @@ class VideoPage(PageObject): ...@@ -674,45 +676,6 @@ class VideoPage(PageObject):
selector = self.get_element_selector(VIDEO_MENUS[menu_name]) selector = self.get_element_selector(VIDEO_MENUS[menu_name])
return self.q(css=selector).present return self.q(css=selector).present
def select_transcript_format(self, transcript_format):
"""
Select transcript with format `transcript_format`.
Arguments:
transcript_format (st): Transcript file format `srt` or `txt`.
Returns:
bool: Selection Result.
"""
button_selector = self.get_element_selector(VIDEO_MENUS['transcript-format'])
button = self.q(css=button_selector).results[0]
hover = ActionChains(self.browser).move_to_element(button)
hover.perform()
if '...' not in self.q(css=button_selector).text[0]:
return False
menu_selector = self.get_element_selector(VIDEO_MENUS['download_transcript'])
menu_items = self.q(css=menu_selector + ' a').results
for item in menu_items:
if item.get_attribute('data-value') == transcript_format:
ActionChains(self.browser).move_to_element(item).click().perform()
self.wait_for_ajax()
break
self.browser.execute_script("window.scrollTo(0, 0);")
if self.q(css=menu_selector + ' .active a').attrs('data-value')[0] != transcript_format:
return False
if '.' + transcript_format not in self.q(css=button_selector).text[0]:
return False
return True
@property @property
def sources(self): def sources(self):
""" """
......
...@@ -32,7 +32,7 @@ CLASS_SELECTORS = { ...@@ -32,7 +32,7 @@ CLASS_SELECTORS = {
BUTTON_SELECTORS = { BUTTON_SELECTORS = {
'create_video': 'button[data-category="video"]', 'create_video': 'button[data-category="video"]',
'handout_download': '.video-handout.video-download-button a', 'handout_download': '.wrapper-handouts .btn-link',
'handout_download_editor': '.wrapper-comp-setting.file-uploader .download-action', 'handout_download_editor': '.wrapper-comp-setting.file-uploader .download-action',
'upload_asset': '.upload-action', 'upload_asset': '.upload-action',
'asset_submit': '.action-upload', 'asset_submit': '.action-upload',
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
Acceptance tests for Video. Acceptance tests for Video.
""" """
import os import os
from ddt import ddt, unpack, data
from mock import patch from mock import patch
from nose.plugins.attrib import attr from nose.plugins.attrib import attr
...@@ -199,6 +200,7 @@ class VideoBaseTest(UniqueCourseTest): ...@@ -199,6 +200,7 @@ class VideoBaseTest(UniqueCourseTest):
@attr(shard=4) @attr(shard=4)
@ddt
class YouTubeVideoTest(VideoBaseTest): class YouTubeVideoTest(VideoBaseTest):
""" Test YouTube Video Player """ """ Test YouTube Video Player """
...@@ -491,15 +493,16 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -491,15 +493,16 @@ class YouTubeVideoTest(VideoBaseTest):
self.assertTrue(self.video.is_button_shown('transcript_button')) self.assertTrue(self.video.is_button_shown('transcript_button'))
self._verify_caption_text('Welcome to edX.') self._verify_caption_text('Welcome to edX.')
def test_download_transcript_button_works_correctly(self): @data(('srt', '00:00:00,260'), ('txt', 'Welcome to edX.'))
@unpack
def test_download_transcript_links_work_correctly(self, file_type, search_text):
""" """
Scenario: Download Transcript button works correctly Scenario: Download 'srt' transcript link works correctly.
Download 'txt' transcript link works correctly.
Given the course has Video components A and B in "Youtube" mode Given the course has Video components A and B in "Youtube" mode
And Video component C in "HTML5" mode And Video component C in "HTML5" mode
And I have defined downloadable transcripts for the videos And I have defined downloadable transcripts for the videos
Then I can download a transcript for Video A in "srt" format Then I can download a transcript for Video A in "srt" format
And I can download a transcript for Video A in "txt" format
And I can download a transcript for Video B in "txt" format
And the Download Transcript menu does not exist for Video C And the Download Transcript menu does not exist for Video C
""" """
...@@ -524,19 +527,7 @@ class YouTubeVideoTest(VideoBaseTest): ...@@ -524,19 +527,7 @@ class YouTubeVideoTest(VideoBaseTest):
self.navigate_to_video() self.navigate_to_video()
# check if we can download transcript in "srt" format that has text "00:00:00,260" # check if we can download transcript in "srt" format that has text "00:00:00,260"
self.assertTrue(self.video.downloaded_transcript_contains_text('srt', '00:00:00,260')) self.assertTrue(self.video.downloaded_transcript_contains_text(file_type, search_text))
# select the transcript format "txt"
self.assertTrue(self.video.select_transcript_format('txt'))
# check if we can download transcript in "txt" format that has text "Welcome to edX."
self.assertTrue(self.video.downloaded_transcript_contains_text('txt', 'Welcome to edX.'))
# open vertical containing video "B"
self.course_nav.go_to_vertical('Test Vertical-1')
# check if we can download transcript in "txt" format that has text "Equal transcripts"
self.assertTrue(self.video.downloaded_transcript_contains_text('txt', 'Equal transcripts'))
# open vertical containing video "C" # open vertical containing video "C"
self.course_nav.go_to_vertical('Test Vertical-2') self.course_nav.go_to_vertical('Test Vertical-2')
......
...@@ -39,52 +39,49 @@ from openedx.core.djangolib.js_utils import js_escaped_string ...@@ -39,52 +39,49 @@ from openedx.core.djangolib.js_utils import js_escaped_string
</div> </div>
<div class="focus_grabber last"></div> <div class="focus_grabber last"></div>
<ul class="wrapper-downloads">
% if download_video_link: % if download_video_link or track or handout or branding_info:
<li class="video-sources video-download-button"> <h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
<a href="${download_video_link}">${_('Download video')}</a> <div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
</li> % if download_video_link:
% endif <div class="wrapper-download-video">
% if track: <h4 class="hd hd-5">${_('Video')}</h4>
<li class="video-tracks video-download-button"> <a class="btn-link video-sources video-download-button" href="${download_video_link}">
% if transcript_download_format: ${_('Download video file')}
<a href="${track}">${_('Download transcript')}</a> </a>
<div class="a11y-menu-container"> </div>
<a class="a11y-menu-button" href="#" title="${'.' + transcript_download_format}" role="button" aria-disabled="false">${'.' + transcript_download_format}</a> % endif
<ol class="a11y-menu-list" role="menu"> % if track:
<div class="wrapper-download-transcripts">
<h4 class="hd hd-5">${_('Transcripts')}</h4>
% if transcript_download_format:
<ul class="list-download-transcripts">
% for item in transcript_download_formats_list: % for item in transcript_download_formats_list:
% if item['value'] == transcript_download_format: <li class="transcript-option">
<li class="a11y-menu-item active"> <% dname = _("Download {file}").format(file=item['display_name']) %>
% else: <a class="btn btn-link" href="${track}" data-value="${item['value']}">${dname}</a>
<li class="a11y-menu-item"> </li>
% endif
## This is necessary so we don't scrape 'display_name' as a string.
<% dname = item['display_name'] %>
<a class="a11y-menu-item-link" href="#${item['value']}" title="${_(dname)}" data-value="${item['value']}" role="menuitem" aria-disabled="false">
${_(dname)}
</a>
</li>
% endfor % endfor
</ol> </ul>
</div> % else:
% else: <a class="btn-link external-track" href="${track}">${_('Download transcript')}</a>
<a href="${track}" class="external-track">${_('Download transcript')}</a> % endif
% endif </div>
</li> % endif
% endif % if handout:
% if handout: <div class="wrapper-handouts">
<li class="video-handout video-download-button"> <h4 class="hd hd-5">${_('Handouts')}</h4>
<a href="${handout}" target="_blank">${_('Download Handout')}</a> <a class="btn-link" href="${handout}">${_('Download Handout')}</a>
</li> </div>
% endif % endif
% if branding_info:
% if branding_info: <div class="branding">
<li id="branding" class="branding"> <span class="host-tag">${branding_info['logo_tag']}</span>
<span class="host-tag">${branding_info['logo_tag']}</span> <a href="${branding_info['url']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a>
<a href="${branding_info['url']}" target="_blank" title="${branding_info['logo_tag']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a> </div>
</li> % endif
</div>
% endif % endif
</ul>
</div> </div>
% if cdn_eval: % if cdn_eval:
<script> <script>
......
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