# -*- coding: utf-8 -*- # pylint: disable=missing-docstring from lettuce import world, step, before, after import json import os import time import requests from nose.tools import assert_less, assert_equal, assert_true, assert_false from common import i_am_registered_for_the_course, visit_scenario_item from django.utils.translation import ugettext as _ from django.conf import settings from cache_toolbox.core import del_cached_content from xmodule.contentstore.content import StaticContent from xmodule.contentstore.django import contentstore TEST_ROOT = settings.COMMON_TEST_DATA_ROOT LANGUAGES = settings.ALL_LANGUAGES VIDEO_SOURCE_PORT = settings.VIDEO_SOURCE_PORT ############### ACTIONS #################### HTML5_SOURCES = [ 'http://localhost:{0}/gizmo.mp4'.format(VIDEO_SOURCE_PORT), 'http://localhost:{0}/gizmo.webm'.format(VIDEO_SOURCE_PORT), 'http://localhost:{0}/gizmo.ogv'.format(VIDEO_SOURCE_PORT), ] FLASH_SOURCES = { 'youtube_id_1_0': 'OEoXaMPEzfM', 'youtube_id_0_75': 'JMD_ifUUfsU', 'youtube_id_1_25': 'AKqURZnYqpk', 'youtube_id_1_5': 'DYpADpL7jAY', } HTML5_SOURCES_INCORRECT = [ 'http://localhost:{0}/gizmo.mp99'.format(VIDEO_SOURCE_PORT), ] VIDEO_BUTTONS = { 'CC': '.hide-subtitles', 'volume': '.volume', 'play': '.video_control.play', 'pause': '.video_control.pause', 'fullscreen': '.add-fullscreen', 'download_transcript': '.video-tracks > a', 'quality': '.quality-control', } VIDEO_MENUS = { 'language': '.lang .menu', 'speed': '.speed .menu', 'download_transcript': '.video-tracks .a11y-menu-list', } coursenum = 'test_course' @before.each_scenario def setUp(scenario): world.video_sequences = {} @after.each_scenario def tearDown(scenario): world.browser.cookies.delete('edX_video_player_mode') class RequestHandlerWithSessionId(object): def get(self, url): """ Sends a request. """ kwargs = dict() session_id = [{i['name']:i['value']} for i in world.browser.cookies.all() if i['name'] == u'sessionid'] if session_id: kwargs.update({ 'cookies': session_id[0] }) response = requests.get(url, **kwargs) self.response = response self.status_code = response.status_code self.headers = response.headers self.content = response.content return self def is_success(self): """ Returns `True` if the response was succeed, otherwise, returns `False`. """ if self.status_code < 400: return True return False def check_header(self, name, value): """ Returns `True` if the response header exist and has appropriate value, otherwise, returns `False`. """ if value in self.headers.get(name, ''): return True return False def get_metadata(parent_location, player_mode, data, display_name='Video'): kwargs = { 'parent_location': parent_location, 'category': 'video', 'display_name': display_name, 'metadata': {}, } if player_mode == 'html5': kwargs['metadata'].update({ 'youtube_id_1_0': '', 'youtube_id_0_75': '', 'youtube_id_1_25': '', 'youtube_id_1_5': '', 'html5_sources': HTML5_SOURCES }) if player_mode == 'youtube_html5': kwargs['metadata'].update({ 'html5_sources': HTML5_SOURCES, }) if player_mode == 'youtube_html5_unsupported_video': kwargs['metadata'].update({ 'html5_sources': HTML5_SOURCES_INCORRECT }) if player_mode == 'html5_unsupported_video': kwargs['metadata'].update({ 'youtube_id_1_0': '', 'youtube_id_0_75': '', 'youtube_id_1_25': '', 'youtube_id_1_5': '', 'html5_sources': HTML5_SOURCES_INCORRECT }) if player_mode == 'flash': kwargs['metadata'].update(FLASH_SOURCES) world.browser.cookies.add({'edX_video_player_mode': 'flash'}) if data: conversions = { 'transcripts': json.loads, 'download_track': json.loads, 'download_video': json.loads, } for key in data: if key in conversions: data[key] = conversions[key](data[key]) kwargs['metadata'].update(data) return kwargs def add_videos_to_course(course, player_mode=None, display_names=None, hashes=None): parent_location = add_vertical_to_course(course) kwargs = { 'course': course, 'parent_location': parent_location, 'player_mode': player_mode, 'display_name': display_names[0], } if hashes: for index, item_data in enumerate(hashes): kwargs.update({ 'display_name': display_names[index], 'data': item_data, }) add_video_to_course(**kwargs) else: add_video_to_course(**kwargs) def add_video_to_course(course, parent_location=None, player_mode=None, data=None, display_name='Video'): if not parent_location: parent_location = add_vertical_to_course(course) kwargs = get_metadata(parent_location, player_mode, data, display_name=display_name) world.scenario_dict['VIDEO'] = world.ItemFactory.create(**kwargs) def add_vertical_to_course(course_num): world.scenario_dict['LAST_VERTICAL'] = world.ItemFactory.create( parent_location=world.scenario_dict['SECTION'].location, category='vertical', display_name='Test Vertical-{}'.format(len(set(world.video_sequences.values()))), ) return last_vertical_location(course_num) def last_vertical_location(course_num): return world.scenario_dict['LAST_VERTICAL'].location.replace(course=course_num) def upload_file(filename, location): path = os.path.join(TEST_ROOT, 'uploads/', filename) f = open(os.path.abspath(path)) mime_type = "application/json" content_location = StaticContent.compute_location( location.course_key, filename ) content = StaticContent(content_location, filename, mime_type, f.read()) contentstore().save(content) del_cached_content(content.location) def navigate_to_an_item_in_a_sequence(number): sequence_css = '#sequence-list a[data-element="{0}"]'.format(number) world.css_click(sequence_css) def change_video_speed(speed): world.browser.execute_script("$('.speeds').addClass('is-opened')") speed_css = 'li[data-speed="{0}"] a'.format(speed) world.wait_for_visible('.speeds') world.css_click(speed_css) def open_menu(menu): world.browser.execute_script("$('{selector}').parent().addClass('is-opened')".format( selector=VIDEO_MENUS[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) def duration(): """ Total duration of the video, in seconds. """ elapsed_time, duration = video_time() return duration def elapsed_time(): """ Elapsed time of the video, in seconds. """ elapsed_time, duration = video_time() return elapsed_time 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') # 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)) def parse_time_str(time_str): """ Parse a string of the form 1:23 into seconds (int). """ time_obj = time.strptime(time_str, '%M:%S') return time_obj.tm_min * 60 + time_obj.tm_sec def find_caption_line_by_data_index(index): SELECTOR = ".subtitles > li[data-index='{index}']".format(index=index) return world.css_find(SELECTOR).first def wait_for_video(): world.wait_for_present('.is-initialized') world.wait_for_present('div.vidtime') world.wait_for_invisible('.video-wrapper .spinner') world.wait_for_ajax_complete() @step("I reload the page with video$") def reload_the_page_with_video(_step): _step.given('I reload the page') wait_for_video() @step('youtube stub server (.*) YouTube API') def configure_youtube_api(_step, action): action = action.strip() if action == 'proxies': world.youtube.config['youtube_api_blocked'] = False elif action == 'blocks': world.youtube.config['youtube_api_blocked'] = True else: raise ValueError('Parameter `action` should be one of "proxies" or "blocks".') @step('when I view the (.*) it does not have autoplay enabled$') def does_not_autoplay(_step, video_type): actual = world.css_find('.%s' % video_type)[0]['data-autoplay'] expected = [u'False', u'false', False] assert actual in expected @step('the course has a Video component in "([^"]*)" mode(?:\:)?$') def view_video(_step, player_mode): i_am_registered_for_the_course(_step, coursenum) data = _step.hashes[0] if _step.hashes else None add_video_to_course(coursenum, player_mode=player_mode.lower(), data=data) visit_scenario_item('SECTION') wait_for_video() @step('a video in "([^"]*)" mode(?:\:)?$') def add_video(_step, player_mode): data = _step.hashes[0] if _step.hashes else None add_video_to_course(coursenum, player_mode=player_mode.lower(), data=data) visit_scenario_item('SECTION') wait_for_video() @step('video(?:s)? "([^"]*)" in "([^"]*)" mode in position "([^"]*)" of sequential(?:\:)?$') def add_video_in_position(_step, video_ids, player_mode, position): sequences = {video_id.strip(): position for video_id in video_ids.split(',')} add_videos_to_course(coursenum, player_mode=player_mode.lower(), display_names=sequences.keys(), hashes=_step.hashes) world.video_sequences.update(sequences) @step('I open the section with videos$') def visit_video_section(_step): visit_scenario_item('SECTION') wait_for_video() @step('I select the "([^"]*)" speed$') def i_select_video_speed(_step, 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) @step('I open video "([^"]*)"$') def open_video(_step, player_id): navigate_to_an_item_in_a_sequence(world.video_sequences[player_id]) @step('video "([^"]*)" should start playing at speed "([^"]*)"$') def check_video_speed(_step, player_id, speed): speed_css = '.speeds .value' assert world.css_has_text(speed_css, '{0}x'.format(speed)) @step('youtube server is up and response time is (.*) seconds$') def set_youtube_response_timeout(_step, time): world.youtube.config['time_to_response'] = float(time) @step('the video has rendered in "([^"]*)" mode$') def video_is_rendered(_step, mode): modes = { 'html5': 'video', 'youtube': 'iframe', 'flash': 'iframe', } html_tag = modes[mode.lower()] assert world.css_find('.video {0}'.format(html_tag)).first @step('videos have rendered in "([^"]*)" mode$') def videos_are_rendered(_step, mode): modes = { 'html5': 'video', 'youtube': 'iframe', 'flash': 'iframe', } html_tag = modes[mode.lower()] actual = len(world.css_find('.video {0}'.format(html_tag))) expected = len(world.css_find('.xmodule_VideoModule')) assert actual == expected @step('all sources are correct$') def all_sources_are_correct(_step): elements = world.css_find('.video-player video source') sources = [source['src'].split('?')[0] for source in elements] assert set(sources) == set(HTML5_SOURCES) @step('error message is shown$') def error_message_is_shown(_step): selector = '.video .video-player h3' assert world.css_visible(selector) @step('error message has correct text$') def error_message_has_correct_text(_step): selector = '.video .video-player h3' text = _('ERROR: No playable video sources found!') assert world.css_has_text(selector, text) @step('I make sure captions are (.+)$') def set_captions_visibility_state(_step, captions_state): SELECTOR = '.closed .subtitles' if world.is_css_not_present(SELECTOR): if captions_state == 'closed': world.css_click('.hide-subtitles') else: if captions_state != 'closed': world.css_click('.hide-subtitles') @step('I see video menu "([^"]*)" with correct items$') def i_see_menu(_step, menu): open_menu(menu) menu_items = world.css_find(VIDEO_MENUS[menu] + ' li') video = world.scenario_dict['VIDEO'] transcripts = dict(video.transcripts) if video.sub: transcripts.update({ 'en': video.sub }) languages = {i[0]: i[1] for i in LANGUAGES} transcripts = {k: languages[k] for k in transcripts} for code, label in transcripts.items(): assert any([i.text == label for i in menu_items]) assert any([i['data-lang-code'] == code for i in menu_items]) @step('I see "([^"]*)" text in the captions$') def check_text_in_the_captions(_step, text): world.wait_for_present('.video.is-captions-rendered') 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): world.wait_for_present('.video.is-captions-rendered') for index, video in enumerate(_step.hashes): assert (video.get('text') in world.css_text('.subtitles', index=index)) @step('I select language with code "([^"]*)"$') def select_language(_step, code): world.wait_for_visible('.video-controls') # Make sure that all ajax requests that affects the language menu are finished. # For example, request to get new translation etc. world.wait_for_ajax_complete() selector = VIDEO_MENUS["language"] + ' li[data-lang-code="{code}"]'.format( code=code ) world.css_find(VIDEO_BUTTONS["CC"])[0].mouse_over() world.wait_for_present('.lang.open') world.css_click(selector) assert world.css_has_class(selector, 'is-active') assert len(world.css_find(VIDEO_MENUS["language"] + ' li.is-active')) == 1 # 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() world.wait_for_visible('.subtitles') world.wait_for_present('.video.is-captions-rendered') @step('I click video button "([^"]*)"$') def click_button(_step, button): world.css_click(VIDEO_BUTTONS[button]) if button == "play": # Needs to wait for video buffrization world.wait_for( func=lambda _: world.css_has_class('.video', 'is-playing') and world.is_css_present(VIDEO_BUTTONS['pause']), timeout=30 ) world.wait_for_ajax_complete() @step('I see video slider at "([^"]*)" position$') def start_playing_video_from_n_seconds(_step, time_str): position = parse_time_str(time_str) actual_position = elapsed_time() assert_equal(actual_position, int(position), "Current position is {}, but should be {}".format(actual_position, position)) @step('I see duration "([^"]*)"$') def i_see_duration(_step, position): world.wait_for( func=lambda _: duration() > 0, timeout=30 ) assert duration() == parse_time_str(position) @step('I wait for video controls appear$') def controls_appear(_step): world.wait_for_visible('.video-controls') @step('I seek video to "([^"]*)" position$') def seek_video_to_n_seconds(_step, time_str): time = parse_time_str(time_str) jsCode = "$('.video').data('video-player-state').videoPlayer.onSlideSeek({{time: {0}}})".format(time) world.browser.execute_script(jsCode) world.wait_for( func=lambda _: world.retry_on_exception(lambda: elapsed_time() == time and not world.css_has_class('.video', 'is-buffering')), timeout=30 ) _step.given('I see video slider at "{0}" position'.format(time_str)) @step('I have a "([^"]*)" transcript file in assets$') def upload_to_assets(_step, filename): upload_file(filename, world.scenario_dict['COURSE'].location) @step('menu "([^"]*)" doesn\'t exist$') def is_hidden_menu(_step, menu): assert world.is_css_not_present(VIDEO_MENUS[menu]) @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]) @step('I can download transcript in "([^"]*)" format that has text "([^"]*)"$') def i_can_download_transcript(_step, format, text): assert world.css_has_text('.video-tracks .a11y-menu-button', '.' + format, strip=True) formats = { 'srt': 'application/x-subrip', 'txt': 'text/plain', } url = world.css_find(VIDEO_BUTTONS['download_transcript'])[0]['href'] request = RequestHandlerWithSessionId() assert request.get(url).is_success() assert request.check_header('content-type', formats[format]) assert (text.encode('utf-8') in request.content) @step('I select the transcript format "([^"]*)"$') def select_transcript_format(_step, format): button_selector = '.video-tracks .a11y-menu-button' menu_selector = VIDEO_MENUS['download_transcript'] button = world.css_find(button_selector).first height = button._element.location_once_scrolled_into_view['y'] world.browser.driver.execute_script("window.scrollTo(0, {});".format(height)) button.mouse_over() assert world.css_has_text(button_selector, '...', strip=True) menu_items = world.css_find(menu_selector + ' a') for item in menu_items: if item['data-value'] == format: item.click() world.wait_for_ajax_complete() break world.browser.driver.execute_script("window.scrollTo(0, 0);") assert world.css_find(menu_selector + ' .active a')[0]['data-value'] == format assert world.css_has_text(button_selector, '.' + format, strip=True) @step('video (.*) show the captions$') def shows_captions(_step, show_captions): if 'not' in show_captions or 'n\'t' in show_captions: assert world.is_css_present('div.video.closed') else: assert world.is_css_not_present('div.video.closed') @step('I click on caption line "([^"]*)", video module shows elapsed time "([^"]*)"$') def click_on_the_caption(_step, index, expected_time): world.wait_for_present('.video.is-captions-rendered') find_caption_line_by_data_index(int(index)).click() actual_time = elapsed_time() assert int(expected_time) == actual_time @step('button "([^"]*)" is (hidden|visible)$') def is_hidden_button(_step, button, state): selector = VIDEO_BUTTONS[button] if state == 'hidden': world.wait_for_invisible(selector) assert_false( world.css_visible(selector), 'Button {0} is invisible, but should be visible'.format(button) ) else: world.wait_for_visible(selector) assert_true( world.css_visible(selector), 'Button {0} is visible, but should be invisible'.format(button) ) @step('button "([^"]*)" is (active|inactive)$') def i_see_active_button(_step, button, state): selector = VIDEO_BUTTONS[button] if state == 'active': assert world.css_has_class(selector, 'active') else: assert not world.css_has_class(selector, 'active')