Files
edx-platform/common/test/acceptance/tests/video/test_video_events.py
2016-01-20 12:09:53 -05:00

347 lines
14 KiB
Python

"""Ensure videos emit proper events"""
import datetime
import json
from nose.plugins.attrib import attr
import ddt
import unittest
from ..helpers import EventsTestMixin
from .test_video_module import VideoBaseTest
from ...pages.lms.video.video import _parse_time_str
from openedx.core.lib.tests.assertions.events import assert_event_matches, assert_events_equal
from opaque_keys.edx.keys import UsageKey, CourseKey
class VideoEventsTestMixin(EventsTestMixin, VideoBaseTest):
"""
Useful helper methods to test video player event emission.
"""
def assert_payload_contains_ids(self, video_event):
"""
Video events should all contain "id" and "code" attributes in their payload.
This function asserts that those fields are present and have correct values.
"""
video_descriptors = self.course_fixture.get_nested_xblocks(category='video')
video_desc = video_descriptors[0]
video_locator = UsageKey.from_string(video_desc.locator)
expected_event = {
'event': {
'id': video_locator.html_id(),
'code': '3_yD_cEKoCk'
}
}
self.assert_events_match([expected_event], [video_event])
def assert_valid_control_event_at_time(self, video_event, time_in_seconds):
"""
Video control events should contain valid ID fields and a valid "currentTime" field.
This function asserts that those fields are present and have correct values.
"""
current_time = json.loads(video_event['event'])['currentTime']
self.assertAlmostEqual(current_time, time_in_seconds, delta=1)
def assert_field_type(self, event_dict, field, field_type):
"""Assert that a particular `field` in the `event_dict` has a particular type"""
self.assertIn(field, event_dict, '{0} not found in the root of the event'.format(field))
self.assertTrue(
isinstance(event_dict[field], field_type),
'Expected "{key}" to be a "{field_type}", but it has the value "{value}" of type "{t}"'.format(
key=field,
value=event_dict[field],
t=type(event_dict[field]),
field_type=field_type,
)
)
class VideoEventsTest(VideoEventsTestMixin):
""" Test video player event emission """
def test_video_control_events(self):
"""
Scenario: Video component is rendered in the LMS in Youtube mode without HTML5 sources
Given the course has a Video component in "Youtube" mode
And I play the video
And I watch 5 seconds of it
And I pause the video
Then a "load_video" event is emitted
And a "play_video" event is emitted
And a "pause_video" event is emitted
"""
def is_video_event(event):
"""Filter out anything other than the video events of interest"""
return event['event_type'] in ('load_video', 'play_video', 'pause_video')
captured_events = []
with self.capture_events(is_video_event, number_of_matches=3, captured_events=captured_events):
self.navigate_to_video()
self.video.click_player_button('play')
self.video.wait_for_position('0:05')
self.video.click_player_button('pause')
for idx, video_event in enumerate(captured_events):
self.assert_payload_contains_ids(video_event)
if idx == 0:
assert_event_matches({'event_type': 'load_video'}, video_event)
elif idx == 1:
assert_event_matches({'event_type': 'play_video'}, video_event)
self.assert_valid_control_event_at_time(video_event, 0)
elif idx == 2:
assert_event_matches({'event_type': 'pause_video'}, video_event)
self.assert_valid_control_event_at_time(video_event, self.video.seconds)
def test_strict_event_format(self):
"""
This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new
fields are not added to all events mistakenly. It should be the only existing test that is updated when new top
level fields are added to all events.
"""
captured_events = []
with self.capture_events(lambda e: e['event_type'] == 'load_video', captured_events=captured_events):
self.navigate_to_video()
load_video_event = captured_events[0]
# Validate the event payload
self.assert_payload_contains_ids(load_video_event)
# We cannot predict the value of these fields so we make weaker assertions about them
dynamic_string_fields = (
'accept_language',
'agent',
'host',
'ip',
'event',
'session'
)
for field in dynamic_string_fields:
self.assert_field_type(load_video_event, field, basestring)
self.assertIn(field, load_video_event, '{0} not found in the root of the event'.format(field))
del load_video_event[field]
# A weak assertion for the timestamp as well
self.assert_field_type(load_video_event, 'time', datetime.datetime)
del load_video_event['time']
# Note that all unpredictable fields have been deleted from the event at this point
course_key = CourseKey.from_string(self.course_id)
static_fields_pattern = {
'context': {
'course_id': unicode(course_key),
'org_id': course_key.org,
'path': '/event',
'user_id': self.user_info['user_id']
},
'event_source': 'browser',
'event_type': 'load_video',
'username': self.user_info['username'],
'page': self.browser.current_url,
'referer': self.browser.current_url,
'name': 'load_video',
}
assert_events_equal(static_fields_pattern, load_video_event)
@attr('shard_8')
@ddt.ddt
class VideoBumperEventsTest(VideoEventsTestMixin):
""" Test bumper video event emission """
# helper methods
def watch_video_and_skip(self):
"""
Wait 5 seconds and press "skip" button.
"""
self.video.wait_for_position('0:05')
self.video.click_player_button('skip_bumper')
def watch_video_and_dismiss(self):
"""
Wait 5 seconds and press "do not show again" button.
"""
self.video.wait_for_position('0:05')
self.video.click_player_button('do_not_show_again')
def wait_for_state(self, state='finished'):
"""
Wait until video will be in given state.
Finished state means that video is played to the end.
"""
self.video.wait_for_state(state)
def add_bumper(self):
"""
Add video bumper to the course.
"""
additional_data = {
u'video_bumper': {
u'value': {
"transcripts": {},
"video_id": "video_001"
}
}
}
self.course_fixture.add_advanced_settings(additional_data)
@ddt.data(
('edx.video.bumper.skipped', watch_video_and_skip),
('edx.video.bumper.dismissed', watch_video_and_dismiss),
('edx.video.bumper.stopped', wait_for_state)
)
@ddt.unpack
def test_video_control_events(self, event_type, action):
"""
Scenario: Video component with pre-roll emits events correctly
Given the course has a Video component in "Youtube" mode with pre-roll enabled
And I click on the video poster
And the pre-roll video start playing
And I watch (5 seconds/5 seconds/to the end of) it
And I click (skip/do not show again) video button
Then a "edx.video.bumper.loaded" event is emitted
And a "edx.video.bumper.played" event is emitted
And a "edx.video.bumper.skipped/dismissed/stopped" event is emitted
And a "load_video" event is emitted
And a "play_video" event is emitted
"""
def is_video_event(event):
"""Filter out anything other than the video events of interest"""
return event['event_type'] in (
'edx.video.bumper.loaded',
'edx.video.bumper.played',
'edx.video.bumper.skipped',
'edx.video.bumper.dismissed',
'edx.video.bumper.stopped',
'load_video',
'play_video',
'pause_video'
) and self.video.state != 'buffering'
captured_events = []
self.add_bumper()
with self.capture_events(is_video_event, number_of_matches=5, captured_events=captured_events):
self.navigate_to_video_no_render()
self.video.click_on_poster()
self.video.wait_for_video_bumper_render()
sources, duration = self.video.sources[0], self.video.duration
action(self)
# Filter subsequent events that appear due to bufferisation: edx.video.bumper.played
# As bumper does not emit pause event, we filter subsequent edx.video.bumper.played events from
# the list, except first.
filtered_events = []
for video_event in captured_events:
is_played_event = video_event['event_type'] == 'edx.video.bumper.played'
appears_again = filtered_events and video_event['event_type'] == filtered_events[-1]['event_type']
if is_played_event and appears_again:
continue
filtered_events.append(video_event)
for idx, video_event in enumerate(filtered_events):
if idx < 3:
self.assert_bumper_payload_contains_ids(video_event, sources, duration)
else:
self.assert_payload_contains_ids(video_event)
if idx == 0:
assert_event_matches({'event_type': 'edx.video.bumper.loaded'}, video_event)
elif idx == 1:
assert_event_matches({'event_type': 'edx.video.bumper.played'}, video_event)
self.assert_valid_control_event_at_time(video_event, 0)
elif idx == 2:
assert_event_matches({'event_type': event_type}, video_event)
elif idx == 3:
assert_event_matches({'event_type': 'load_video'}, video_event)
elif idx == 4:
assert_event_matches({'event_type': 'play_video'}, video_event)
self.assert_valid_control_event_at_time(video_event, 0)
def assert_bumper_payload_contains_ids(self, video_event, sources, duration):
"""
Bumper video events should all contain "host_component_id", "bumper_id",
"duration", "code" attributes in their payload.
This function asserts that those fields are present and have correct values.
"""
self.add_bumper()
video_descriptors = self.course_fixture.get_nested_xblocks(category='video')
video_desc = video_descriptors[0]
video_locator = UsageKey.from_string(video_desc.locator)
expected_event = {
'event': {
'host_component_id': video_locator.html_id(),
'bumper_id': sources,
'duration': _parse_time_str(duration),
'code': 'html5'
}
}
self.assert_events_match([expected_event], [video_event])
def test_strict_event_format(self):
"""
This test makes a very strong assertion about the fields present in events. The goal of it is to ensure that new
fields are not added to all events mistakenly. It should be the only existing test that is updated when new top
level fields are added to all events.
"""
captured_events = []
self.add_bumper()
filter_event = lambda e: e['event_type'] == 'edx.video.bumper.loaded'
with self.capture_events(filter_event, captured_events=captured_events):
self.navigate_to_video_no_render()
self.video.click_on_poster()
load_video_event = captured_events[0]
# Validate the event payload
sources, duration = self.video.sources[0], self.video.duration
self.assert_bumper_payload_contains_ids(load_video_event, sources, duration)
# We cannot predict the value of these fields so we make weaker assertions about them
dynamic_string_fields = (
'accept_language',
'agent',
'host',
'ip',
'event',
'session'
)
for field in dynamic_string_fields:
self.assert_field_type(load_video_event, field, basestring)
self.assertIn(field, load_video_event, '{0} not found in the root of the event'.format(field))
del load_video_event[field]
# A weak assertion for the timestamp as well
self.assert_field_type(load_video_event, 'time', datetime.datetime)
del load_video_event['time']
# Note that all unpredictable fields have been deleted from the event at this point
course_key = CourseKey.from_string(self.course_id)
static_fields_pattern = {
'context': {
'course_id': unicode(course_key),
'org_id': course_key.org,
'path': '/event',
'user_id': self.user_info['user_id']
},
'event_source': 'browser',
'event_type': 'edx.video.bumper.loaded',
'username': self.user_info['username'],
'page': self.browser.current_url,
'referer': self.browser.current_url,
'name': 'edx.video.bumper.loaded',
}
assert_events_equal(static_fields_pattern, load_video_event)