Commit 936bd4d5 by Muhammad Ammar

Bok-Choy CMS Video Tests

parent 619f72f3
......@@ -2,83 +2,7 @@
Feature: CMS Video Component
As a course author, I want to be able to view my created videos in Studio
# 1
Scenario: YouTube stub server proxies YouTube API correctly
Given youtube stub server proxies YouTube API
And I have created a Video component
Then I can see video button "play"
And I click video button "play"
Then I can see video button "pause"
# 2
Scenario: YouTube stub server can block YouTube API
Given youtube stub server blocks YouTube API
And I have created a Video component
And I wait for "3" seconds
Then I do not see video button "play"
# 3
Scenario: Autoplay is disabled in Studio
Given I have created a Video component
Then when I view the video it does not have autoplay enabled
# 4
Scenario: Creating a video takes a single click
Given I have clicked the new unit button
Then creating a video takes a single click
# 5
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are hidden correctly
Given I have created a Video component with subtitles
And I have hidden captions
Then when I view the video it does not show the captions
# 6
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are shown correctly
Given I have created a Video component with subtitles
Then when I view the video it does show the captions
# 7
# Sauce Labs cannot delete cookies
@skip_sauce
Scenario: Captions are toggled correctly
Given I have created a Video component with subtitles
And I have toggled captions
Then when I view the video it does show the captions
# 8
Scenario: Video data is shown correctly
Given I have created a video with only XML data
And I reload the page
Then the correct Youtube video is shown
# 9
# Disabled 11/26 due to flakiness in master.
# Enabled back on 11/29.
Scenario: When enter key is pressed on a caption shows an outline around it
Given I have created a Video component with subtitles
And Make sure captions are opened
Then I focus on caption line with data-index "0"
Then I press "enter" button on caption line with data-index "0"
And I see caption line with data-index "0" has class "focused"
# 10
Scenario: When start and end times are specified, a range on slider is shown
Given I have created a Video component with subtitles
And Make sure captions are closed
And I edit the component
And I open tab "Advanced"
And I set value "00:00:12" to the field "Video Start Time"
And I set value "00:00:24" to the field "Video Stop Time"
And I save changes
And I click video button "play"
Then I see a range on slider
# 11
# Disabled 2/19/14 after intermittent failures in master
#Scenario: Check that position is stored on page refresh, position within start-end range
# Given I have created a Video component with subtitles
......@@ -96,7 +20,7 @@ Feature: CMS Video Component
# And I click video button "play"
# Then I see video starts playing from "0:16" position
# 12
# 3
# Disabled 2/18/14 after intermittent failures in master
# Scenario: Check that position is stored on page refresh, position before start-end range
# Given I have created a Video component with subtitles
......@@ -114,7 +38,7 @@ Feature: CMS Video Component
# And I click video button "play"
# Then I see video starts playing from "0:12" position
# 13
# 4
# Disabled 2/18/14 after intermittent failures in master
# Scenario: Check that position is stored on page refresh, position after start-end range
# Given I have created a Video component with subtitles
......
......@@ -55,7 +55,10 @@ class StubYouTubeHandler(StubHttpRequestHandler):
"Youtube provider received GET request to path {}".format(self.path)
)
if 'test_transcripts_youtube' in self.path:
if 'get_config' in self.path:
self.send_json_response(self.server.config)
elif 'test_transcripts_youtube' in self.path:
if 't__eq_exist' in self.path:
status_message = "".join([
......
......@@ -122,20 +122,24 @@ class VideoPage(PageObject):
else:
return '.vert.vert-0'
def get_element_selector(self, class_name):
def get_element_selector(self, class_name, vertical=True):
"""
Construct unique element selector.
Arguments:
class_name (str): css class name for an element.
vertical (bool): do we need vertical css selector or not. vertical css selector is not present in Studio
Returns:
str: Element Selector.
"""
return '{vertical} {video_element}'.format(
vertical=self.get_video_vertical_selector(self.current_video_display_name),
video_element=class_name)
if vertical:
return '{vertical} {video_element}'.format(
vertical=self.get_video_vertical_selector(self.current_video_display_name),
video_element=class_name)
else:
return class_name
def use_video(self, video_display_name):
"""
......@@ -253,6 +257,17 @@ class VideoPage(PageObject):
"""
self._captions_visibility(False)
def is_captions_visible(self):
"""
Get current visibility sate of captions.
Returns:
bool: True means captions are visible, False means captions are not visible
"""
caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
return not self.q(css=caption_state_selector).present
@wait_for_js
def _captions_visibility(self, captions_new_state):
"""
......@@ -265,28 +280,16 @@ class VideoPage(PageObject):
states = {True: 'Shown', False: 'Hidden'}
state = states[captions_new_state]
caption_state_selector = self.get_element_selector(CSS_CLASS_NAMES['closed_captions'])
def _captions_current_state():
"""
Get current visibility sate of captions.
Returns:
bool: True means captions are visible, False means captions are not visible
"""
return not self.q(css=caption_state_selector).present
# Make sure that the CC button is there
EmptyPromise(lambda: self.is_button_shown('CC'),
"CC button is shown").fulfill()
# toggle captions visibility state if needed
if _captions_current_state() != captions_new_state:
if self.is_captions_visible() != captions_new_state:
self.click_player_button('CC')
# Verify that captions state is toggled/changed
EmptyPromise(lambda: _captions_current_state() == captions_new_state,
EmptyPromise(lambda: self.is_captions_visible() == captions_new_state,
"Captions are {state}".format(state=state)).fulfill()
@property
......
......@@ -4,20 +4,27 @@ CMS Video
import os
import requests
from bok_choy.page_object import PageObject
from bok_choy.promise import EmptyPromise, Promise
from bok_choy.javascript import wait_for_js, js_defined
from ....tests.helpers import YouTubeStubConfig
from ...lms.video.video import VideoPage
from selenium.webdriver.common.keys import Keys
CLASS_SELECTORS = {
'video_container': 'div.video',
'video_init': '.is-initialized',
'video_xmodule': '.xmodule_VideoModule',
'video_spinner': '.video-wrapper .spinner',
'video_controls': 'section.video-controls',
'attach_handout': '.upload-dialog > input[type="file"]',
'upload_dialog': '.wrapper-modal-window-assetupload',
'xblock': '.add-xblock-component',
'slider_range': '.slider-range',
}
BUTTON_SELECTORS = {
'create_video': 'a[data-category="video"]',
'handout_download': '.video-handout.video-download-button a',
'handout_download_editor': '.wrapper-comp-setting.file-uploader .download-action',
'upload_handout': '.upload-action',
......@@ -28,7 +35,7 @@ BUTTON_SELECTORS = {
@js_defined('window.Video', 'window.RequireJS.require', 'window.jQuery', 'window.XModule', 'window.XBlock',
'window.MathJax.isReady')
class VidoComponentPage(PageObject):
class VideoComponentPage(VideoPage):
"""
CMS Video Component Page
"""
......@@ -37,7 +44,11 @@ class VidoComponentPage(PageObject):
@wait_for_js
def is_browser_on_page(self):
return self.q(css='div{0}'.format(CLASS_SELECTORS['video_xmodule'])).present
return self.q(css='div{0}'.format(CLASS_SELECTORS['video_xmodule'])).present or self.q(
css='div{0}'.format(CLASS_SELECTORS['xblock'])).present
def get_element_selector(self, class_name, vertical=False):
return super(VideoComponentPage, self).get_element_selector(class_name, vertical=vertical)
def _wait_for(self, check_func, desc, result=False, timeout=30):
"""
......@@ -59,9 +70,10 @@ class VidoComponentPage(PageObject):
"""
Wait until video component rendered completely
"""
self._wait_for(lambda: self.q(css=CLASS_SELECTORS['video_init']).present, 'Video Player Initialized')
self._wait_for(lambda: not self.q(css=CLASS_SELECTORS['video_spinner']).visible, 'Video Buffering Completed')
self._wait_for(lambda: self.q(css=CLASS_SELECTORS['video_controls']).visible, 'Player Controls are Visible')
if not YouTubeStubConfig.get_configuration().get('youtube_api_blocked'):
self._wait_for(lambda: self.q(css=CLASS_SELECTORS['video_init']).present, 'Video Player Initialized')
self._wait_for(lambda: not self.q(css=CLASS_SELECTORS['video_spinner']).visible, 'Video Buffering Completed')
self._wait_for(lambda: self.q(css=CLASS_SELECTORS['video_controls']).visible, 'Player Controls are Visible')
def click_button(self, button_name):
"""
......@@ -74,6 +86,17 @@ class VidoComponentPage(PageObject):
self.q(css=BUTTON_SELECTORS[button_name]).first.click()
self.wait_for_ajax()
@staticmethod
def file_path(filename):
"""
Construct file path to be uploaded to assets.
Arguments:
filename (str): asset filename
"""
return os.sep.join(__file__.split(os.sep)[:-5]) + '/data/uploads/' + filename
def upload_handout(self, handout_filename):
"""
Upload a handout file to assets
......@@ -82,7 +105,7 @@ class VidoComponentPage(PageObject):
handout_filename (str): handout file name
"""
handout_path = os.sep.join(__file__.split(os.sep)[:-5]) + '/data/uploads/' + handout_filename
handout_path = self.file_path(handout_filename)
self.click_button('upload_handout')
......@@ -137,6 +160,77 @@ class VidoComponentPage(PageObject):
"""
Check if handout download button is visible
"""
# TODO! Remove .present below after bok-choy is updated to latest commit, Only .visible is enough
return self.q(css=BUTTON_SELECTORS['handout_download']).present and self.q(
css=BUTTON_SELECTORS['handout_download']).visible
return self.q(css=BUTTON_SELECTORS['handout_download']).visible
def create_video(self):
"""
Create a Video Component by clicking on Video button and wait for rendering to complete.
"""
# Create video
self.click_button('create_video')
self.wait_for_video_component_render()
def xblocks(self):
"""
Tells the total number of video xblocks present on current unit page.
Returns:
(int): total video xblocks
"""
return len(self.q(css='.xblock-header').filter(
lambda el: 'xblock-header-video' in el.get_attribute('class')).results)
def focus_caption_line(self, line_number):
"""
Focus a caption line as specified by `line_number`
Arguments:
line_number (int): caption line number
"""
caption_line_selector = ".subtitles > li[data-index='{index}']".format(index=line_number - 1)
self.q(css=caption_line_selector).results[0].send_keys(Keys.ENTER)
def is_caption_line_focused(self, line_number):
"""
Check if a caption line focused
Arguments:
line_number (int): caption line number
"""
caption_line_selector = ".subtitles > li[data-index='{index}']".format(index=line_number - 1)
attributes = self.q(css=caption_line_selector).attrs('class')
return 'focused' in attributes
def set_settings_field_value(self, field, value):
"""
In Advanced Tab set `field` with `value`
Arguments:
field (str): field name
value (str): field value
"""
query = '.wrapper-comp-setting > label:nth-child(1)'
field_id = ''
for index, _ in enumerate(self.q(css=query)):
if field in self.q(css=query).nth(index).text[0]:
field_id = self.q(css=query).nth(index).attrs('for')[0]
break
self.q(css='#{}'.format(field_id)).fill(value)
@property
def is_slider_range_visible(self):
"""
Check if slider range visible.
Returns:
bool: slider range is visible or not
"""
return self.q(css=CLASS_SELECTORS['slider_range']).visible
"""
Test helper functions and base classes.
"""
import json
import unittest
import functools
import requests
......@@ -194,3 +195,77 @@ class UniqueCourseTest(WebAppTest):
self.course_info['number'],
self.course_info['run']
])
class YouTubeConfigError(Exception):
"""
Error occurred while configuring YouTube Stub Server.
"""
pass
class YouTubeStubConfig(object):
"""
Configure YouTube Stub Server.
"""
PORT = 9080
URL = 'http://127.0.0.1:{}/'.format(PORT)
@classmethod
def configure(cls, config):
"""
Allow callers to configure the stub server using the /set_config URL.
Arguments:
config (dict): Configuration dictionary.
Raises:
YouTubeConfigError
"""
youtube_stub_config_url = cls.URL + 'set_config'
config_data = {param: json.dumps(value) for param, value in config.items()}
response = requests.put(youtube_stub_config_url, data=config_data)
if not response.ok:
raise YouTubeConfigError(
'YouTube Server Configuration Failed. URL {0}, Configuration Data: {1}, Status was {2}'.format(
youtube_stub_config_url, config, response.status_code))
@classmethod
def reset(cls):
"""
Reset YouTube Stub Server Configurations using the /del_config URL.
Raises:
YouTubeConfigError
"""
youtube_stub_config_url = cls.URL + 'del_config'
response = requests.delete(youtube_stub_config_url)
if not response.ok:
raise YouTubeConfigError(
'YouTube Server Configuration Failed. URL: {0} Status was {1}'.format(
youtube_stub_config_url, response.status_code))
@classmethod
def get_configuration(cls):
"""
Allow callers to get current stub server configuration.
Returns:
dict
"""
youtube_stub_config_url = cls.URL + 'get_config'
response = requests.get(youtube_stub_config_url)
if response.ok:
return json.loads(response.content)
else:
return {}
......@@ -7,9 +7,9 @@ Acceptance tests for CMS Video Module.
from unittest import skipIf
from ...pages.studio.auto_auth import AutoAuthPage
from ...pages.studio.overview import CourseOutlinePage
from ...pages.studio.video.video import VidoComponentPage
from ...pages.studio.video.video import VideoComponentPage
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
from ..helpers import UniqueCourseTest, is_youtube_available
from ..helpers import UniqueCourseTest, is_youtube_available, YouTubeStubConfig
@skipIf(is_youtube_available() is False, 'YouTube is not available!')
......@@ -24,7 +24,7 @@ class CMSVideoBaseTest(UniqueCourseTest):
"""
super(CMSVideoBaseTest, self).setUp()
self.video = VidoComponentPage(self.browser)
self.video = VideoComponentPage(self.browser)
# This will be initialized later
self.unit_page = None
......@@ -41,6 +41,8 @@ class CMSVideoBaseTest(UniqueCourseTest):
self.course_info['run'], self.course_info['display_name']
)
self.assets = []
def _install_course_fixture(self):
"""
Prepare for tests by creating a course with a section, subsection, and unit.
......@@ -49,13 +51,15 @@ class CMSVideoBaseTest(UniqueCourseTest):
Create a user and make that user a course author
Log the user into studio
"""
if self.assets:
self.course_fixture.add_asset(self.assets)
# Create course with Video component
self.course_fixture.add_children(
XBlockFixtureDesc('chapter', 'Test Section').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection').add_children(
XBlockFixtureDesc("vertical", "Test Unit").add_children(
XBlockFixtureDesc('video', 'Video'),
XBlockFixtureDesc('vertical', 'Test Unit').add_children(
XBlockFixtureDesc('video', 'Video')
)
)
)
......@@ -95,17 +99,194 @@ class CMSVideoBaseTest(UniqueCourseTest):
"""
Open component Edit Dialog for first component on page.
"""
self.unit_page.xblocks[0].edit()
# The 0th entry is the unit page itself.
self.unit_page.xblocks[1].edit()
def open_advanced_tab(self):
"""
Open components advanced tab.
"""
self.unit_page.xblocks[0].open_advanced_tab()
# The 0th entry is the unit page itself.
self.unit_page.xblocks[1].open_advanced_tab()
def save_unit_settings(self):
"""
Save component settings.
"""
self.unit_page.xblocks[0].save_settings()
# The 0th entry is the unit page itself.
self.unit_page.xblocks[1].save_settings()
class CMSVideoTest(CMSVideoBaseTest):
"""
CMS Video Test Class
"""
def setUp(self):
super(CMSVideoTest, self).setUp()
self.addCleanup(YouTubeStubConfig.reset)
def _create_course_unit(self, youtube_stub_config=None, subtitles=False):
"""
Create a Studio Video Course Unit and Navigate to it.
Arguments:
youtube_stub_config (dict)
subtitles (bool)
"""
if youtube_stub_config:
YouTubeStubConfig.configure(youtube_stub_config)
if subtitles:
self.assets.append('subs_OEoXaMPEzfM.srt.sjson')
self.navigate_to_course_unit()
def _create_video(self):
"""
Create Xblock Video Component.
"""
self.video.create_video()
video_xblocks = self.video.xblocks()
# Total video xblock components count should be equals to 2
# Why 2? One video component is created by default for each test. Please see
# test_studio_video_module.py:CMSVideoTest._create_course_unit
# And we are creating second video component here.
self.assertTrue(video_xblocks == 2)
def test_youtube_stub_proxy(self):
"""
Scenario: YouTube stub server proxies YouTube API correctly
Given youtube stub server proxies YouTube API
And I have created a Video component
Then I can see video button "play"
And I click video button "play"
Then I can see video button "pause"
"""
self._create_course_unit(youtube_stub_config={'youtube_api_blocked': False})
self.assertTrue(self.video.is_button_shown('play'))
self.video.click_player_button('play')
self.assertTrue(self.video.is_button_shown('pause'))
def test_youtube_stub_blocks_youtube_api(self):
"""
Scenario: YouTube stub server can block YouTube API
Given youtube stub server blocks YouTube API
And I have created a Video component
Then I do not see video button "play"
"""
self._create_course_unit(youtube_stub_config={'youtube_api_blocked': True})
self.assertFalse(self.video.is_button_shown('play'))
def test_autoplay_is_disabled(self):
"""
Scenario: Autoplay is disabled in Studio
Given I have created a Video component
Then when I view the video it does not have autoplay enabled
"""
self._create_course_unit()
self.assertFalse(self.video.is_autoplay_enabled)
def test_video_creation_takes_single_click(self):
"""
Scenario: Creating a video takes a single click
And creating a video takes a single click
"""
self._create_course_unit()
# This will create a video by doing a single click and then ensure that video is created
self._create_video()
def test_captions_hidden_correctly(self):
"""
Scenario: Captions are hidden correctly
Given I have created a Video component with subtitles
And I have hidden captions
Then when I view the video it does not show the captions
"""
self._create_course_unit(subtitles=True)
self.video.hide_captions()
self.assertFalse(self.video.is_captions_visible())
def test_captions_shown_correctly(self):
"""
Scenario: Captions are shown correctly
Given I have created a Video component with subtitles
Then when I view the video it does show the captions
"""
self._create_course_unit(subtitles=True)
self.assertTrue(self.video.is_captions_visible())
def test_captions_toggling(self):
"""
Scenario: Captions are toggled correctly
Given I have created a Video component with subtitles
And I have toggled captions
Then when I view the video it does show the captions
"""
self._create_course_unit(subtitles=True)
self.video.click_player_button('CC')
self.assertFalse(self.video.is_captions_visible())
self.video.click_player_button('CC')
self.assertTrue(self.video.is_captions_visible())
def test_caption_line_focus(self):
"""
Scenario: When enter key is pressed on a caption, an outline shows around it
Given I have created a Video component with subtitles
And Make sure captions are opened
Then I focus on first caption line
And I see first caption line has focused
"""
self._create_course_unit(subtitles=True)
self.video.show_captions()
self.video.focus_caption_line(1)
self.assertTrue(self.video.is_caption_line_focused(1))
def test_slider_range_works(self):
"""
Scenario: When start and end times are specified, a range on slider is shown
Given I have created a Video component with subtitles
And Make sure captions are closed
And I edit the component
And I open tab "Advanced"
And I set value "00:00:12" to the field "Video Start Time"
And I set value "00:00:24" to the field "Video Stop Time"
And I save changes
And I click video button "play"
Then I see a range on slider
"""
self._create_course_unit(subtitles=True)
self.video.hide_captions()
self.edit_component()
self.open_advanced_tab()
self.video.set_settings_field_value('Video Start Time', '00:00:12')
self.video.set_settings_field_value('Video Stop Time', '00:00:24')
self.save_unit_settings()
self.video.click_player_button('play')
self.assertTrue(self.video.is_slider_range_visible)
......@@ -4,10 +4,8 @@
Acceptance tests for Video.
"""
import json
from unittest import skipIf, skip
import requests
from ..helpers import UniqueCourseTest, is_youtube_available
from ..helpers import UniqueCourseTest, is_youtube_available, YouTubeStubConfig
from ...pages.lms.video.video import VideoPage
from ...pages.lms.tab_nav import TabNavPage
from ...pages.lms.course_nav import CourseNavPage
......@@ -18,8 +16,6 @@ from ..helpers import skip_if_browser
VIDEO_SOURCE_PORT = 8777
YOUTUBE_STUB_PORT = 9080
YOUTUBE_STUB_URL = 'http://127.0.0.1:{}/'.format(YOUTUBE_STUB_PORT)
HTML5_SOURCES = [
'http://localhost:{0}/gizmo.mp4'.format(VIDEO_SOURCE_PORT),
......@@ -32,13 +28,6 @@ HTML5_SOURCES_INCORRECT = [
]
class YouTubeConfigError(Exception):
"""
Error occurred while configuring YouTube Stub Server.
"""
pass
@skipIf(is_youtube_available() is False, 'YouTube is not available!')
class VideoBaseTest(UniqueCourseTest):
"""
......@@ -68,7 +57,7 @@ class VideoBaseTest(UniqueCourseTest):
self.youtube_configuration = {}
# reset youtube stub server
self.addCleanup(self._reset_youtube_stub_server)
self.addCleanup(YouTubeStubConfig.reset)
def navigate_to_video(self):
""" Prepare the course and get to the video and render it """
......@@ -96,7 +85,7 @@ class VideoBaseTest(UniqueCourseTest):
self.course_fixture.install()
if len(self.youtube_configuration) > 0:
self._configure_youtube_stub_server(self.youtube_configuration)
YouTubeStubConfig.configure(self.youtube_configuration)
def _add_course_verticals(self):
"""
......@@ -148,39 +137,6 @@ class VideoBaseTest(UniqueCourseTest):
self._navigate_to_courseware_video()
self.video.wait_for_video_class()
def _configure_youtube_stub_server(self, config):
"""
Allow callers to configure the stub server using the /set_config URL.
:param config: Configuration dictionary.
The request should have PUT data, such that:
Each PUT parameter is the configuration key.
Each PUT value is a JSON-encoded string value for the configuration.
:raise YouTubeConfigError:
"""
youtube_stub_config_url = YOUTUBE_STUB_URL + 'set_config'
config_data = {param: json.dumps(value) for param, value in config.items()}
response = requests.put(youtube_stub_config_url, data=config_data)
if not response.ok:
raise YouTubeConfigError(
'YouTube Server Configuration Failed. URL {0}, Configuration Data: {1}, Status was {2}'.format(
youtube_stub_config_url, config, response.status_code))
def _reset_youtube_stub_server(self):
"""
Reset YouTube Stub Server Configurations using the /del_config URL.
:raise YouTubeConfigError:
"""
youtube_stub_config_url = YOUTUBE_STUB_URL + 'del_config'
response = requests.delete(youtube_stub_config_url)
if not response.ok:
raise YouTubeConfigError(
'YouTube Server Configuration Failed. URL: {0} Status was {1}'.format(
youtube_stub_config_url, response.status_code))
def metadata_for_mode(self, player_mode, additional_data=None):
"""
Create a dictionary for video player configuration according to `player_mode`
......
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